初めて本格的にSymofnyを使った開発をしたので、その話を。
今回の開発は、<Hoge>という分野。それをきいただけで複雑な開発になることは想像できた。しかもお客さんいわく、「その分野でも誰もやってない新しいことをしたい」とのこと。
これはSymfonyを使うべきだろうと。そしてDDDを意識して開発してみようと。
まだ本格的に公開してないので、分野についてはぼかして書きます。
モデルとユビキタス言語
DDDについて学ぶと、最初に出てくるのが「ユビキタス言語」。そう言えば2000年ぐらいに「ユビキタス・コンピューティング」というのが一瞬はやったなぁ。そこから名付けたのかな?
もともと受託なので「お客さんの要望を聞く」のは実践してたと思うけど、開発側からお客さんに能動的に働きかけることは意識していなかった。なので、今回は
「関係者全員が共通理解を持って対話ができるようになる」
のを目標に置いた。
モデルの構築
まずは、<Hoge>の分野を理解すること、そしてお客さんの要望を理解すること。その上で、両方を満たすモデルを作成すること。
話を聞いていくと、お客さんの要望からは、「計算する」ぐらいしか挙動が出てこなかった。あとは「クローズする」とかで、CRUDに近いシステムになりそう。
その代わり、データ構造が複雑で、どうやって<Hoge>の情報をデータベースに落とし込むか。その上で、お客さんの要望を実現する方法を見つけるのが主題の開発になった
ので、あまりDDDな感じにはならなかったと思う。
モデルの洗練
とにかく始めるにあたって、簡素なモデルを作ってから、<Hoge>の説明を調べたり、お客さんの要望を聞くことにした。
そのモデルでは説明できない場合、お客さんに色々と質問したり、モデルを洗練する。
ちなみに、こんな図を書いて、お客さんと一緒ににらみながらの作業。
モデル図をみながら、「今の要望は、ここから、あんなのが生えるわけですね」みたいな会話を続ける。するとモデルの足りない点がみえるだけでなく、お客さんの要望の矛盾点も見えてくる。
という作業を隔週で2ヶ月ほど繰り返してから実装に入った。お客さんも自分が欲しいものがクリアに見えてくるので、面白がってくれた。
お客さんの理解度
お客さんに図を見せて構造を理解してくれる方がいいのは確かだけれど、かならずしも、全てのお客さんが理解する必要はないと思う。
ただ今回は理解してもらう必要があった。
上の図の中の「カテゴリ」とか「要素」がある。これの裏にさらに幾つか構造が隠れてるのだけど、この中身がシステムの肝であり、今までにない試みの部分だったりする。
その中身がお客さんのノウハウを詰めたデータになっていて、お客さんしか作ることができない部分だった。
という理由があったので、今回は「お客さん自身がモデルを理解することが必須」という状況だった。
感想
普通、お客さんはシステムの視点から考えたことはないので、要望に整合性はない。もっと言えば、行き当たりばったり、その場の思いつきの場合もある。後から話が変わったり、矛盾していたり、よく考えてみたら重要でなかったり。
お客さんにも言葉やモデルを理解してもらうことで、とんでもない要望が出てこないようになったのは感じられた。あるいは、要望に対して何故難しいのかを説明すると理解してもらえるのは開発側としても良かった。
できたシステムは、おそらくお客さんが当初考えてなかったことまで実現できたと思う。これも、じっくりと話をした結果かなと、思う。
でもお客さんも大変だったと思う。
実装
開発に使ったのは、Symfony 3.2とDoctrine2。
DBテーブル設計
先のモデル図を元にDBテーブルを設計。
本当は「T字型ER」の手法を使いたいところ。テーブルはスッキリときれいになるし、過去未来の状態まで表せるので素晴らしい設計手法だと思う。でも、まだ自分は習熟してない。
なので結局は「意識」した程度。そもそも挙動が少ない=リソースばかりでトランザクション系のデータが少ないシステムだったというのが理由の一つ。そして、いまだにSQLの書き方やパフォーマンスなどで不安が拭えない。初めてのDoctrine2での開発なので、ここは安全に今まで通りの設計に近かった。
エンティティとVO
今回は、挙動が少ないのが特徴。ということでエンティティから導出するValue Object (VO)側に挙動のほぼ全てを閉じ込めることができた。
VOってなんだろう?
ところで、とあるVOは、エンティティからリレーションで別エンティティを取得して、ストラタジーパターン使って10個以上のクラスを使い分けるのですが…
「エンティティ内でnewしていいのがVO」という理解でいるのだけど、サービスのようにも見えるし。でもエンティティのプロパティから一意に挙動が決まるので、VOな気もするし…
これをVOと呼んでいいの?
そもそもVOって定義は何だろう?
エンティティは登録と削除のみ
メインのデータは更新しないシステムとした。
ただし理由としては、仕様が安定していない段階で、極めて複雑な登録と更新のフォームを両方維持できないと思ったから。
実際にやってみた感想は、登録と削除だけでシステムが組めるなら、これは開発が楽になるなと。
一方、フォームで登録できるのがデータの一部だったので、そのまま削除するとリレーションが壊れてしまう。実は、そこの対応が面倒だった。
同じく、リレーションで使われる側も再登録でIDが変わると使いものにならないので、更新処理にした。どんな状況でも使える銀の玉は少ない。
サービス
DDDといえばドメインサービス、の出番は無し。
その代わりに表示用のサービスディレクトリを作って、Symfony/Formオブジェクト、DTO、そして表示用Collection、などを管理した。
表示用Collectionこそがドメインサービスなのかもしれないけど、実を言うと、ドメインとアプリの壁が理解できていない。
それよりエンティティはエンティティのみに依存する、表示用サービスはエンティティと表示用サービスのみに依存することを意識した。要は、レベルの上下だけ考えてた。
テスト
余りテストはかけなかった。
開発で確認したいのは、
- モデルが正しく分野を表しているか?
- モデルが正しくお客さんの要望を表しているか?
- お客さんが作った設定に不備がないか?
- 計算が正しいか?
の順に確認したい内容で、テストで確認できるのは4.だけ。
結局、モデルや設定が正しいかどうかを確認するには、実際に手を使って動かして、目で見て利用者の視点から判断するしかない。「うーん、この表示は変だけど、設定の問題かなぁ」「あー、こう見せると利用者は混乱するので、モデル拡張しないとだめだなぁ」みたいなことを開発チームとお客さんで話しながら動作確認してた。
計算用ValueObjectにはテストを書いた。例えば計算そのものは正しかったけど要望が微妙に違ってる場合で、リファクタリングするときはテストがあると早いし楽だし安心だし、やっぱりテストは便利。
名前付けは大事
ちょっと面白いと思った話。
Hoge種別?
モデルを作っていくと、ある大事な構造体が出てきた。「Hoge種別」だ。構造はできたが、これが何を表すのか?これが分からない。なので名前をつけられない。
調査(ググって)してみても、似たような分類や言及はあるけれど、カチッとした名前がない。
お客さんに聞いてみたら、それは業界でも存在しない概念だとのこと。
いや「それこそがこのシステムの新規性だ」と。
おー、何かすごく気分がいい!
とはいえ仕方ないので、「Hoge種別」と名付けてみた。
やっぱり後悔した。
なんとか種別とか、注意
色々な属性を表す「種別」という言葉は気をつけないと、と思っていて。
例えばUserType
。お客さんの種別です、と言われても、よく聞けば会員をあらわすという。それならMemberType
の方が良くないか?とか、よく考えないと、表したいものと名前が違ったりする。
この「Hoge種別」も同じだった。
上の図のHogeマスタにも<Hoge>種別があるけど、構造体とは別定義なのだ。しかも「Hoge種別」の実際の種別は別テーブルで定義していて、Hoge種別コード、な項目が出てきたりとカオスに。
あー、やっちまった。
名前も中身もない構造
さて、この「Hoge種別」、設計が進む毎に中身が減っていった。
結局、日付けが2個ぐらいの中身スカスカのテーブル、一般的な名前もない、にもかかわらず、システムの肝という不思議なデータ構造が出来上がった。
さらに、実装がおわり、さてアプリを動かし始めた段階で、色々と見落とした内容が大量に出てきた。が、幸い、ほぼすべて「Hoge種別」テーブルからの派生モデル(要はDBテーブル)として表すことができた。
こう考えると、ドメインの理解としては正しかったのかなと思う。ただ名前だけが、心残り。
Symfonyの話
少しはSymfonyの話でも。
Symfony/Form
フォーム操作と言えばSymfony/Form
を使うのが一般的らしい。ということで、使ってみることにした。
データ上は3層構造のものを一度の操作で登録するため、フォーム上では2層構造に見せかけることにした。3層構造のフォームなど、人間に扱えないだろうし。
このため、エンティティからフォーム用DTOを作って、フォームに展開。フォームの入力は、逆にDTOからエンティティを再構成、という処理にしてみた。
総論としては、Symfony/Formはすごくて、多機能かつ慣れればRADにも使えるぐらい簡単、になりそうかなと思っている。
では他人に進められるかというと、よく分からん。最初は何が何やら分からないし、ちょっと複雑になるとさらに分からない。その先に、どうやら素晴らしい世界がありそうな、そんなコンポーネント。
海外の情報を探したが、あまりBest Practiceのようなのが見当たらない。思うに、有料セミナーとかで学ぶことになっているのかもしれない。
DTOと入力チェック
フォーム専用DTOは、きちんと作る必要はないよね。ということで、全部publicプロパティにしてGetter/Setter無しで作った。
class HogeTypeDto {
public $hogeType;
}
Symfony/Formでは、入力チェックはDTO側にまとめられる。
エンティティと分離できて便利なのだけど、DBとの整合性チェックだと、DTOにEntityManagerをDIするのだけど、ちょっと面倒。
さらにチェックが複雑になるに連れツラミがでかくなった。機能が豊富すぎて、調べる度に違うやり方で書いたのが原因で、見通しが悪くなったのかなと思っている。
Setterは嫌い
フォーム用DTOを作ったので、エンティティにはSetterを作る必要がなくなったのは良かった。とは言え、セットする必要はある。そこで…
trait FillProperty {
public function fill(array $data) {
foreach($data as $key => $value) {
if (method_exists($this, $method = "set{$key}")) {
$this->$method($value);
} elseif (isset($this->$key)) {
$this->$key = $value;
}
}
}
}
みたいの作って、エンティティとDTOに埋め込んで使った。VOと変換が必要な場合は、privateでセッターを書いて、fill
でなんとする感じ。どこかで見たような関数名なのはLaravelも好きだからです。
Setterが嫌いなのは、エンティティのコードが長くなって見通しが悪くなるからと、同じセッターなら格好いい分かりやすい名前を考えるべきと思うから。
例えば、フォームの入力を登録するなら、それこそfill
という挙動でありセッターさんの役割ではないのかなと。ステータスをセットするなら、open
とかclose
という挙動で表すべきでしょう。内部でclosed_at
の設定とかも確実にできるし。エンティティ内のデータ整合性を担保するのは、なるべくエンティティに持たせたいと思うので、やはりSetterではないのかななと。
Getterは?
Getterもコードが長くなりますが、普通に情報表示ビューで使うので全部書きました。書くとPhpStormだとコード補完が効きます。twig内では、効いてた記憶があるけれど、今確認したら駄目だ。
Form用DTOは必要?
今回は、必要だったので使いましたが…
Symfony/Formを利用するなら、エンティティにはGetterとSetterの両方が必要になります。DTOはpublicプロパティにしてしまえば、それほど面倒ではないので、結局は全てのフォームでDTOを使ったと思う…
あとは、先のFillProperty
トレートを使うと、意外と簡単にエンティティ↔DTOの変換ができたというのもあります。ま、これが原因で例外が発生したこともあったのだけど。
ControllerとRouteアノテーション
Routeアノテーションいいです。
Controllerが太ってきてメソッドを別クラスに移動するときも、アノテーションごとコピペすればOKなので、気持ちよくリファクタリングできる。
ルートが一箇所にまとまってないので、URLからコントローラーを特定するのが面倒かと思ってたけど…
大量かつフラットなroute定義からコントローラーを探すより、Controllerディレクトリを探すほうが楽な気がする。もちろんControllerクラスを上手に分ける必要があるのだけど、そこはリファクタリングし易いのでクラス分けしやすいのがポイント高いです。
最後に
ひとまず開発中に悩んだ点とSymfonyの良かった話を書きました。
契約
このような開発は受託では無理です。
今回は成功報酬のような形で関わりました。
書いていて
最初は断定口調で、だんだんとですます体になったのが面白かった。