FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

単一責任の原則(SRP)についての見解と方法論

こんにちは@kimutyamです。

今週末はいよいよScalaMatsuri2017ですね。

弊社は、今年も将軍スポンサーとして参加させていただきます。
そして今年も同人誌を配布させていただきます。

同人誌については去年配布した話を杉谷がブログで紹介しております。 labs.septeni.co.jp

今年の同人誌目次は以下となります。

  • ScalaAndroidアプリ開発
  • 単一責任の原則(SRP)についての見解と方法論
  • 末尾再帰の呼び出し最適化の有無によるScalaコンパイラの挙動について
  • Akka HTTP で LINE bot を作ってみました
  • 新卒scalaエンジニアが書くISUCONの歩き方

去年に比べて記事数は少なめですが、内容は濃くなっております。
ちなみに私は弊社の今泉と一緒に『単一責任の原則(SRP)についての見解と方法論』を執筆しました。 (Scala関係ないw)
代わりに他のエンジニアがたくさんScalaのことについては執筆しております!!
去年は700部刷ったのですが、おかげさまで全て配布できました。
今年は800部刷りました!!
ScalaMatsuri会場に持っていきますので、是非手にとってみてください。

では、同人誌の一部である『単一責任の原則(SRP)についての見解と方法論』を紹介します。どうぞ。


『単一責任の原則(SRP)についての見解と方法論』

前説

飲み屋で小一時間議論が白熱した。 木村と今泉の中で単一責任の原則の考え方が違ったからだ。
木村がシステムリーダーを担っているプロジェクトでは、「あー。単一責任の原則はちゃんと守ろうね。一般的なOOPの考え方だよ。」というテンションでプロジェクトを進めていく内に、ドツボにハマった。
スタートアップさせるプロジェクトで初期設計者は木村含めた2人で行った。
最初の方はうまくいった。4人くらいまでチームがスケールしても初期設計はたいしてぶれなかった。
そんなこんなでマーケットが変わり、組織構造が変わり、チームメンバーが増減する中で気付いたら設計違反しているコードが目立つようになった。
設計を抜本的に改善している過渡期に今泉がチームにジョインしてきた。
「単一責任の原則は一般的だからこそ明文化する必要があるんだよ」と発言してくれて、木村も納得感を得たので明文化してみた。
そしたらメンバーによって単一責任の原則の考え方が違うことが発覚したのだ。
我々は議論を重ね、単一責任の原則を考えるにあたって考慮すべきことをいくつか発見することができた。 本稿ではそれについての見解や方法論を紹介する。

過去の見解

木村の過去の見解

単一責任の原則は凝集度を強め、結合度を弱めて内部品質を高めるための戦術の1つくらいに捉えていた。
モデルの正当性はクライアントクラスを実装する時に明らかになると考えていた。
つまり、変更理由はクライアントクラスとその実装者によって発生するものだとしていた。
“ある程度"経験則と対話によって要件の変更を予測して、設計していた。
ただ、この予測が属人化しており明文化されていることはなかった。
可能性が低いクラスやモジュールに対しても構造分割をしているように映り、不必要な複雑さと捉えるメンバーもいたようだった。
ビジネスの核はドメイン駆動開発(DDD)で捉えたつもりになっていたが、要件レベルでの変更理由との同期を都度都度にとれてなかったため、要件に沿わないモデルができあがってしまった。

今泉の過去の見解

1つのクラスが色んな事をできるのを防ぐためのものくらいに捉えていた。
つまりそもそも誤った認識をしていた。
所謂神クラス的な物を避けるために、このクラスの役割はこれ1つだと定めるもの。
そのため、クラスの役割の粒度をプロジェクト単位で定めなければ、プロジェクトでの意思統一ができないものだと思い上記のような発言になった。
余談となるが『オブジェクト指向入門 第2版方法論・実践』の「22.2.2 私のクラスは・・・を実行する」では、クラスに対して以下のような事を記載している。

クラスは何か1つのことをするものではなく、特定の型のオブジェクトに対していくつかのサービスを提供するものである。 クラスが本当に1つのことだけをするのならば、おそらく偉大なる過ちのケースの1つであろう。 すなわち、ほかの何らかのクラスの1ルーチンにすべきものに対してクラスを作ったのである。

話を戻し、その議論の後、そもそも単一責任の原則って何?というところで原典を漁っていき、『アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』にたどり着く。
会社で本を購入し、読んでみた所、責務 = 変更理由となっており、上記議論の際にもそもそも変更理由という単語が登場しない事に違和感を感じた。
今までコードレビューや会話等で責務という単語は多数聞いていたが、その全てが本当に変更理由を指していたとは到底思えないくらい変更理由という言葉の出て来る回数は責務に比べ少ない印象があった。
上記の誤解の後に、モジュールを変更する要因とはなんだろうと考えていった際にプロジェクトのステークホルダーステークホルダーを動かす組織体制というビジネス要因の部分。
プロジェクトのコード規約や言語、ライブラリ等のアップデート等の技術的要因の部分。
この2点があり、どちらかというと私は前者であるビジネス要因の部分、すなわち要件を強く考慮する必要があると感じた。

