.NET で作った一昔前のイケてないソリューションを最近のイケてるソリューションにする


もしかしたら一昔前と言わずもっと前かもしれないけど、安全にイケているものにバージョンアップするにはこんな感じで進めて行けば大丈夫ですよ。と言った内容です。
対象読者はタイトルの通りで、システムが流石に古すぎるしメンテナンスも大変すぎるから少しづつ変えていくか。みたいなことをやりたい人におすすめです。
少し長いので手っ取り早く最終的な構成が知りたい人は ソリューション構成のベストプラクティス までスクロールしてください。

プラットフォーム

Windows forms
ASP.NET Web forms

上記二つを対象にイケているものにしていきます。 WPF とか ASP.NET MVC はそこまで古くないと思うので省略します。

プロジェクト構成

大体の構成は二つのどちらかに当てはまると思います。

パターン1: 全ての処理(コードファイル)を適当に突っ込む

パターン2: 3階層アーキテクチャ (一つのプロジェクトに全部)

3階層アーキテクチャって何?という人は ビジネス・アプリケーションの論理3階層 - IT の最初の方を見てください。

どこがイケてないのか

一言でまとめると メンテナンスが大変 で終わってしまうのですが、エセ外人風にもう少し掘り下げてみましょう。

むむ。どこにメンテナンスしたい処理があるのかわからないぞ?

正しく階層分けされてされていないか、もしくは階層構造が壊れている可能性があるので直しましょう。

3階層アーキテクチャの階層名も凄くわかりやすいのですが、今回は Domain Driven Design (通称 DDD) の階層名前で説明します。こんな感じに分けます。(やっていることは 3階層とほぼ同じです。)

実際の DDD とは少し異なるので、きちんとしたものを作りたいなら少しづつメンテナンスしていくのではなく、しっかりとドメインをまとめた上でシステムを作り直してください。
ただ、マスター系や、テーブルスキーマとドメイン層が 1:1 に設計されている場合は DDD で正しく分けられますが、業務アプリケーションはそこまでしっかり設計されていないと思うので作り直しは大変です。
今回はこんな感じで作ります。

Application Layer

アプリケーション層で実装する主な内容は下記の通りです。

  • データの入出力を行う UI (User Interface) の提供
  • 入力データの妥当性チェック (Validation)
  • ドメイン層のコンポーネントの呼び出し

ドメイン層に渡すオブジェクトへの変換や、ドメイン層から戻ってきたオブジェクトをアプリケーション層のオブジェクトに変換するような処理もこの層で実装します。

Domain Layer

ドメイン層で実装する主な内容は下記の通りです。

  • 業務ロジックの実装
  • トランザクションの管理
  • インフラストラクチャ層の CRUD の呼び出し

アプリケーション層から渡ってきたオブジェクトを元に業務ロジックを処理して、必要に応じてトランザクション管理やインフラストラクチャ層の CRUD を呼び出します。
ドメイン層ではアプリケーション層で行うことをしてはいけません。 UI Control などがパラメーターとして渡ってきたり、メッセージの表示やアラートなどをここで実装してはいけません。
クラスライブラリとして別ソリューションで作成して、 System.Windows.Forms や、 System.Web.UI (これは System.Web にくっついてきた気がするからどうしようもないかも) などを参照に入れないでおくと、うっかりミスを防げるようになります。

Infrastructure Layer

インフラストラクチャ層で実装する主な内容は下記の通りです。

  • データベースなどの永続モデルに対してのアクセス
  • SQL の実装
  • O/R マッパーの実装

この層では主にデータベースのアクセスを管理します。少し古いシステムだと O/R マッパーにデータセットが使用されているケースがあると思います。
個人的には 重い null が使えない 管理が大変 の三拍子が揃ったデータセットが嫌いなのですが、これを改修するのは結構大変だと思うので時間がとてもある時にしたほうがよいです。

なんてこった!コードを変更したら別の機能が壊れてしまった!

モジュール結合度が高すぎたり、共通部品が肥大化しすぎているのが原因なのできちんと分けましょう。

結合度を下げるためには DI を使用するのが一番てっとり早いです。 DI って何?という人は やはりあなた方のDependency Injectionはまちがっている。 を参照してください。
DI のライブラリはたくさんあるのですが、とりあえずここではライブラリを使用せずコンストラクタでインジェクションしておこうと思います。

private readonly IRepository1 _repo;

public Service1(IRepository1 repo)
{
    // Constructor injection.
    _repo = repo;
}

public Repository1Entity MethodA()
{
    return _repo.Find(new SelectParameter { Id = 123 });
}

上記ではインターフェースを使用するようになっていますが、どうしても使用したくないという拘りがある人は public membervirtual をつけておけば、それなりになんとかなります。
また、共通部品の肥大化への対策は機能ごとにクラスを分割するのがよいです。例えば文字列操作系の共通サービス、暗号化の共通サービスなどといった共通サービスに分けていくことでコードの保守性が上がります。
綺麗なオブジェクト指向設計では共通部品は悪だ!みたいな考え方もあるのですが、古いシステムにこれを適用するとしたら設計し直しからやらなければならず時間がかかるので、コードを役割ごとに分割するくらいが一番楽しく対応できると思います。

ハハハッ!メンテナンスの度に同じテストを何度も手作業でやる必要があるぜ!今日も残業だ!

可能な箇所から少しづつ自動化テストを導入しましょう。

とても非効率なのでテストコードを書いて自動化テストを初めましょう。慣れるまで時間がかかると思いますが、そのうち無駄な残業の恐怖からも開放されて自由になれます。
自動化テストについては前に書いた 業務用 Web アプリケーション開発のテスト自動化 が参考になると思います。
具体的にはこんな感じの構成になります。

ソリューション構成のベストプラクティス

個人的なベストプラクティスはこんな感じだと思います。

この状態でヘルパークラスなど必要な物を好きなように追加していってください。
階層ごとにプロジェクトを分ける分けないは、それぞれメリットが含まれるので下記を参考に決めてください。

分けるメリット

  • 階層関係が必ず正しくなる(下位レイヤーから上位レイヤーは参照できない)
  • ドメイン層の使い回しができる(アプリケーション層を復数用意することでクロスプラットフォームなどの対応が可能)

分けないメリット

  • 参照ライブラリが復数に分散しなくてすむ
  • 設定ファイルの値がどこからでも簡単に参照できる
  • モデルオブジェクトの使い回しができる(分けるべきだと思いますが、マスター系の画面などはほぼ同じものを復数作る必要があるため少し面倒)

雑記、参考資料など

いくらイケてないソリューションだからと言っても過去に沢山の利益を生み出しているはずです(どうでもいいアプリはなくなっているだろうし)。なのでバージョンアップする際は必ず愛を持って接してあげてください。
最近のアーキテクチャはそれなりにしっかりしているので、イケてないソリューションは誕生しないと思いますが、もし誕生しそうになったら全力で止めてあげてください。後々保守開発する人たちが地獄を見ることになってしまいます。
今回はあくまでソリューションのみ対象にしてバージョンアップをしたのですが、コードを綺麗にしたり CI/CD を使用したりメンテナンスしやすいドキュメントにしたりなどソリューション以外の部分も組み合わせることによって無限のパワーで戦えます。時間的に余裕があったりする場合はぜひそちらも試して頂きたいです。

アーキテクチャを考えるときに一番参考にしたのは TERASOLUNA Server Framework for Java (5.x) Development Guideline の 2.4 - 3.4 です。興味がある人がいたら見てください (コードは Java です) 。