2019年1月9日水曜日

変更に強いアーキテクチャについてIT業界19年目の僕が超ザックリ説明する

勉強の為に転載しました。
https://qiita.com/lobin-z0x50/items/39131a4f47ed7c5df443?utm_source=Qiita%E3%83%8B%E3%83%A5%E3%83%BC%E3%82%B9&utm_campaign=0f7ecd62ef-Qiita_newsletter_344_01_09_2019&utm_medium=email&utm_term=0_e44feaa081-0f7ecd62ef-33246877

この記事は、設計・アーキテクチャ Advent Calendar 2018 の第7日目の記事である。

はじめに

この記事では、IT業界19年目の僕が実践している変更に強いアーキテクチャについて、出来るだけ難しい表現を避け、教科書的なありきたりな内容ではなく現場の肌感覚に近い切り口で「超ザックリ」な解説を試みてみようと思う。
普段自分がよく用いている実装パターンの紹介ともいうべきかも知れない。

この記事で説明すること

いざ「変更に強いアーキテクチャとは」とズバリ訊かれても、一概に「これだ!」という答えはない。
プログラミング言語や、フレームワークによっても条件が異なるし、利用可能な技術や開発チームの特性、業務要件や運用要件の特性によっても様々であるし、インフラや開発プロセスまで含めて考えると考慮すべきことは無限にある。
ここでは主にソフトウェアの構造という観点から、"変更に強い" ということを考えてみたいと思う。
早速、スマホで "変更に強い アーキテクチャ" などとググッてみると、それなりに専門的な解説が書かれているサイトがヒットすると思う。
例えば、オブジェクト指向を筆頭に、インターフェースやポリモーフィズム、共通性分析だとか、DI、SOA(サービス指向設計)だとか。
もちろんそれらは難解ではあるが実は重要で、役に立つ概念であり、上級者になるためには理解しておくべき概念でもある。
だがしかし今回のテーマは 「現場の肌感覚でザックリ解説」 なので、専門書やググって出てくるようなサイトの難しい表記は極力避け、なるべく分かりやすく具体的に、要点だけをかいつまんで説明しながら、最後はインタフェースを使った依存関係解消の実装例まで例示してみたいと思う。

対象とする読者層

対象とする読者は、プログラミング経験1年以上、詳細設計やプログラム設計は何度かやったことあるけど、あまり自信がない若手プログラマ、オブジェクト指向の言葉は知ってるけどイマイチ使いこなせてない感が残る、初球から中級へステップアップしようとしているエンジニア、俗に言う IT土方 、といったところか。
特に、オブジェクト指向の本を読んで何となく理解したけども、実際に仕事でそれを活用できていない、具体的な実装例を見てみたい、といった人をターゲットとしている。
この記事を読んで「なるほど、そうすればいいのか」といった気づきが得られれば幸いだ。

最もコスパの良い3つの施作

変更に強いアーキテクチャとは、いくつもの概念や指標の組み合わせと、複雑な理論の上に成り立っている。
そういったいくつもの重要な概念のうち、最もコスパの良い(経験上これだけは絶対にゆずれない、費用対効果の高い)項目をあげるとすると、以下の三つだ。
  1. 適切なモジュール分割
  2. 依存関係を一方向にする
  3. テストコード
誤解の無いよう再度断っておくが、この3つだけで変更に強いアーキテクチャが必ず実現できるというものではない。
上記は昨今のソフトウェアの大規模化やフレームワークの流行、実装方式のトレンドなどを鑑みて、ソフトウェア設計者が最も考慮すべきで最も効果の高い項目、といった位置づけで選出したものある。
ある程度の設計経験がある方なら感覚で分かると思うが、実は上記3項目には相関がある
つまり、テストコードを書くためには、モジュール間が疎結合になっている必要があり、モジュール間を疎結合にするためには適切なモジュール分割が行なわれている必要がある、ということだ。
したがって 1 → 2 → 3 の順に実現していく必要がある。このとき、例えば 2 までしか達成できなくとも、それなりに大きな効果がある。

1. 適切なモジュール分割

説明のためにひとつ身近な例を挙げよう。
一般的な MVC フレームワークをモジュールという観点で考えると、Model, View, Controller にモジュールが分かれている。
図1.png
それぞれの役割分担は以下のようになっている。
  • M: データアクセス、業務ロジック
  • V: 画面の表示、UI
  • C: M, V の制御