原理原則的にはどうなのか?

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』の見解

単一責任の原則(SRP)

クラスを変更する理由は1つ以上存在してはならない
単一責任の原則(SRP)では、「役割(責任) = 変更理由」と定義している。クラスを変更するのに2つ以上の理由がある場合、そのクラスには2つ以上の役割があることになる。

この「変更理由」こそが立場によって解釈が異なるのだ。
アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』ではSOLIDが提唱されており、他の原則からも変更理由に関して考えを強化できる。

オープン・クローズドの原則(OCP: Open-Closed Principle)

ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対して開いて(オープン: Open)いて、修正に対して閉じて(クローズド: Closed)いなければならない

どんなに「閉じた(Closed)」モジュールであっても、閉じることのできない変更が必ずあるのだ。つまり、すべてのケースに適用できる自然モデルなど存在しないのだ!
あらゆる変更に対して完璧に閉じることが不可能なら、戦略的に閉じるしかない。

(中略)

では、どうすれば発生しそうな変更を推測できるだろうか? 適切なリサーチを行い、適切な質問 を投げかけ、その上で経験と常識を使う。それ以外にできることは何もない。あとは実際に変更が起 きるのを待つしかない。

完璧なモデルが存在しないのであれば、先を見越しすぎても却って「不要な複雑性」につながる。
一方で先を全然見越さなければ、拡張性と修正性が損なわれる可能性がある。
モデリングする上でこのバランスを保つために変更理由となりうる登場人物との対話は欠かせないだろう。

リスコフの置換原則(LSP: Liskov Substitution Principle)

派生型はその基本型と置換可能でなければならない

リスコフの置換原則は私たちを非常に重要な結論に導いてくれる。それは、「モデルの正当性は立場によって異なり、立場抜きに正当性を証明することは意味をなさない」ということだ。そのモデルの正当性を明確にできるのはクライアントだけなのだ。
ある特定の設計の正当性を検討するときは、他の立場を無視してそれ単独で考えてはいけない。その設計を利用するユーザーの視点で考え、その立場で合理的な仮定をしなければならない。

この結論の前には正方形と長方形の設計の話があった。
正方形は長方形の派生型と定義して最もらしいモデルができあがったが、長方形の抽象を使うクライアントの使い方を考慮していなかったためクライアントにとって不備のある設計となってしまった例だった。
このように、立場が異なるとモデルの組み方も変わるので、変更理由となりうる立場の人は誰なのか、その立場の人の考えを捉え、対話する必要がありそうだ。

オブジェクト指向入門 第2版原則・コンセプト』の見解

開放/閉鎖の原則

モジュールは開いていると同時にとじているべきである。

モジュールが拡張を受け入れられる状態にある場合、そのモジュールは開放されているという。
モジュールがほかのモジュールから使用できる状態にある場合、そのモジュールは閉鎖されているという。

どうすればモジュールを開放されているのと同時に閉鎖されている状態にすることができるのだろうか。
オブジェクト指向ならば、継承のおかげで特にエレガントにこの問題に対処することができる。

あるモジュールAを利用しているモジュールBが存在し、その後Aのモジュールを拡張、改修したモジュールを要求する別のモジュールCが登場した場合にモジュールAをどうすればいいのか。
こういう時にAを継承したA’を使えば、開放と閉鎖を満たせるというもの。
アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』でのオープン・クローズドの原則は『オブジェクト指向入門 第2版原則・コンセプト』の開放/閉鎖の原則を参考元と置いているが、『アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』でのオープン・クローズドの原則と『オブジェクト指向入門 第2版原則・コンセプト』の開放/閉鎖の原則はかなり違った印象を与える説明がなされている。
オブジェクト指向入門 第2版原則・コンセプト』ではオブジェクト指向におけるモジュール性に関する原理原則の文脈で登場しており、解決の手法に上記の様に継承を用いており、継承はエレガントに開放と閉鎖を達成すると記載されている。
一方『アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』でのオープン・クローズドの原則では抽象が鍵であると記載しており、さらにこの抽象はやりすぎると余計な複雑性を生むことの指摘をしている。
原典を漁っている時、同じ原則でさえこのように異なる視点で記述されている点を考えると、自分の中で当たり前と思っている原理原則達が、本当に正しく認識できているのか疑う視点を持つことの重要さというものがよくわかる。

単一責任選択の原理

ソフトウェアシステムが選択肢を提供しなければならないとき、そのシステムの中の1つのモジュールだけがその選択肢のすべてを把握すべきである。

同じif文の分岐が多数のモジュールにある必要はない、1つにあれば多相性と動的束縛で解決出来る。というもの。
まとめの部分では下記のように記載されている。

単一責任選択の原則により、特定の概念の変化表明の完全な一覧の情報が分散されるのを防ぐことができる。

この記載を見た時に、「特定の概念の変化表明の完全な一覧の情報」と単一責任の原則における「変更理由」が 近しい概念を示しているように感じられた。

単一責任の原則で考慮する戦略と戦術

