asken テックブログ

askenエンジニアが日々どんなことに取り組み、どんな「学び」を得ているか、よもやま話も織り交ぜつつ綴っていきます。 皆さまにも一緒に学びを楽しんでいただけたら幸いです!

増田亨さんによる「設計の考え方とやり方」勉強会 書き起こし2 「設計スタイルの選択とクラス設計のスタイル」

増田亨さんによる「設計の考え方とやり方」勉強会 書き起こし2ページ目です。最初からお読み頂く場合は、こちらから御覧ください。

資料

書き起こしリンク

目次


設計スタイルの選択

ソフトウェアの変更を楽で安全にするために、私自身がどういう方向性の設計をやっているか、少し背景的な説明を含めて具体的にどういう方法を目指しているのかをお話ししていきます。

クラス設計のスタイルという視点、テーブル設計のスタイルという視点、あとは開発のやり方という視点で、私の設計の考え方とやり方を説明します。

アプリケーション開発の今昔

私はそれなりの年齢を重ねてきているので、大昔からこの業界でいろんなソフトウェアの現場にいろんな立場で関わってきています。かつての潮流は業務系アプリケーションはトランザクションスクリプト方式一択という感じでした。「ソフトウェア開発とはトランザクションスクリプトだよ」みたいな世界があって、特にリレーショナルデータベースが出てきてからは、データベースに対するトランザクションスクリプト方式でアプリケーションを開発するのが主流であり、基本的なやり方でした。

そのデータベースの使い方は、これは対比の問題ではあるんですけど、どちらかというと上書き更新型です。「現在の状態がこうなっている」ということを常に更新し続けるスタイルです。同じテーブル、同じレコードに対してどんどん更新をかけていって、アップ・トゥ・デートなカラムがそのレコードでセレクトすれば見られるようにする、というのが上書き更新型のデータベースというスタイルです。

あとは目標を固定して、システムの最終形を確定して、それを関係者で合意して、それを具体的に作るための工程についてはトップダウン的に分解しながら、いわゆるウォーターフォール的なやり方でやっていくやり方です。

一方、今後の潮流というか、既にもう始まっている変化なんですけれども、変更を楽に安全にすることを重視するなら、ドメインモデル方式は相当有力な選択肢だと思っています。

あとは追記型のデータベースですね。イミュータブルデータモデルに基づくテーブル設計みたいなこと。あとでもうちょっと解像度を上げた話もしますけれども、今後はこういうことを意識しておくと、変更は楽で安全になるんだろうなと思います。

あとは目標可変の組み立て思考というのは、それがいいか悪いかという価値観の話ではなくて、多分そうせざるを得ない状況だと思っています。

3年後、5年後にこういうシステムがあればいい、ということに対して人を集めて3年後ぐらいに完成させて、そのあと5年10年と同じようなものをあまり変えずに使い続ける、なんていうやり方は、今の事業環境、社会環境ではちょっとあり得ないです。目標は常に変わります。走りながら目標を変えながらやっていくにはどういうソフトウェアの作り方をするのか?そういう作り方をせざるを得ないんじゃないかと思います。目標可変の組み立て思考という考え方が今後の潮流になるんだろうなと感じています。

クラス設計のスタイル

クラス設計の分かれ道

クラス設計のスタイルは、先ほど説明したように、トランザクションスクリプト方式がかつての潮流でしたよね。データクラスと機能クラスを分けて、中核の関心ごとは入出力処理で、画面とかデータベースとかWeb APIの外部仕様を決めて、それをとにかく動くように作るというような作り方。

あとは内部を見ると、プリミティブな型でプログラミングするということ。このプリミティブな型のプログラミングというのは意味がわからないかもしれませんが、ドメインモデル方式で採用している独自の型ということに対する対比ですね。

防衛的プログラミングというのは、いわゆる入力データとか引数で渡されるデータというものは信用してはいけません、ということです。受け取った側が徹底的にチェックして、不正なデータは受け付けないということ、徹底的に防御的なコードを書きなさい、という考え方です。

こういうやり方がトランザクションスクリプト方式。トランザクションスクリプトだからこうなるのか?という話はちょっとあるにせよ、いわゆる従来のやり方ではこういうものを全部セットでやってきています。