Model が DB に対応していて、Controller から呼び出すわけだが、業務ロジックを直接 Controller に書くのはよくない、みたいな意識をなんとなく持っている人はいるだろうか。
実際、DBアクセス処理や外部APIの呼び出しなどを Controller 内に直接ずらずらと書き連ねていくのはよろしくない。
※WindowsForms の人は、Form の中にずらずらと業務ロジックを書いてあるような状況と考えて欲しい。
小規模なアプリケーションでは十分問題ないが、業務システムなどといったある程度の規模のソフトウェアでこういう作り方を続けていくと、そのうち Controller が肥大化し、苦しくなってくる(下図)1
図2.png
かといって Controller の代わりに Model に業務ロジックを実装していっても、システムの規模が大きくなれば同じように苦しくなってくる。
当然テストを書くことなんて到底不可能だ。
ではどうしたらよいのか。

「サービス」の導入

一つの解決策は、 M, V, C 以外に、S: Service というモジュールを導入することだ2
Service モジュール内に、業務や機能の分類ごとにサービスクラスを作っていき、業務ロジックはすべてそこに追い出してしまう。
Controller は単にサービスクラスを呼び出すだけ。で、サービスの出力を View に渡したり、処理結果を元に画面遷移したりという風に本来の Controller の仕事に専念すればよい。
図3.png
"MVCSアーキテクチャ" という言葉はないのだが、まぁそんなイメージだ。3
注意して欲しいのは、Service モジュールは、View や Controller に依存しないように構成しなくてはならない(後述)。
Model には依存してかまわない4。図の中で Controller の下に Service が描いてあるのはそういう意味でもある。

サービスモデル

こうやって小さなプロダクトを完成させたとして、それを機能拡張して改修して、と続けていくと、サービスモジュール内のあるサービスクラスが大規模化してくることが起こりうる。そしてクラスの入出力データも大規模化してくる。
たとえば、検索処理を行って検索結果一覧を返すサービスがあったとする。
その入力は、検索条件を表す画面からのポストデータ(というか連想配列)で、その出力は Model の配列だったとしよう。
ところが機能拡張により検索条件が複雑化して、入力データが連想配列では苦しくなってくる。
そういうときには、サービスの入力データを表す新しいモデルクラスを定義し、それを使ってデータを受け渡すようにする。
MVCフレームワークの Model とは区別したいので、それぞれに適切な呼び名をつけよう。
ここまでの説明上の流れでは、MVCフレームワークの Model はデータベースモデルを指しているとしているので、これを「EntityModel」と呼ぶ。
そしてサービスの入力データは、なんでも良いのだが、ここでは「ServiceModel」とでもしておこう。
図4.png
※なお、この図からは Model を表す箱は「データ」を意味する平行四辺形で描くことにした。
視覚的に分かりやすくするためであり、それ以上の深い意図はない。

ビューモデル

次に、サービスの出力データが複雑化してくるケースだ。
これもサービスクラスが大きくなってくると当然のように起こりうる。
この場合も同様に、それを表す新しいモデルを定義し、それを使ってデータを受け渡すようにする。
名前は、どうしようか。
サービスの出力はビューに出力するためのデータであるとすると、「ViewModel」とでも呼ぶことにする。
図5.png
ビューモデルを使うアーキテクチャとして MVVM 5 があるが、まさにそれと同じようなイメージだ。この構成は Ajax を使うときにもよくマッチする。 View がブラウザ側で動いているような構成になるが、ViewModel をそのまま Json エンコードしてフロントに返してやれば、VueJS などで直接バインディングするだけで画面に値が反映される。とても分かりやすくなるしラクになるので、オススメのアーキテクチャパターンの一つだ6
ViewModel という名前に違和感があるなら、違った観点から命名してもよい。
例えば、WebAPI の出力を表しているのなら、WebApiResultModel としてもいいだろう。
このように、データの意味に基づいて〇〇モデルというように命名して、入出力データを整理していけばいいのだ。
※(2019-01-02 追記)個人的に Model という言葉は抽象的すぎると思っているので、例えば MVC の M がDBを表しているモデルなら EntityModel と呼ぼう、みたいに、具体的に〇〇モデルと呼び分ける方が整理しやすくなるし、理解しやすくなると思っている。 
この時、サービスの入力と出力を同一のモデルに統合してしまわないとこ。
本当に同じ意味のデータならそれで構わないが、意味として異なるのであれば別々のモデルとして定義するべきだ。
たまに見かける失敗例の一つなので、注意して欲しい(僕も何度かやらかした記憶があるが、それは秘密にしておこう)。

バッチ処理

