前職ではRuby on Railsでの開発がメインだったが、転職してからはScala/Play Frameworkでの開発が主になり、それに伴い開発の手法, メンタルモデルも大きく変わった。 それに適応していくために特に大事だったと思うことを書く。
型駆動開発で進める
Scalaは記述量を減らすための工夫が至るところでされていると感じる。それでも動的型付け言語と比べると抽象を全て型として定義するため、定義する型の数からして違う。そのため、実装を終えてから出戻りが発生してしまうとものすごい時間が溶ける。
私はScalaでの開発を始めた当初、1つのクラスのテストや実装を一気通貫でやっていたが、作ったクラスを後で他のクラスと協調させる段階になって不都合が明らかになり、しばしば工数を無駄にしてしまった。 しかもそのような状況で焦って考え直したインターフェースはよくないものであることが多く、プロジェクトが一旦終わったあと、余裕のあるタイミングでまるごと作り直すことになった。
そのような事態を避けるために、型宣言だけ先にやってしまい、実装は後回しにするという手法が有効だった。
作為的な例だが"Scalaスケーラブルプログラミング第4版:4,600円"
のような、<商品名>:<定価>
という形式の文字列を渡すと、価格を消費税込みの金額に直した値を数値で返す関数の実装について考えてみる。
以下のような手順を踏めば目的の計算ができそう。
- 与えられた文字列から価格の部分の文字列を抽出する
- 価格の文字列を数値に直す
- 税額を足す
まず、これらをそれぞれ関数として宣言する。
def extractPrice(s: String): Option[String] = ??? def toInt(price: String): Option[Int] = ??? def addTax(price: Int): Int = ???
ここでそれぞれの関数の実装には入らず、先にこれら3つの関数を組み立てる。 こうすることで、自分が考えているロジックが型レベルで矛盾していないか予め確認できる。
def extractPriceIncludingTax(s: String): Option[Int] = for { priceStr <- extractPrice(s) price <- toInt(priceStr) } yield addTax(price)
今回は実装を省くが、この後ようやくそれぞれの関数を実装する。 実際の開発ではこの関数1つ1つがクラスのpublicメソッドになるイメージ。
当然、型の宣言を終えて実装していく内にやはりこのシグニチャではダメだったということも出てくるが、次に書くレイヤードアーキテクチャを意識し始めてからは、フレームワークやライブラリに由来するデータ型をアプリケーションロジックやビジネスロジック中でなるべく登場させないように心がけるようになったため、それも少なくなってきたように思う。
戦術的設計からDDDに入門する
これは、私がある程度複雑なドメインを持つ業界のアプリケーションを開発しているためポジショントークかもしれないが、DDDの戦術的設計について学習し実践していくことは、Scalaでの開発を始めてから行なったことの中でもコスパがよかった。
既にたくさんの人に言われているとおり、戦術的設計はドメイン駆動設計の全てではないし、私が実践しているのは軽量DDDと呼ばれるものだろう。 しかし、以下のようなものが無秩序にいくつかのクラスに押し込められた様を想像してほしい。
はっきり言ってドメイン知識をコードに落とし込むどころではない。 それどころか、そもそもテストが書けず変更ができないコードになる可能性も高い。 戦略的設計、更にはより深いDDDの哲学の実践に進む前に、テスタブルで変更に耐えられるコードにする必要がある。
戦術的設計を実践することで自ずとテスタブルなコードに近付いていくことが多いし、変更があった場合も大体あの辺を修正すればいいのではないかと見当がつきやすくなった。 私にからすれば軽量DDDはアンチパターンというより、最悪の状態からDDDの実践へ至る途中にある過程の1つだと思う。 プロトタイプの開発では軽量DDDの実装コストがメリットを上回ってしまうかもしれないが、長期的に運用され変更も入るソフトウェアなら、DDDを実践しきれていなくてもコードが整備されていた方がよほどいい。
DDDの足がかりとしては、ドメイン駆動設計 モデリング/実装ガイドに大いに助けられた。 あまり時間がなくても1週間ほどでさっと読めるし、それでいて「このクラスはどのレイヤーに置くべきか?」という疑問をいくらか自分で考えるための軸を構築してくれるので、強く勧めたい。
テストコードを減らしていく
これは私がまだ取り組んでいないので、「RubyからScalaに移る上で大事だったこと」と言い切れないのだが、重要ではないかと思っている。
Rubyで開発していた頃はまだRBSがRuby本体にバンドルされるようになってそう時間も経っておらず、私が携わっていたプロジェクトでも導入していなかったので型検査が無かった。
なので、依存関係のツリーの途中にあるクラスが設計の見直しによりインターフェースを変更されるも、依存している側の修正が十分ではなくNoMethodError
を招いてしまう可能性が考えられ、それに対応するためにある程度E2Eに近いテストコードを書いておかないと安心できなかった。
しかし、型検査がある場合はそのようなことは無くなる。これは一例に過ぎないが、型検査を導入したことによって、テストとの向き合い方も変えていき、型のもたらすメリットを更に享受していく必要があるように感じる。
さしあたっては単体テストの考え方/使い方という書籍が参考になるのではないかと思い、近々読んでみようと思っている。