ドメインモデル方式の場合は、データクラスと機能クラスを分けるのではなくて、ロジックとデータを一つのクラスにカプセル化するということを重視します。それが大前提です。 データと、そのデータを操作するロジックは一つのクラスとして、独立したモジュールとしてカプセル化されるんですよね。

なんでカプセル化をするかというと、中核の関心ごとは計算判断ロジックだということです。 計算判断ロジックはソフトウェアを作る側からの言い方で、業務でソフトウェアを使う側の人、ビジネスをやっている人達から見たら、それはビジネスルールなんですよね。業務を進める上での決め事。そういうものがソフトウェアを複雑にする根源であるので、そういう中核の関心ごとである計算判断ロジックを、データとロジックをカプセル化するというアプローチがドメインモデル方式です。

アプリケーションで扱う値、たとえば金額とか数量とか期限とか、あるいは顧客の区分とか、出荷区分とか、製品のカテゴリとか、そういうものを独自の型としてどんどん定義していく。アプリケーションで使う値を独自の型として定義したクラスは、必要なロジックとデータを持っているカプセル化されたクラスとして作っていきます。

あとは契約プログラミングですね。防御的プログラミングと真逆のアプローチです。 事前条件は引数の型です。引数の型でオブジェクトを渡すことが、メソッドを実行する事前の約束です。引数を渡す側に正しい値を渡すことを強制する。 「じゃあ何を約束してくれるんですか?」という事後条件は、メソッドを返す型として「こういう型のオブジェクトを返しますからね」「NULLは返さないし基本的には例外も返しませんよ」という約束をする。

「あなたが、使う側が引数の型として約束を守ってくれる限りは、NULLを渡してこない限りは、こういう型のオブジェクトを必ず完全な状態でお渡ししますよ」と、そういう契約に基づいて、依頼する側と依頼された結果を返す側が、約束事を守るという大前提でコードを書く。 防御的なプログラミングだと、コードの中に引数がこんなのくるかもしれない、あんなのくるかもしれない、何がくるかわからない、あるいは返ってきた値がゴミが返ってきてるかもしれないみたいなチェックをする。そういう防御的プログミング的な発想に基づく冗長な検査コードを一切なくす、というのがこの契約プログラミングの考え方です。

これらがドメインモデル方式の本質的な属性かというと、またちょっと違う部分もあるんですけど、ドメインモデル方式でやろうとしたら、自然にこういう選択肢になってくると思います。

あるいはこういう方向を選択すれば、ドメインモデル方式のソフトウェア開発がやりやすくなりますよね。そういうふうに考えてやっています。

なぜドメインモデル方式か

なぜドメインモデル方式にするのかを確認しておきます。これも私自身の経験談なんですが、トランザクションスクリプト方式で作ったコードに凄く苦労してきました。何かいい方法がないかと模索していた時に、エヴァンスの『ドメイン駆動設計』に出会いました。

マーチン・ファウラーが書いた本(PoEAA)でドメインモデルという言葉は知っていたんですけれども、現実的にどういうふうにするのかがわかっていませんでした。それがエヴァンスの本を読むようになって、それを参考に自分でいろいろトライアルするようになってから、本当にいい効果が出てくるようになりました。ソフトウェアの変更を楽に安全にすることで発生するメリットがけっこう凄いなということを実感して、それ以降ドメインモデル方式を採用するようになりました。

ドメインモデル方式とトランザクションスクリプト方式の違いは、計算判断ロジックの複雑さをどう扱うかということに大きな違いが現れます。トランザクションスクリプト方式で実際にやっていた時のコードだと、あちこちのデータ入出力処理に似たような計算判断ロジックが断片化して、しかも下手するとコピペでかなりの計算式とかswitch文がボコっと入ってくる。

そのコピペした計算式に変更があった時とか、switchでの分岐に変更があった時に、どこにそういうswitchの分岐があるかわからないのが怖くてしょうがない。なので探しまくるんですが、探してもきちんと直せたのか、探し切れていたかがわからないんで、テストを徹底的にやる。それじゃスピードも出るわけがありません。

あとはビジネスルールが暗号化されてしまっていて、コードを書いた時はわかっていたつもりでも、仕様変更が必要になった時には意味がわからなくなってしまっているというケースもあります。