さてここで少し視点を変えて、バッチ処理についても考えてみたい。
業務システムに於いて必ずといっていい程存在するのが、スケジュールバッチや夜間バッチなどと呼ばれるいわゆる "バッチ処理" だ。
現場の肌感覚で説明するといった以上、バッチ処理についても触れざるを得ない(こういった現場目線での具体例が専門書には少ないように思う)。
MVCに於けるコントローラと同様に、実はバッチ処理についても、バッチ本体に直接業務ロジックをダラダラと書き連ねるべきではない。
業務ロジックはサービスクラスとして分離独立させ、バッチからはそのサービスを呼ぶだけ、という様な構成が好ましい。
つまりバッチ本体は単純なコントローラーと同じような役割となるべきだ。
図6.png
こうすることで、バッチ処理についてもアーキテクチャの統一感をもたらし、モジュール間の疎結合を実現し、テストコードで保護するための基本構成が得られるようになる。

2. 依存関係を一方向にする

まず依存関係について少し掘り下げて説明しておこう。
あるクラス A とクラス B があるとする。
A が B を利用(呼び出しや参照)しているとすると、A は B に依存していることになる。次に、B からは A を一切利用していないとすると、B は A に依存していないことになる7
クラスだけでなくモジュールに於いても同様に依存関係を考えることが出来る。
先程少し触れたように、Service は View や Controller に依存しないようにしなくてはならない。
つまり、各モジュールは下位のモジュールにのみ依存するようにして欲しい。
言葉で説明するとイマイチぴんと来ないかもしれないので、下図を参照して頂きたい。
これまで提示してきた図を、モジュールごとの上下関係(レイヤー)と依存関係に着目して書き直してみたものだ。
図8.png
このように、図の上の方にあるモジュールから下にあるモジュールを参照したり呼び出したりするのは構わないが、その逆はダメだ。
また、同列のモジュール間で相互に依存するようなことが無いように注意する8
モジュール間(そしてクラス間)の依存関係を上から下へ一方向にし、レイヤーを構成させるわけだ。
要点としては、ある下位のレイヤーだけを取り出して、その部分だけでも構造が成り立つような仕組みになっている必要がある。
例えば Service とその依存関係にある下位のモジュール(ServiceModel, ViewModel, EntityModel)を取り出して、その部分だけで構造が成り立つようになっているのが分かるだろうか9
これがもし、Service から Controller への依存があったりすると、そうはいかない。Controller と Service が奇麗に分離されていないということになる。

3. テストコード

ここまでの構成が出来れば、とりあえずテストコードを書くことが出来る。
サービスクラスが下位のモジュールにしか依存していないので、そこだけ切り出してきて全く別のコードベースから呼び出すことが出来るはずだからだ(下図)10
図7.png
これまでモジュール分割だの依存関係だのと厳しい制約のもとソフトウェアの構造を整理してきた目的は、ここにある。

テストコードを書くことのメリット

今さらかもしれないが、テストコードを書くことのメリットをおさらいしておこう。
  • コード修正の度にデグレが発生していないか繰り返すテストを実行して検証することができる11
  • 品質が向上する
  • テストコードを書くこと自体にコストがかかるが、長い目で見ると全体的なコストは下がる
  • テストコードを読むことでそのクラスの使い方(仕様)が分かる
  • テストが通っているという絶対的な安心感
  • そもそもテストコードを書ける構造になっていることに価値がある
最後の項目が、最初に述べた 1, 2, 3 の相関関係の裏返しとなっている。
つまり、奇麗な構造になっていないとテストが書けないのだ。
しかしながらテストコードのないソフトウェアが依然として多い。大規模SIプロジェクトや、パッケージソフトではそのほとんどがテストコード無しという現状である。
一方で、テストコードを書かない文化というものもあるようで、あえてそのような選択をしている開発チームであればテストは書かなくてもよいと思う。しかし、テストを書いたこともないのに書かない選択をしているとしたら、是非この機会にチャレンジしてみて欲しい。
特に、プロダクトの初期構築の段階に携わっている場合は、テストコードを導入するチャンスだ。最初にテストコードが書ける構造を整備してテストを書いておかないと、後になってからテストを導入することは非常に骨の折れる作業なのだ。理由は、先述の通りソフトウェアの構造が奇麗になっていないとそもそもテストが書けないため、構造改革に甚大な労力を要するためだ。それを裏返せば、テストコードを維持しつ続けることでソフトウェアの構造が奇麗に保たれる、という効果が得られるのだ12

サービス間の依存関係への対処