戦略について
戦略は顧客の要求やシステムの外部要因(組織構造や外部システム)、システム境界(ユースケースなど)、ドメインを変数として立てる。
これらの前提が変更するとシステムに対して大きな変更理由になる。
戦略面が明確になっていないとクラスやモジュール粒度での変更理由の統一感を持つことは困難だ。
戦略が整っていない(ないしは浸透していない)場合は設計の関心事が技術よりに向かう傾向がある。
この場合、単一責任の原則を適用する上でも技術よりな視点になってしまう。
システムによってもたらされる価値が重要だという本質から逸脱してしまう。
従って、戦略を先に立てる必要がある。

戦略の立て方
戦略の立て方で有用だったこと、もしくは有用だと思われることを紹介する。

  • ドメイン駆動設計(DDD)
    ドメイン駆動設計(DDD)では戦略的設計を扱うパターンがあり、単一責任の原則を扱う上で重要な視点となる。
    戦略的設計はシステムの全体像を捉え、コンテキストの関係性を明らかできる。
    コンテキストとはドメインモデルの解決領域のことを指す。
    このコンテキスト単位でシステムやモジュール、チームを分割していく。

  • リレーションシップ駆動要件分析(RDRA)
    リレーションシップ駆動要件分析(RDRA)では要件の構成要素を4つの領域に整理し、それらを4つのステップとして捉えている。

    1.システム価値(価値を生み出す対象)
    2.システム外部環境(システムを取り巻く環境)
    3.システム境界(システムとの接点)
    4.システム(システムそのもの)

    f:id:kimutyam:20170221160907g:plain
    『構造に沿って要件をUMLで具体的に定義する』より引用

  • コンウェイの法則
    「システムを設計する組織は、その構造をそっくりまねた構造の設計を生み出してしまう」というとても有名な法則。
    開発組織とアーキテクチャが一致していない状態で、技術的な変更要請が発生した場合、開発組織のコミュニケーションの複雑さ、修正難易度の高さ等は安易に想像ができるのではないだろうか。

  • 組織はマーケットに従う
    『組織パターン チームの成長によりアジャイルソフトウェア開発の変革を促す』という本の組織構築パターンの中にあるパターン言語の一つである。
    マーケットの構造を開発組織に反映させる事を説いているパターンであり、上記のコンウェイの法則と連動し、アーキテクチャはマーケットに従わなければならないという事を説くパターンでもある。
    つまり顧客要求のさらに先にあるマーケットの変動というものが変更理由の大きな要因となる。
    変動が大きいマーケットに関わるアーキテクチャは、その変動の大きさに耐えうる柔軟性の高いものを要求される事となる。

戦術について
戦術は戦略を満たすためのプロセスを指す。
戦略から予想できる変更理由からクラス設計を駆動する。
クラス設計で迷ったり、議論に収拾がつかない場合に戦略に立ち返るとよいだろう。
チーム方針やコード方針を立てて戦略からぶれないようにすることや、テスト駆動開発によって変更理由のフィードバックサイクルを早めることは1つの戦術となりうるだろう。
コード方針を整える上でパターンを利用することは有用だ。
DDDの戦術的設計のパターンGoFを始めとしたデザインパターンを元に議論するのもよいだろう。

結論

我々の見解の違いは要件とクライアントクラスだったがどちらも間違っていないと思う。
しかしオブジェクト指向は設計と開発の境界をシームレスに行っていく技法であることから、どちらの面からも原則を捉えるべき事が本来必要であった。
すなわち、要件の変更に対し柔軟に対応するための戦略、クライアントクラスからの要請に対し柔軟に対応する戦術(この対応が逆転することも往々にしてあるのだろう)、その両方が揃うことで初めて単一責任の原則が目指す所に辿りつけるのだろう。

用語

不必要な複雑さ

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』の引用

「不必要な複雑さ」を内包している設計とは、設計している時点で不必要な要素が含まれている設計である。
こういった問題は、開発者が私用の変更を先取り師、あとで仕様が変更になったときに対処しやすい仕組みをあらかじめソフトウェアに仕込んでおく時に発生する。

DDDの戦術的設計のパターン

戦術的設計としてDDDは7つのパターンを提供している。
- エンティティ - 値オブジェクト - モジュール - サービス - ファクトリ - リポジトリ

参考文献

1) Bertrand Meyer『オブジェクト指向入門 第2版原則・コンセプト』 翔泳社
2) Bertrand Meyer『オブジェクト指向入門 第2版方法論・実践』 翔泳社
3) Robert C. Martin『アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技』SBクリエイティブ
3) 神崎善司『モデルベース要件定義テクニック』秀和システム
4) Eric Evans『エリック・エヴァンスドメイン駆動設計 ソフトウェアの核心にある複雑さに立ち向かう』翔泳社
5) 神崎 善司 『構造に沿って要件をUMLで具体的に定義する』(https://codezine.jp/article/detail/5689 閲覧日: 2017/01/13)
6) James O.Coplien、Neil B.Harrison『組織パターン チームの成長によりアジャイルソフトウェア開発の変革を促す』




では、会場でみなさまにお会いできることを楽しみにしております!