たとえば「区分が9だったら処理数は-1にしろ」というようなコードの変更が必要になったとしても、やりようがないんですね。どんな意味合いで処理数がマイナスになっていて、何が起きるのかなんていうことがわからなくなっている。

これが一箇所だけだったらまだしも、何箇所にもあるわけですね。そういうことで本当に苦しんでいた。トランザクションスクリプトってやっぱりこうなっちゃんだよな、と。

どこにどんな計算判断ロジックが書いてあるかが探しにくい上に、見つけても「こういうふうに直しても大丈夫なのかな?」とか、非常に変更がやっかいで危険です。直しても自信が持てないから相当テストをしたつもりでもまだ安心できない。案の定リリースしてかなり時間がたってから、「この入力ケースで、こんなとんでもないところのコードまで実行されちゃうんだ」というようなバグが出てくるというようなこともあります。ある程度昔風のコードと長年付き合ってきた人には「あるある」なんだと思うんですけどね。

ドメインモデル方式のクラス設計にすると、こういう問題を本当に改善できるということを私は経験してきました。それが今の私のクラス設計の考え方、あるいは現場で今実際にやっているやり方につながっています。

ドメインモデル方式

ドメインモデル方式は計算判断ロジック中心です。入出力の関心事を一旦は脇に置いておく。アプリケーションとしては入出力機能は必要ですが、ドメインモデル方式の核心は計算判断ロジックだけを、入出力から切り離した論理的な計算判断ロジックだけを、ビジネスルールを独立させて、それをきちんとプログラムとして完成させましょう、というものです。

そのためには、関連する業務ロジックと業務データを別のクラスに分けるより、一つのクラスにカプセル化したほうが楽に記述できますよね。一つのクラスにカプセル化することで、同じデータを使った計算判断ロジックがあちこちに断片化したり、重複することも防げるという効果もあります。

あとはクラス名、型名ですね。メソッド名で業務の約束事を表現して、ビジネスで扱うデータの種類ごとにクラスを用意して、データの種類ごとに、「金額を合計したい」「割引率で金額計算したい」というようなビジネスに必要な計算判断をどんどん洗い出していって、そこに名前を付ける。クラス名とかメソッド名に、どんな業務をしたいか、どういうルールがあるかということがわかる名前を付けていく。

それからさっきも言ったような契約プログラムですね。ビジネスで扱う型を使うことで、事前条件(引数の型)と事後条件(返す型)を明示する。 たとえば、引数をintにして渡すと、0を入れてもいいわけです。もしメソッドの中で除算をしているメソッドだと、0除算エラーが出ちゃうんですね。これをQuantityという型にして、Quantityの有効な値は1から100という制限を型、クラスとして定義する。Quantity型のオブジェクトが生成できたということは、数量は1から100であるということが保証されています。これを受け取るメソッドは0除算を心配しなくてよい。

事後条件としてメソッドを返す型でも同じです。受け取る側が、もしかしたらゼロかもしれないから、それはゼロ除算例外をキャッチして処理してね、みたいなことを相手に強要しない。相手が防御的にそういうことまでチェックするコードを書く必要がなくなります。

こういう契約プログラミングを徹底することによって、本当にやりたい計算判断だけを正確にやるだけの記述に単純化できる。コードが非常にわかりやすく書けるようになります。

考え方としては、ドメインモデル方式というのはこういうことなんだろうなと思います。

クラス設計:複雑さを分離する

そうやってビジネスルールクラスを設計してドメイン層のクラスを作ってやると、事業活動の決め事がビジネスルールとして、値の種類とか区分ごとにルールが変わる複雑さや、if文だとかswitch文でやる条件分岐の複雑さなんかを、ドメイン層のビジネスルールを記述したクラスに閉じ込めて宣言的に記述することによって、ほかのプログラムが非常に単純になるんですね。

計算判断の複雑なところを全部ドメイン層が、しかも契約プログラミングで結果を保証してくれるクラスとして揃っていると、ビジネスアクションクラス、いわゆるアプリケーション層でユースケースとか入出力機能を実現するクラスでは、ドメインオブジェクトを生成し何か結果を得るような実行役だけになって、実行結果を記録するとか、通知するとか、そういうことに徹することができます。ごちゃごちゃしたswitch文とかは基本的にはドメイン層のクラスのほうに閉じ込められているので、ビジネスアクションクラス、ユースケースとか機能クラスはものすごく単純に書けるようになります。