さて話を続けよう。プロダクトの構築が終わって、さらに開発が続いていったとして、コードベースが発展し大規模化してくると、サービスクラスが多くなりサービス間の依存関係が増えてくる。その度に、それらの依存関係を少しづつ減らしていく努力が必要だ。そうしないと、テストコードを書くのがだんだん苦しくなってくる。
例えばインスタンス生成の依存関係を解消するにはいくつか対応策があるが13、それらの詳細な解説は他のサイトにお任せするとして、ここでは利用の依存関係を解消するために(王道とも言われる)インターフェースを抽出する方法を説明する。

依存したサービスを切り離す

説明のための例として、ここに「商品受注サービス」があるとする。これは読んで字のごとく商品の受注処理を行うビジネスロジックだと思って欲しい。
そしてその中では、受注データの納期をセットするために「納期検索サービス」を呼び出しているとする。
これらの依存関係を図で表すとこうなる。
図9.png
ここで「商品受注サービス」のテストを書くことを考えてみよう。
このサービス(クラス)のテストを書くためには、内部で利用している納期検索サービスのためのデータ(例えば納期マスタ)を用意しておく必要がある。そうなるとテストコードの書く量が増えることになる。
それに納期検索サービスにバグがある状態だったり、開発工程の関係でまだ実装されていないサービスだったりするかもしれない。
そもそもこれは単体テストというより、結合した状態でのテストになってしまっているのだ14
テストコードを書く負荷を減らすためには、テストの粒度を下げる必要がある。すなわち何とかしてこれを単体テストにする必要がある。
そこでインターフェースの出番だ。

インターフェースの抽出

まず納期検索サービスの中で必要なメソッドをインタフェースに抽出する。
そして「商品受注サービス」から「納期検索サービス」に依存するのではなく、そのインタフェースに依存するように変更する(下図)15
図10.png
これで果たして、単体テストが書けるようになる。
この後は、サンプルとして C# での実装例まで見てみてほしい。Java でも大体同じような感じだ。

C# での例(簡易テストコード)

いざテストを書くと言っても、書いたことない人にとっては全くどうしていいのか分からないことと思う。
Qiitaを読んでいるような意識高い系の人はそんなことないのかも知れないが。
まず心理的ハードルを下げるために一言いっておくと、重厚長大なテストフレームワークを無理に使う必要はないのだ。
エンタープライズアプリケーションの開発現場に於いて、テストフレームワーク新規導入するには時間と手間がかかるし、開発チームの他のメンバーに使い方を覚えてもらうための学習コストもかかるし、テストコードの書き方や適用範囲などの標準化まで考慮しないと、実際にテストコードを書く運用を始めることは難しいだろう。
ここでは、簡易的にテストコードを書く方法もあるということを紹介するため、本質とはズレるかもしれないが、テストフレームワークを使わずに独立した実行モジュールを自作することでテストを書いてみる。C# を選択したのは好みの問題だ。
Javaでいうところの main() 関数を含むクラスを自分で書いて、コンパイルして java コマンドで実行する、.NET でいうところのコンソールアプリケーションを作って .exe を実行する、みたいなイメージだ。
納期検索サービス
商品受注サービス
テストコード
こんな風に自力でテストを書けるはずだ16
余談になるが、Rubyなどのダックタイピングが出来る言語で同じような構造を考える際にはインターフェースを明示的に定義することはないが、僕の頭の中ではインターフェースがあるものとしてクラスを構成しているような感触がある。
動的言語の力によりインターフェースを定義しなくてよいのでコードの記述量が少なくなり、慣れればスッキリした気分で書けるが、その裏を返せば頭の中にインターフェースが見えない人にとっては、理解に時間を要したり、そもそも理解できないこともあるのだろうと思う。
そういう意味では、開発者のレベルに応じてプログラミング言語を選定することも大切なんだなと思う。

あとがき

