はじめに
ソフトウェア設計において、見通しの良さは開発効率や保守性に大きく影響します。
関数型プログラミングとDFDを学んだ結果、データの流れを意識した設計の重要性を実感し、自分なりに実践してみることにしました。
本記事では、この考えを実践した経験や、設計のポイントについてまとめます。
皆さんの設計のヒントになれば幸いです。
注意事項
本記事では、VB.NETのWindows Formsを前提として、データの流れを意識した設計・実装の実践例をまとめています。
設計思想や実践ノウハウは他の言語やフレームワークにも応用できる内容ですが、本記事で想定しているのは中規模開発です。
大規模開発の場合は、工夫が必要になるかと思われます。
※ ここでは、中規模開発を実行ファイル 1つにつき 2,500〜5,000行程度のプログラムと定義しています。
設計手法
仕様書や設計書の書き方などは本記事では省略します。
DFD
DFDを活用することで、全体のデータの流れを可視化し、把握することができます。
TIPS : DFD作成の流れ
-
要件の不明点を解消
要件の曖昧な部分を明確にした上で、DFDの作成を始めます。 -
矛盾点の発見
データの流れの中で矛盾がある箇所を発見します。 -
矛盾点の調査
見つかった矛盾点について詳しく調査します。 -
矛盾点の解消と流れの把握
矛盾点を解消し、データの流れを明確に把握できたら、次のステップに進みます。
クラス図
クラス図を描くことで、より詳細なデータの流れや構造を把握できます。
私は「どこでデータモデルクラスが取得・加工・出力されるか」を明確にできれば十分だと考えます。
※ 本記事では、データの構造を定義したクラスのことを、「データモデルクラス」と呼んでいます。
TIPS : DBや設定ファイルから取得したデータの扱い
DBや設定ファイルから取得するデータは、基本的にデータモデルクラスに落とし込みます。
これにより、データの流れをコード上で明確に捉えやすくなります。
TIPS : 処理の分け方
私は、クラスを細かく分けすぎず、できるだけシンプルに分けることを心がけています。
「データの取得」「データの加工」「データの出力」の3つに分けることで、設計の見通しが良くなり、保守や拡張がしやすくなります。
実装手法
不変性の活用
変数は可能な限り書き換えない実装を心がけます。
VB.NETの場合は言語仕様上、不変性が完全に保証されないため、可能な限りフィールドにはReadOnly
やConst
を活用します。
コンストラクタで値をセットできない場合には、フィールドに値を設定した後、ReadOnly
プロパティからフィールの値を取得するようにしています。
メソッド内では、定数を可能な限り使い、変数は不変性を担保する仕組みがないため、基本は変数に再代入しないように実装します。
TIPS : LINQやイテレータの活用による不変性の担保
LINQやイテレータを使うことで、元のコレクションに影響を与えずに処理が可能です。
LINQは基本的に元のコレクションを変更せず、新しいシーケンスを返します。
LINQは、「適当にLINQで加工した結果の順序は、保証されないと考えるべき。」との意見もあります。
そのため、例えば「社員番号順で表示してほしい」といった要件がある場合は、OrderByで並び替えています。
ただし、加工するデータの元々の順番が重要でOrderByでの並び替えが容易でない場合は、LINQではなくイテレータを使っています。
LINQの順番についての意見は、下記を参照してください。
※ 例えば、LINQでよく使うWhereは、公式ドキュメントで順序の保持が明記されているわけではありません。
副作用の排除
関数型プログラミングの考え方に沿って、できるだけ副作用を排除します。
これにより、処理の独立性やテストの容易性が向上します。
TIPS : データ読み書きのタイミング
例えば、DBの読み書きは、典型的な副作用を伴う処理です。
この場合は、特段事情がない限りは、必要なデータをあらかじめ取得しておき、取得したデータアプリケーション内で加工し、加工した結果をDBへ保存するようにします。
テンプレートメソッドパターン
例えば、ある1つのテーブルから全件データを取得するクラスの場合、「データ取得→データモデルクラスへ保存」といったデータの流れが共通します。
このようなクラスには、テンプレートメソッドパターンを活用しています。
これにより、クラス内のデータの流れを固定し、設計の見通しを良くしています。
DI
DIを活用することで、データの流れを抽象化し、部品として使い回すことができます。
個人的には、ざっくり関数合成のイメージでDIを利用しています。
TIPS : DIで処理を合成するには
例えば、DBから取得したデータを加工して表示する場合は、「データ取得」「データ加工」「データ出力」の各処理を担当するそれぞれのインターフェースを実装したクラスのインスタンスを1つのクラスに注入します。
さらに、メソッドの引数としてDBアクセス用インターフェースの実装クラスのインスタンスを受け取るように設計することで、テスト時や実装の差し替えが容易になります。
このように設計すると、個々の処理の妥当性をテストコードで担保でき、クラスを利用する側は内部実装を気にする必要がありません。
また、クラスを呼び出す側でトランザクション制御を行うことで、クラス内部でトランザクション管理をする必要がなくなり、トランザクションの状態がより明確に把握できるようになります。
補足
注入される側のクラスもインターフェースを定義することで、テストや差し替えがしやすくなります。
ただし、処理ごとにインターフェースを分ける場合、実装クラスの差し替えが頻繁に発生しないことも多く、開発手法などによっては、あまりメリットを感じない場合もあります。
私の場合はテストコードを先に書くため、インターフェースを用意しています。
コンストラクタの活用
コンストラクタにマスタから取得した値など、処理中に変更しない値を渡します。
私は、同じ値を何度も渡す手間を省くために、初期化時に必要な値をまとめて渡しています。
テストコード
テストコードを実装することで、処理ロジックの信頼性を担保します。
関数型プログラミングの性質上、副作用がなければ参照透過性が保たれ、同じ入力(Input)に対して同じ出力(Output)が得られるため、デバッグやテストコードの実装が容易になります。
実施した効果
保守性の向上
スタックトレースを見て、どの部分でエラーが発生したのかを把握しやすくなりました。
レガシーなWindows Formsアプリでよくある「データ表示→表示ボタンクリック」といったスタックトレースから、「データ表示→データ加工→データ取得→表示ボタンクリック」といったデータの流れに沿ったスタックトレースへと改善され、原因の特定が容易になりました。
処理の独立性
関数型プログラミングの考え方を取り入れることで、副作用をできるだけ排除したため、既存の処理の変更や新しい処理の追加がしやすくなりました。
おわりに
データの流れを意識した設計・実装を行うことで、保守性や拡張性が大きく向上します。
つまり、「データがどこから来て、どこで加工され、どこに行くのか」という見通しを良くすることで、保守性が向上します。
皆さんの設計の参考になれば幸いです。