ドメインモデル方式でアプリケーション全体をどう組み立てるか

ドメインモデル方式でアプリケーション全体を組み立てる場合、実際に私が作る順番としては、まずビジネスルール、事業活動の決め事、計算判断ロジックを特定しながら宣言的な記述をしていく、というようになっています。ここがある程度複雑になってくるんですね。

これがある程度部品として出来上がってくると、続いて「受注登録したいんだよね」というようなビジネスアクションクラスを作ります。受注していいのかどうかの判断だとか、受注時の金額計算だとかはビジネスルールクラスがやってくれますから、単純な計算判断を実行して、通知して記録すればOKというような単純なコードになります。

もちろんこれも論理的なクラスでしかないんで、アプリケーションとしてはこのままでは使えません。アプリケーションとして完成させるには、このビジネスルールクラスを土台というか基本コンポーネントにして、それを使うビジネスアクション、論理的なビジネスアクションクラスを作る。そのまわりにコントローラーとか、非同期メッセージのリスナーとか、そういうアクションの起動役のクラスをくっつけてやると、このビジネスアクションクラスが呼び出せるようになります。

また通信クライアントで、Restでほかのところに通知してあげたりとか、永続化クライアントとしてデータベースに書き込むというようなクラスを用意してあげることで、ビジネス的に記録したいんだというタイミングと内容を実際にデータベースへの書き込み、永続化ということで実装するみたいなこと。こういうものを最後にくっつけてあげることでアプリケーションが一つ出来上がります。

クラスの設計を改善する(リファクタリング)

クラスの設計を改善するリファクタリングはドメインモデル方式の設計ではとても重要です。最初からきれいなクラスができるわけではないので、そういう意味ではとっととクラスを作りながら、どんどんリファクタリング、設計改善をやっていくんだという大前提で作っていくやり方です。

あとで作り方の話で出てきますけど、そういう作り方を実際にやっています。

リファクタリングと言ってもだいたいパターンが決まっています。分割不足なんですね。中盤ぐらいになってくるとどんどん特定のクラスが膨らんできたりします。大きなクラスというのは必ず凝集度が低い。複数の関心事がごちゃまぜになっています。そういう分割不足を見つけながらメソッドを抽出したり、クラスを抽出したり、クラス数が増えてくるのでパッケージを追加したり、サブパッケージを追加したり、あるいは名前付けをそれぞれ工夫しながら、分割不足をまず改善していきます。

分割不足の改善は、ある程度中身を理解している人同士ではわかるんですけど、今度はそれを中長期的に変更が楽で安全なものにしていくためには、やはり分割意図というものをわかりやすく伝えないできません。

だからリファクタリングの一つの大きな活動として、分割不足を単に改善するだけではなくて、分割意図を説明するためにパッケージ名を工夫する必要があります。いい感じで分割できた、整理し直したと思っても、整理し直したことによる新しい名前の発見とか、あるいは元々の名前の不整合、解離とか、本来の意味と実際の今の分割後のクラスが違ってしまったとか、そういったところを丁寧に丁寧に見ていきます。タイポみたいなものですね。名前がちょっと違っているというのを放置しないで、タイポがあったので直すというような感覚で、名前が変なので直す、ということを地道に繰り返していきます。

そういうふうにやっていって落ち着いてきても、まだまだ凝集度の改善ポイントが出てきます。そこでクラスを異なるパッケージに移動したりとか、メソッドないしロジックを移動したりとか、逆にインスタンス変数を持っているクラスを変えたりとか。

あとはある程度以上の規模になってきたら、個々のクラスとかメソッドをインスタンス変数という観点よりも、パッケージ、サブパッケージをどうやって移動するんだとか、パッケージ構造を組み換えたほうが全体がスッキリ整理できるんじゃないかとか、変な依存関係が消えるんじゃないのか?みたいなことをやっています。

まとまった時間でこういうリファクタリングをやるというよりは、日常の設計や機能追加、仕様に基づく修正の中でこういうことを常に繰り返している感じですね。


パート2は以上です。パート3の「テーブル設計のスタイル」はこちら。

asken では、サーバーサイドエンジニアを大募集しています!

www.wantedly.com

www.asken.inc