よくわかるSOLID原則1: S(単一責任の原則)
ソフトウェアエンジニアが知っているべきSOLID原則についての記事です。SOLID原則は、5つの原則の頭文字を並べた言葉で、S・O・L・I・Dそれぞれの原則について、5回に分けて説明します。
1) Single Responsibility Principle:単一責任の原則
2) Open/closed principle:オープン/クロースドの原則
3) Liskov substitution principle:リスコフの置換原則
4) Interface segregation principle:インターフェース分離の原則
5) Dependency inversion principle:依存性逆転の原則
今回はSingle Responsibility Principle(単一責任の原則 / SRP)についてです。
なぜSOLID原則が必要なのか?
初回なので、なぜソフトウェアエンジニアはSOLID原則について知っていなければいけないのか?という説明をしておきます。
最近話題のクソコードという言葉があります。スパゲティコードとかウンコードとかヒヨコードとか、別の言葉もありますが、総じて読みづらくメンテナンスしづらいコードを指します。
スパゲティの画像はすごく美味しそうですが、何かしらのコードをメンテナンスしたことのあるソフトウェアエンジニアは、スパゲティコードとか、秘伝のタレというものをとても嫌います。
これは、過去の自分や他人が書いた、一見して意図の分かりづらいコードと格闘した経験のせいです。メンテナンスしづらいコードは修正だけではなく、機能拡張をとてもしづらいものにします。技術的負債などとも呼びますが、ここではこれ以上詳しくは解説しません。
こういったメンテナンスしづらいコードに対して「クソコード」みたいな言葉で呼ぶ人が現れるのは、そういった過去の恨み辛みが怨念となって開発現場に積み重なっているからなのです。
「クソコード」という呼び方はさておき、メンテナンスしづらいコードにはいくつか類型的な特徴があります。SOLID原則は、そういったダメなパターンにハマらない為に生み出された人類の知恵です。
・ SOLID原則はメンテナンスしづらいコードを生み出さないための人類の知恵
クソコードを生み出さないように、悲劇を繰り返さない為にも、SOLID原則を覚え、マスターしましょう。
単一責任の原則(SRP)
単一責任の原則を堅い言葉でいうと、あるモジュールやクラスや関数などを改修する理由はたった1つになるようにしましょうというものです。
実際には、単一責任という言葉では少し足りなくてある1つのアクターに対しての単一責任と言うべきですし、説明についてもあるモジュールやクラスや関数などを、ある1つのアクターに対する責任だけで済むように適切に分割しましょうと言い換える方がわかりやすいでしょう。
アクターとは大抵の場合は、人間を指します。たとえばウェブサービスにアクセスするユーザーや、そのサービスを運用するためのオペレータや、オペレータよりも上位の権限を持つ管理者などです。アクターには自動処理、概念、機械などといった人間以外も含まれます。
大雑把にいうと、あるクラスがユーザーとオペレータと管理者向けの機能を全部持っていると複雑になりすぎるからやっちゃダメだから、単一のアクターに対して機能提供できるように分割しましょうということです。
たとえば、あるクラスが、ユーザー・オペレータ・管理者向けの機能を持っていたとして、ユーザー向けにコードを改修したいとします。
コードを改修するときには影響範囲の調査は必須ですし、改修したらテストも必須です。今回の事例だと、そのクラスはオペレータや管理者向けの機能も持っているため、オペレータや管理者にも影響が無いかどうか?という調査やテストが必須ということです。
これを怠ると、改修したら別のバグを生み出した!というよくある現象に遭遇することになり、貴方は上司に怒られることでしょう。もしかしたらサービスを止めて胃が痛くなるような障害対策をするハメになるかもしれません。
これを避けるためにはあるクラスから、ユーザー向けの機能、オペレータ向けの機能、管理者向けの機能を分離すると良いでしょう。
分離がどうしてもうまくいかない場合は、様々なテクニックを駆使して、改修の影響が単一アクターにのみ限定されるようにすることは可能です。SOLID原則のDにあたるDependency Inversion Principle(依存関係逆転の原則)といったものです。それは4つあとの記事で解説いたします。
ユーザー・オペレータ・管理者にそれぞれ共通のコードももちろん存在するはずです。この場合は当然改修をする時には、ユーザー・オペレータ・管理者に影響が無いか調査・テストが必須ですが仕方ありません。
できることは、共通のコードと、ユーザー向けののみのコードを分離して、胃の痛くなるような共通のコードを最小限にすることです。
・ 単一責任の原則は、クラスや関数などといったものは単一のアクターにのみ機能を提供するように、適切に分割すべき
・ 複数のアクターにまたがるコードは最小限にして、単一のアクターにのみ提供するコードと分離すべき
コンウェイの法則
メルヴィン・コンウェイという人が提唱した法則があります。
システムを設計する組織は、その構造をそっくりまねた構造の設計を生み出してしまう
単一責任の原則が重要になってくる理由は、このコンウェイの法則にもあります。
縦割り型の組織は、縦割り型のシステムを生み出します。当然でしょう。縦割りで横の連携がとれないのに、組織の壁を越えたシステムを生み出すのは困難です。
たとえば縦割り型の組織で、動画配信サイトの組織と、ストリーミング配信の組織が分かれていたらどうでしょうか?
このとき、動画配信に必要な機能とストリーミング配信に必要な機能が、同じクラスに実装されていると、とても大変なことになります。そのクラスの改修には両方の組織で判子承認リレーが度々生じることでしょう。
それならそのクラスは、動画配信に必要な機能だけ、ストリーミング配信に必要な機能だけを持つべきです。少なくともあるクラスを改修する時に2つの組織の事情を調整する必要はありません。
これが単一責任の原則が重要になってくる理由です。
コードベースが異なっていれば、単一責任の原則の範囲では、開発速度や開発難度・バグの生じやすさに問題は生じません。
それらのその組織がリリースするアプリは、動画配信アプリとストリーミング配信アプリが異なるものになっていて、AppleやGoogleのアプリ承認がそれぞれ発生するという問題はあるかもしれませんがそれは別の話です。
組織構造は往々にして、このようにアプリやサービスの境界面となるのです。
※ただの例であり、実在の組織やサービスとは関係ありません。
SRPは大体コンウェイの法則から、成り立っていると思ってかまいません。
名前と提供する機能
SRPと少し離れますが、隣接する話題です。
※ここでの話題は必ずしもSRPと直接の関係はないことにご注意(関係がないとも限らない)
関数やメソッドは、ある1つの機能のみを提供すべきです。複数の機能を提供すれば、それだけ複雑になってしまい、バグを生み出す温床となります。これは循環的複雑度といった数値で計測可能なものです。
calcTaxという関数があった時、おそらくそれは税金を計算する機能だけを持つべきです。この関数がレシートに税金を印字するような機能をもっていれば、少なくとも名前に反します。
では、calcAndPrintTaxという名前に変更しましょうか?人によっては、税金を計算し印刷する関数を作ることに問題を感じないかも知れませんが、印刷処理には、エラー処理がつきものです。紙が無ければ給紙するように促す必要があります。
この場合、PrintTaxには、さらにEncourageTheOperatorToFeedPaperWhenPaperErrorなどの機能があるということです。
オペレータに紙を給紙してもらう為にはブザーを鳴らすなり画面に表示するなりする必要があるでしょう。ReportToDisplayみたいな名前もさらに盛り込まれることになるでしょう。
PrintTaxという名前の関数にはそういったエラーに応じた様々な対策も含まれているべきだ!という主張の人もいるかもしれませんが、その場合、「税金を印字する」という名前には「紙が無くなったら給紙するようにオペレータに通知するために画面に表示する」という隠れた機能が含まれているということになります。
名前は大切なものです。関数・メソッドに適切な名前がつけられていれば、名前を見るだけでおおよそを把握できるからです。ところが先ほどのようにオペレータに通知する機能まで全部をひっくりめておくべきだという人が作ったPrintTax関数は、名前だけを見ても全貌を把握できません。
「だからこそドキュメントを書く必要があるのだ」と言う人もいるでしょう。ただし、名前すらまともにつけられない人がドキュメントをまともに書けますか?そのドキュメントは何かしらの機能や制限が記載されず抜け落ちた、不完全なドキュメントになったりしませんか?
さらにいうと、エラーは紙が無くなるだけではありません。画面に表示する時にもエラーがある可能性はありますし、紙詰まりや、印刷するためのレシートプリンタとの通信に失敗するかもしれません。
もちろんここで考え方を変えて「エラーが生じたら例外を発生させるだけでいいのでは?」という人もいるでしょう。これなら、calcTaxAndPrintTaxは、PrintTaxとしての機能を、レシートへの印字をレシートプリンタに指示して、エラーが発生すれば例外が投げられるという、これまでよりは遙かにシンプルなものになります。
さて、これで万事解決でしょうか?
貴方はレジの開発をしていてcalcTaxAndPrintTaxという名前で、税金の計算とレシートプリンタに印字する指示を出し、エラー時には例外を投げるという関数を書いていました。
ある日「レシートを出さずに、EReceipt形式の電子データを生成する機能をつけてよ」と言われました。
calcTaxAndPrintTaxに、電子データを出す為のフラグをつけて、calcTaxAndPrintTaxOrGenerateTaxToEReceiptという名前の関数に改修するのでしょうか?それとも、calcTaxAndOutputTaxという名前の関数に改修して、レシートプリンタもERecepit形式の電子データも抽象化したOutputTaxという機能にしますか?
後者はいいアイデアかもしれませんが、そもそもcalcTaxとoutputTaxを同じ関数で実行すべき理由はあるのでしょうか?
税金の計算は単一で完結できるようなものです。ユニットテストも簡単に作れます。抽象化するにしても、outputTaxという妙に局所的なものを作るよりはoutputData(税金以外も含めて出力する)方が使い勝手はいいのではないでしょうか?
・ 名前と中身が合ってなければメンテナンスしづらい
・ 関数・メソッドなどは単一の機能を提供するのが望ましい
単一責任の原則にしても、関数やメソッドの書き方にしても、うまく分離した方が、影響範囲が小さくなりテストがしやすくなります。そういったものは、ユニットテスト化しやすいため、人力テストの苦労をある程度減らせられます。
まとめ
SOLID原則は、メンテナンスしづらいコード、技術的負債を生み出さないための人類の知恵です。なぜなら、メンテナンスしづらいコードには類型的特徴があるからです。
SOLID原則のうち、SはSRP(単一責任の原則)です。これはあるクラス・モジュール・関数といったものは、単一のアクター(人間だったり機械だったりバッチ処理だったり)に対してのみ責任を負うように分割をすべきいう原則です。
そもそも関数やメソッドといった単位のものは、単一の機能のみを提供し、適切な名前がつけられるべきです。
機能が増えれば増えるほどバグの生じる可能性が増え、改修が面倒になり、適切な名前をつけづらくなり、適切なドキュメントを残しづらくなるからです。
次回は、SOLIDのOであるOpen-Closed Principle (オープン・クローズドの原則)について説明します。