はじめまして。マイクロアドでアプリケーションエンジニアをしている新卒1年目の石田です。 主に、MicroaAd BLADEという広告配信プラットフォームの開発をしています。 内定者アルバイト時代からマイクロアドでBLADEの開発に携わっていました。BLADEのソースコードはJava/Scalaを中心になっており、大半がJavaでした。現在は新規機能開発や、リファクタではScalaで実装を行なっています。
今回は、自身が担当した 「JavaコードのScala化を行う実案件」 に絡めて、「可変オブジェクトの問題」や「ScalaでのDDD」に関する話を書いていきたいと思います。
JavaコードのScala化
自分のScala力が多少まともになった要因となったのが、このScala化の案件です。 私が学生の時には、とあるインタプリタ方式の手続き型プログラミング言語を書いていました。加えて浅学だったので、静的型付けのコンパイラ方式の言語のメリットなんてよく分かっていませんでした。
「型?面倒臭いだけじゃん。コンパイラうるさいよ。黙って。」
いいえ、コンパイラは常に私達に道を示していました。コンパイラと対話することこそが大切だったのです。
ではJavaからScalaにリファクタリングするにあたり、個人的に特に有用だったことについて軽く触れていきます。
nullからOptionへ
Javaのように正規のロジックとしてnull
を扱うことによるデメリットをいくつか以下に挙げます。
- nullが入る変数がどれかという事がコードだけでは判断することができない
- nullが入る変数がどれか判断できないので、nullチェックなどのボイラープレートコードが大量に生まれる
JavaからScalaに移行するときに、必ず変更するポイントといえば、null
をOption
に書き換えることでした。Scalaとしては初歩的な話になるのですが、Javaプログラマが簡単に取り入れやすいものだと思います。最初は「null
をOption
として扱って、ループにfor
とwhile
を使わないのがScala」だと思っていたぐらいです(全然違います)
「値があるかないかわからない」という文脈を持った変数をOption
型として扱うことで、nullを直接扱う場合のようなデメリットが無くなります。
Option
は値がある時にはその実態はSome
、値がない時にはNone
として表現されています。Option
のクライアントからは、値があるかないかということに関心を持つ必要はありません。
値があれば、関数が実行され、値がなければ関数が実行されないといった挙動になります。
例として、「有るか無いか分からない整数値」に対して、「有るなら1を足して、2を掛けたい」というケースを図で見てみましょう。
値が有る時には関数が実行され、無い時には実行されていないのがわかります。 文脈を持った変数を、流暢なコレクションAPIで処理していくことで、「有るか無いか」という文脈を保ったまま演算を行なって行くことができるわけですね。
エラーを値で扱う
例外を補足する必要がある場合には、Javaのようにtry ~ catch
式で例外処理せずに、scala.util.Try
を使う場合があります。Try
を使って例外処理を行うことで、Optionの場合と同様に「成功か失敗か」という文脈を保ったまま処理を進めていくことができます。「ビジネスロジックに例外処理のコードが含まれなくなり、コードの見通しがよくなる」などのメリットが生まれるわけです。
単にJavaからScalaに置き換えるだけであれば、Intellijが勝手に置き換えしてくれるので簡単です。しかし、それではScala化を行う理由はほとんどありません。 JavaのコードをScalaで最適な形に書き換えるには、ほぼ全てのロジックを書き換える必要がありました。そういったこともあり、最終的には数百ファイルにも及ぶJavaコードを一から作り直す形でリファクタリングを完遂しました。
巨大システムにおける可変オブジェクトの問題
BLADEはそれ単体で巨大な広告配信システムです。BLADEの開発や、今回説明したScala化のようなリファクタリングをいくつかこなすことで、システム上の大きな問題も見えてきました。
Javaでは可変オブジェクトを扱うことが一般的で、状態を持つオブジェクトはパブリックなgetter/setter
メソッドを持ちます。また、そのスコープ内からであればどこでも状態を変化させることができます。
可変オブジェクトの扱いは難しく、状態を容易に変更できる状況はバグの温床になってしまいます。
現在のBLADEでは、元々完全にJavaで書かれていたということもあり、可変オブジェクトが多くの箇所に存在しています。その結果、可変オブジェクトの内部状態の変化を追うことが難しい状態になっている箇所もあります。 それに加え、初期の設計思想とは大きく異なった度重なる機能追加や改修によって、全体として密結合、低い凝集度になっていました。
こういった問題によって、アップデート時に影響範囲の調査をするときの調査範囲も広範囲に広がり、高速な開発とデプロイに悪影響も生まれていました。 単体テストをはじめとする様々なテストケースを作っても、配信サービス全体の影響を予測することが難しい状況です。自分自身、リリース後に障害を起こしてしまうこともありました。
また、ドキュメントもあまり整備されておらず、そのシステム全体を完全に把握する人は限られており、新しい人材がチームに参加するハードルが高いプロダクトになりつつありました。
Scalaでは不変オブジェクトが基本です。不変オブジェクトを参照している側は、当然そのオブジェクトが変更されることなど考えずにロジックを実装できます。また、Scalaの持つ関数型のパラダイムでは、副作用の無い関数1を用いたコーディングが推奨されます。不変オブジェクトを基本とした、関数による振る舞いによって、コードを推論する範囲を小さくできます。つまり、関数で書かれた振る舞いは、その関数だけに焦点を当てれば良いので、ロジックの理解が容易になリます。逆に、可変オブジェクトを使った、副作用を持つ関数の振る舞いによって、コードを推論する範囲が大きくなってしまいます。
そういった可変オブジェクトの問題がありながらも、元々全てJavaで書かれていたBLADEを、全てScalaにリファクタリングしていく作業というのは容易なものではありません。 今正常に動いているサービスを部分的にScala化する為には、以下のような複数の段階を踏む必要があります。
- リファクタリング範囲の決定
- 事前影響調査
- 現状の実装の把握
- リファクタリングの実装
- テスト
- 検証
- 受け入れテスト
- リリース
- 事後影響調査
- etc...
広告配信システムは当然止めることのできないシステムであり、綿密な検証が必要になるのですが、これを何回も繰り返していくのは骨が折れる作業です。
現状のシステムの巨大さと複雑さを鑑みて、チームでは広告配信システムを新規開発する動きに乗り出しました。 「コードが仕様であり、仕様がコード」となるドメイン駆動設計[以下DDD]の手法を実践することで、設計思想をダイレクトに反映し、より高速な開発サイクルを回せることが期待できます。ScalaがDDDに親和性があることも後押しとなりました。
ScalaのDDDとの親和性
DDDとは、集約、エンティティ、値オブジェクトをはじめとする、状態と振る舞いをカプセル化したドメインモデルに、ビジネスのユースケースを反映する設計手法です。
DDDにおけるエンティティは、「意味的に」状態が可変なオブジェクトであり、その状態は時間とともに変化していきます。Javaでのエンティティは、setter
メソッドによって状態が変更可能な可変オブジェクトとしてモデリングされます。可変オブジェクトは前述の通り、避けるべき選択です。
Scalaでのエンティティは、不変オブジェクトによってモデリングされます。これによって参照しているエンティティが他の何かによって勝手に変更されるという事態を防ぐことができます。
正しくモデリングされた境界づけられたコンテキストによって、そのコンテキスト単体で完結させます。さらにそのコンテキスト内部でも、不変オブジェクトによる純粋関数による処理を心がけることで、改修単位が最小限になり、高凝集性、疎結合が実現できます。
また、DDDではビジネスのユースケースに従った、ユビキタス言語を反映したモデル名やメソッド名にする必要があります。 Scalaは単項演算のメソッドを中置き記法で記述できるので、まるで自然言語のように記述することができ、DDDの意図を反映しやすいのです。
case class
による不変オブジェクトの定義も魅力的です。case class
によってドメインモデルをモデリングできるのですが、その威力はエンティティのモデリングにおいて特に顕著です。
Javaでエンティティを実装しようとすると、プロパティの数だけのsetter/getter
メソッド、equals
メソッドなどのボイラープレートコードを実装しなければなりません。
case class
はそのようなメソッドを自動的に定義してくれます。
DDDそのものの利点、ScalaにおけるDDDの利点について詳しく言及するのは他の解説記事や書籍に任せるとして、 Scalaには優れたDDDを実践する為の機能がそなわっていることについては感じていただけたと思います。
まとめ
今回は、JavaコードのScala化を皮切りに、既存システムでの可変オブジェクトにまつわる課題、ScalaのDDDとの親和性について書かせていただきました。実際にこのような課題を解決した具体的なプロダクトを例にしたかったのですが、会社としてDDDは黎明期なので今後記事にしていきたいと思っています。
新卒に絡めた話をしようと思ったのですが、あまりなかったので結果的にこのような記事になってしまいましたが、今回は以上になります。ありがとうございました。
-
本記事では、特に明記しない限り「関数」という言葉を「副作用のない関数」という意味で使用します。↩