変更に強いアーキテクチャの本質は、テストフレームワークを使うことでも、優秀な設計ツールを使うことでもない。
これまで見てきたように、適切にモジュール分割をして依存関係を整理し、それらをテストコードで保護していく事が本質だ。
それを実施するために必要な概念を整理して知識としてまとめたものがオブジェクト指向設計論であり、テストフレームであり、ドメイン駆動開発などの方法論だと僕は思っている。
方法論だけを論じても意味がない。本質を見極めて、現実世界での困りごとに適切に適用できるようになりたいものだ。
そうはいっても、テストフレームワークを使う方がずっと楽にテストが書けて運用しやすいので、自力で実装する例はあくまでお試し、こんなことも出来ますよ、という参考までにとどめておいて欲しい。一般的に流行しているようなフレームワークはとりあえず触れてみるなどして、継続して自分の知識をアップデートしていくべきだ。
ここまで、3ステップでの施策として構造設計の課題点と解決策を駆け足で見てきたが、そもそもそれらをどこまで適用するかの見極めも重要である。小規模でラピッドなプロトタイプ開発であれば、最初からコントローラに全部処理を書いてしまってもいいし、中規模まで発展することを見込むなら、ユニットテストだけでなくインテグレーションテストまで最初から用意する、みたいにケースバイケースで判断していくしかない。これにはやはりある程度の経験が必要だろう。
また、この記事では出来るだけ専門用語を避け、平易な表現で説明してきたが、向学の志がある方のために注釈にて専門用語の紹介を書いておいた。興味があれば自分で調べてみて欲しいと思う。
長くなったが、この記事によって一人でも多くの人が「テストコードを導入してみよう」と思うキッカケになれば幸いに思う。
現場からは以上だ。

  1. Fat Controller とも呼ばれ、アンチパターンの一つだ 
  2. Service という言葉には別の意味があるかもしれないが、SOA(サービス指向アーキテクチャ)という考え方があり、システムの規模が巨大化していくにつれて最終的には SOA に近い形態を目指すこととなるため、僕は最初からサービスという概念で設計していっている。 
  3. ここでいうサービスモジュールは、DDD(ドメイン駆動設計)ではドメインモデルという言葉で表されるものだ。 
  4. 本当はデータアクセスを抽象化する「リポジトリパターン」というアーキテクチャパターンもある。とりあえずここではテストコードからも実際のDBにアクセスする前提としている。 
  5. Model-View-ViewModel アーキテクチャ。Microsoft の WPF を始めフロントエンドライブラリでよく用いられる。DBに対応した Model とは別に、View に対応した ViewModel を用意し、画面項目にバインディングするような考え方。 
  6. この場合は MVC フレームワークの View はほぼ使わないことになる。VueJS や AngularJS などのフロントエンドで MVVM を実現するフレームワークを導入するなら、バックエンドは WebAPI としての役割に徹する方が分かりやすくなる。 
  7. 依存関係には度合いがあり、例えばUMLではクラスの継承(is-a), インスタンス生成(create)、所有(has-a)、利用(uses-a)というように分類わけされている。このうち利用(uses-)が最も弱い依存関係だとされており、インスタンス生成(create)は最も強い結合、プログラミング言語でいう new する事にあたる。new するには相手の実体を知っている必要があるためだ。 
  8. 循環参照といってアンチ設計パターンの一つだ 
  9. クリーンアーキテクチャという概念がこれに近い。実際、クリーンアーキテクチャという言葉は最近知ったので、それに影響を受けているわけではない。自分の勉強不足に反省。。。 
  10. テスト用にDBを分けて、テストコードからテスト用DBにアクセスするというやり方になる。本当はもっと色々考慮した方がいい事があるのは重々承知であるが、とりあえずテストコードを書くことは出来る状態になっている、ということの本質をここでは伝えたい。 
  11. リグレッションテスト(回帰テスト)という 
  12. "奇麗" の基準に個人差があるので賛否両論あるところかと思うが、少なくともここで言わんとしているモジュール分割と依存関係の一方向性は維持できるはずと考えている 
  13. 興味のある方は ファクトリーパターンや、DI(Dependency Injection: 依存性の外部注入)などのキーワードでググってみて欲しい。 
  14. もちろん結合テストのためのテストコードというものも存在する。Selenium などのテストフレームワークでは、ブラウザを実際にテストコードで操作してUIからバックエンドまでモジュール結合した状態でシナリオテストを行うようなことが出来る。インテグレーションテスト、e2eテスト(End to End Test)というキーワードで調べてみよう。 
  15. オブジェクト指向でいう依存関係逆転の原則である。その原則に厳密に従うなら、インタフェースは実は商品受注サービスの側のモジュールに属している必要がある。 
  16. なお、サンプルコード中にコメントで書いてあるように、実際にはフレームワークの初期化やDBの初期化などのお膳立てが必要となる。例えば EntityFramework なら App.config を用意するだけで何とかなるし、高級なフレームワークほど外部コードから呼び出すための手順などが整備されているようなので、フレームワークのドキュメントを詳細は参照されたい。実際、Java の Play2 + JPA の案件でこのようにして自力で簡易テストコードを書いたりしたことがあった。 

0 コメント:

コメントを投稿