Blog
RubyistがScalaを勉強してみた
初めまして!新卒のフィです。
RightSegmentという子会社でエンジニアをやっています。
大学時代からずっとRubyやPython, JavascriptなどのLL系の言語を使用しており、たまにJavaでAndroidアプリを書くぐらいなので、型言語を触るのは久しぶりです。アドテクスタジオでは多くのプロダクトでScalaが利用されており、今回はじめてScalaについて調査しました。
Scala言語は調べれば調べるほど楽しそうですが僕はプログラミング言語の中ではRubyが一番好きなので、Ruby言語の特徴をScalaで再現してみました。
- method_missingを使ったActiveRecordのパターン
- 既存の演算子をオーバーライドできる
- Duck Typingができる
この三つのパターンをScalaで実現するか、また実現できたらどうなるかをやってみました。
今回の記事では、ActiveRecordパターンをScalaで実現することを試しました。
ダイナミックにActive Recordのパターンを実現
1. ActiveRecordパターンとは
Wikipediaの定義により
Active Recordはデータベースからデータを読み出すためのアプローチである。データベーステーブルあるいはビューの1行が1つのクラスにラップされ、オブジェクトのインスタンスがそのデータベースの1つの行に結合される
Ruby On Railsには、例えば、People というデータベーステーブルがあります。そのテーブルの中に、name, ageというフィルドがあります。そのテーブルに紐づくモデルが Person とすると(注:Railsではテーブル名が複数形、コード上のモデル名は単数系ではないといけない)
1 2 |
Person.find_by_name(name) Person.find_by_age(age) |
の二つのメソッドが自動的に作成されます。(不思議ですよね。)
Rubyでは、もしメソッドが見つからない場合は、ExceptionをRaiseする前に、method_missingというプロキシメソッドを通って、見つからない関数名+パラメータをmethod_missingに渡す挙動を取ります。
そのため、Personのmethod_missingメソッドをオーバーライドすることにより、ある名前に該当するメソッドをランタイムに作れます。
1 2 3 4 5 6 7 8 9 10 |
class People def method_missing(name) #もしメソッド名がfind_byで始まる場合 if name.to_s =~ /^find_by(\d+)$/ #自分の好きな処理をする else super end end end |
みたいな感じで、メソッドがランタイムに作れます。
ではScalaではどのようにmethod_missingを実現するか見ていきましょう。
2. ScalaでDynamicにActiveRecordパターンを実現する
2.1 自由のメソッドを作れるようにDynamic traitを活用
Scalaでは、メソッドが見つからない場合はどうなるかを実装してみると
1 2 |
object A { } A.foo |
コンパイルしてみると
1 2 3 4 5 |
#error: expected class or object definition #A.foo #^ #one error found |
コンパイルするときに怒られました。
ウェブで探してみると、DynamicというタイプがScala 2.10から追加され, Dynamicは Scala で”動的にメソッドやフィールドを定義するため”のものです!
実現したイメージがDynamicで実現できそうなので、早速試してみます。
1 2 3 4 5 6 |
import scala.language.dynamics class People extends Dynamic { } val me = new Person() me.findByName() |
でコンパイルしてみたら、
1 |
error: value applyDynamic is not a member of object People |
また、コンパイルするときに怒られました。
Scala-lang.orgの資料を読んでみると、以下の例があります:
1 |
foo.method("blah") #foo.applyDynamic("method")("blah") |
なるほど、つまり存在していないメソッドは自動的にapplyDynamicというメソッドにディスパッチするみたいですね。そのまめ、Personクラスの中にapplyDynamicを定義しないといけない。資料をみたら、applyDynamicは二つのパラメータを受け取って、一つ目はメソッド名、二つ目はパラメータの配列です。
コードを書き直してみますと:
1 2 3 4 5 6 7 8 |
class Person extends Dynamic { def applyDynamic(methodName: String)(args: Any*) { println("i'm dynamic, yay!") } } me = new Person() me.findByName() |
1 |
#i'm dynamic, yay! |
と出力されました!
やった!Rubyライクにメソッド見つからないときにプロキシメソッドを通して、自由な処理ができるようになりました。
2.2 RubyのActiveRecordのようにDB接続まで書いてみよう
JDBCを利用してデータベースに接続してみます 。例なのでデータベースconnection管理の部分は省略します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//DBの処理をラップするためのクラス package activerecord import java.sql.{ResultSet, DriverManager} class DatabaseWrapper ( val dbName: String, val userName: String, val passWord: String ) { Class.forName("com.mysql.jdbc.Driver").newInstance() val jdbcString = s"jdbc:mysql://localhost/$dbName?user=$userName&password=$passWord" val connection = DriverManager.getConnection(jdbcString) def query(queryString: String): ResultSet = { val statement = connection.createStatement() statement.executeQuery(queryString) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//ActiveRecordパターンを実現するためのクラス package activerecord import java.sql.ResultSet import java.util.logging.Logger import scala.language.dynamics class ActiveRecord(_db: DatabaseWrapper) extends Dynamic { val db:DatabaseWrapper = _db val tableName = inferenceTableNameByClassName() val logger = Logger.getLogger("active record") def applyDynamic(methodName: String)(args: String*) { val findPattern = """findBy(.*)""".r methodName match { case findPattern(argument) => findDispatcher(s"$argument", args(0)) case _ => s"" } } private def findDispatcher(field: String, arg: String): ResultSet = { val normalizeField = field.toLowerCase() val value = arg val queryString = s"SELECT * FROM $tableName WHERE $normalizeField = '$value'" logger.info(s"dynamic query: $queryString") db.query(queryString) } private def inferenceTableNameByClassName(): String = { this.getClass().getName().split('.').last.toLowerCase() } } |
1 2 3 4 |
//ActiveRecordを継承する、usersテーブルを表すためのクラス package activerecord class Users(_db: DatabaseWrapper) extends ActiveRecord(_db) { } |
Let’s connect!
1 2 3 4 5 6 7 8 9 10 11 |
package activerecord object Main { def main(args: Array[String]): Unit = { val db = new DatabaseWrapper("ktmt_platform_development", "root", "") val user = new Users(db) println(user.findByName("adtech")) println(user.findByEmail("adtech@gmail.com")) } } |
やった!ちゃんと動きました!!!!!
3.説明
上記ではScalaのDynamic traitを利用し、Active Recordを実装してみました。
これにより
- テーブル名をクラス名で自動的にマッピングされ、一つのテーブルイコール一つのクラスを簡潔に表せる。
- フィールド名を関数名から推測され、呼び出す側が楽になる
という二つのメリットが得られます。
もちろんDynamicのやり方をやり過ぎには注意です。
- 一番大きいデメリットはコードをgrepできない、または参照場所がわかない。。
- デバッグしづらい
- ちゃんと設計しないとコードが一つのところ(applyDynamic)に集まり過ぎて、雑なコードになりやすいです
うまくDynamicを利用していきたいですね
次回には演算子オーバーライドとDuck Typingについて話をしたいですので、是非を楽しみにしてください!
Copyright about logo
Copyright (C) 2008 合同会社Rubyアソシエーション
Copyright (c) 2002-2015 EPFL
Author