Skip to content

Instantly share code, notes, and snippets.

@onionmk2
Forked from matarillo/1_srp.md
Created February 20, 2025 02:26
Show Gist options
  • Select an option

  • Save onionmk2/b6b5ed19c51a2360e8841498a4ba2002 to your computer and use it in GitHub Desktop.

Select an option

Save onionmk2/b6b5ed19c51a2360e8841498a4ba2002 to your computer and use it in GitHub Desktop.
SOLID原則はソリッドではない

SOLIDはソリッドではない - 単一責任原則を検証する

https://naildrivin5.com/blog/2019/11/11/solid-is-not-solid-rexamining-the-single-responsibility-principle.html

2019年11月11日

最近、SOLIDの原則について考えていて、その有用性に疑問を感じている。 SOLIDの原則は曖昧で、範囲が広すぎて、混乱を招き、場合によっては完全に間違っている。しかし、これらの原則の動機は正しい。問題は、ニュアンスの異なる概念を簡潔な文に落とし込もうとすることにあって、翻訳の過程で価値の大部分を失っているのだ。 これはプログラマーを間違った道へと導いてしまう(私にとっては確かにそうだった)。

おさらいとして、SOLIDの原則は以下の通りである:

  • 単一責任原則
  • オープン/クローズの原則
  • リスコフの置換原理
  • インターフェース分離の原則
  • 依存関係の逆転原理

今回は「単一責任原則」を取り上げ、4回にわたって他の原則に取り組む。

単一責任原則

ウィキペディアには 次のように書かれている。

つまり、ソフトウェアの仕様のひとつに対する変更だけが、そのクラスの仕様に影響を与えることができる。

これはかなり曖昧だ。「仕様」とは何だろう? 私はこの23年間、仕様の定まったソフトウェアに携わったことがない。 そして、ここでの「影響」とは何を意味するのか?

ウィキペディアの記事では、「例」のセクションで説明している(強調は原文のまま):

マーティン[この言葉を作ったロバート・マーティン 1 ]は、責任とは変更理由であると定義している。

というのも、 すべて のコードには、バグを修正するか機能を追加するかという、少なくとも 2つ の変更理由があるからだ。では、それらが別の理由と見なされないのであれば、「理由」とは何なのか?

そこが曖昧なので、コードレビューに単一責任原則を適用するとだいたい泥沼化する。というのも、誰もがレビュー中のコードの質ではなく、原則をどう解釈するかについて話し始めるからだ。

とはいえ、コードが持つべき仕事/事柄/責任は1つだけというのは正しい 気がする 。 このRailsコントローラを考えてみよう:

class WidgetsController < ApplicationController
  def create
    @widget = Widget.create(widget_params)
    if @widget.valid?
      redirect_to :index
    else
      render :new
    end
  end

  def widget_params
    params.require(:widget).permit(:name, :price)
  end
end

これは非常にバニラな実装で、新しいウィジェットが有効であればデータベースに保存し、有効でなければ、バリデーションの問題を修正するためにユーザーをフォームに送り返す。

「バグフィックスと新機能」という変更理由はさておき、このクラスには変更する理由がたくさんありそうだ。 ウィジェットを要求するために必要なパラメータを追加するかもしれない。 ウィジェットが作成されたときに、ユーザを別の場所にルーティングする必要があると判断するかもしれない。 ウィジェットが作成されるたびに、管理者にメールを送信する必要があるかもしれない。つまり、このコードが単一責任原則に違反していることは明らかであり、したがって悪いことであり、変更されるべきなのだ。そうだろう?

ここでそれを受け入れるのは難しい。 このコードは、Railsが推奨するコードの書き方の規範になっているだけでなく、短く、直接的で、要点がまとまっている。 もちろん、時間が経てばこのコントローラにさらにコードを追加することもできるし、コントローラが大きく複雑になることもあるだろう。しかし、このコードの変更理由が 正確に1つ であるべきだとか、修正が必要だと言うのだろうか?それは意味がない。

科学のために、このコードを変更して責任の数を減らしてみよう。

class WidgetsController < ApplicationController
  def create
    @widget = WidgetCreator.create(params)
    WidgetRouter.route(self, @widget)
  end
end

class WidgetCreator
  def self.create(params)
    Widget.create(params.require(:widget).permit(:name, :price)
  end
end

class WidgetRouter
  def self.route(controller, widget)
    if widget.valid?
      controller.redirect_to :index
    else
      controller.render :new
    end
  end
end

各クラスの責任は確かに軽くなり、変わる理由も少なくなった。しかし、これを改善と見るのは難しい。確かに、ウィジェットの作成方法が複雑になれば、別のクラスを持つことに価値があるかもしれない。また、作成時のルーティングが多くの微妙なルールに左右されるのであれば、それを抽出することに価値があるかもしれないが、今回はそうではない。決してこのコードが優れているわけではない。

このことが私に教えてくれるのは、単一責任原則はそのままでは役に立たず、盲目的に固執すれば、解決しようとしている以上の問題を引き起こすかもしれないということだ。

とはいえ、単一責任原則の意図は正しい。それは、モジュールの要素がどの程度まとまっているかという 凝集性 についての方向性を与えようとしているのである。 問題は、結束はそれほど単純明快ではないということだ。

凝集性

凝集性 とは、コンピュータ・サイエンスで長い間議論されてきた概念で、要素(コードの一部)が一緒になっているモジュール(コードのグループ化を意味する)は、要素が一緒になっていないモジュールよりも保守性が高く、理解しやすいというものだ。

凝集性だって単一責任原則と同様に曖昧だが、 原則 としては提示されておらず、遵守しなければならない客観的な尺度としても提示されていない。

強力な規定措置がないということは、責任の数を数えるのをやめて、今あるコードとそれに加えたい変更について話し始めることができるということだ。 元のコントローラに対する2つの変更を見てみよう。これらの変更はどちらも単一責任の原則に違反することになる。しかし、クラスの凝集性に重大な影響を与えるのは1つだけだ。

最初の例では、ウィジェットが作成されるたびにメールを送信するコードを追加する。

class WidgetsController < ApplicationController
  def create
    @widget = Widget.create(widget_params)
    if @widget.valid?
      WidgetMailer.widget_created(@widget) # <------
      redirect_to :index
    else
      render :new
    end
  end

  def widget_params
    params.require(:widget).permit(:name, :price)
  end
end

ウィジェットの作成とそれに関するEメールの送信は、一緒にあるべきもののように思えるので、この変更はこのクラスのまとまりに実質的な影響を与えないと主張したい2.

ウィジェットを保持するテーブルのデータベース統計を記録する、別の変更を見てみよう:

class WidgetsController < ApplicationController
  def create
    @widget = Widget.create(widget_params)
    if @widget.valid?
      DatabaseStatistics.object_created(:widget) # <-----
      redirect_to :index
    else
      render :new
    end
  end

  def widget_params
    params.require(:widget).permit(:name, :price)
  end
end

コントローラーはデータベースとは何の関係もない。だから、この変更は、私たちがこの変更に疑問を持つのに十分なほど、クラスのまとまりを弱めるように感じる。

しかし、どちらの場合も単一責任原則に違反している。 このことは、凝集性の概念を単一責任原則に当てはめることが絶対に間違っていることを物語っている。

私からのアドバイスだ: 単一責任について話すのをやめて、凝集性の話を始めよう。

次回は「オープン/クローズの原則」を取り上げる。この原則は、全く役に立たないほど混乱している。

Footnotes

  1. ロバート・マーティン、別名 "アンクル・ボブ "は、私の個人的価値観と矛盾する発言をオンライン上で行っている。 とはいえ、彼はソフトウェアとオブジェクト指向設計の世界で影響力を持っており、彼のアイデアは多くの開発者によって教えられているため、彼の考えを批判することには価値があるのだ。アンクル・ボブのオンライン上での行動についてもっと知りたいのであれば、Twitterで彼を見つけるのがよいだろう。

  2. また、この変更がリファクタリング後のバージョンでどのように物事を複雑にしていたかにも注目すべきだ。このコードを WidgetRouter に追加する必要があり、それは非常に間違っていると感じられるはずであり、したがって、この1行のコードを追加するためには、より大規模なリファクタリングが必要になるのだ。

オープン/クローズドの原則は混乱させるし、まあ、間違っている(SOLIDはソリッドではない)

https://naildrivin5.com/blog/2019/11/14/open-closed-principle-is-confusing-and-well-wrong.html

2019年11月14日

SOLID 原則は、当初考えられていたほど「solid(堅牢)」ではないことに気づきつつあるのである。前回の投稿 では、単一責任原則の問題点を概説したが、今回は5つの原則の中で最も理解しづらいオープン/クローズドの原則について述べたいと思う。

この原則は、ソフトウェアは「拡張に対してオープンであり、修正に対してクローズドであるべき」というものである。この要約は非常にわかりづらく、深く掘り下げてみると悪いアドバイスばかりであった。この原則は完全に無視すべきだ。その理由を見ていこう。

オープン/クローズドの原則の意味するところ

この原則は(SOLIDにおける理解では)、ロバート・マーチン 1 がバートランド・メイヤーの著書『 Object-Oriented Software Construction 』での記述を基に書いた論文で提唱されたものである。

マーチンはメイヤーの言葉を次のようにパラフレーズしている。

ソフトウェアの構成要素(クラス、モジュール、関数など)は、拡張に対してオープンであり、修正に対してクローズドであるべきだ。

そして、「拡張に対してオープン」とは、アプリケーションの要件が変更されたり、新しいアプリケーションのニーズに対応したりするために、モジュールを「新しく異なる方法で動作させることができる」ことを意味すると定義している。一方、「修正に対してクローズド」とは、「誰もそのソースコードを変更することを許されない」ことを意味すると定義している。

はあ?これは全く逆のように思えるのだが。

コードに不必要な柔軟性を追加すること(拡張に対してオープンにすること)は、複雑さとキャリングコストを生み出す。究極的な柔軟性を実現するために、存在しないありとあらゆるユースケースを想像する必要がある。これは時間の無駄であり、より複雑でわかりにくいコードを生み出し、必要のないすべての柔軟性を永続的にメンテナンスすることを要求するのである。

ソースコードを変更できないという考え方ほど奇妙ではないが、バグを修正するためにコードを変更できないとしたら、どうすればよいのだろうか。削除して最初からやり直すのか。この原則のこの部分は明らかに間違っているように思えて、自分の現実認識を疑ってしまう。

この論文を掘り下げてみると、クラスは抽象基底クラスに依存すべきであり、それによって特定のクラスの実装を、そのクラスの利用者に影響を与えることなく入れ替えることができるようにすべきだと述べているようである。そして、これが一つの 原則 であるため、私の解釈では、常にこのようにすべきだということになる。

これは悪いアドバイスである。柔軟性はほとんど必要とされず、ほとんどの場合、解決するよりも多くの問題を生み出す。また、システムの振る舞いを理解するのが難しくなることもあり得る。

柔軟性はコストがかかる

他の条件が同じであれば、より柔軟性の高いコードは、構築、テスト、メンテナンスがより難しくなる。必要のない機能をコードに組み込むことは余分な作業だ。キャリングコストの概念だけでも、クラスを「拡張に対してオープン」にするために必要な作業は避けるべきだ。柔軟性が必要ないのであれば、構築してはいけない。

そのキャリングコストの一つが、システムの振る舞いを理解する能力だ。高い柔軟性を持つコードは、ナビゲートすべきコードパスを多く生成し、この論文で示されているような柔軟性(抽象基底クラスを追加すること)は、それらのコードパスを発見するのを難しくする。

何について話しているのかを見てみよう。この論文では、Serverに依存するClientの例が示されている。

A client class depends on a concrete server class directly

論文の ClientServer の関係の再現(新しいウィンドウでより大きなバージョンを表示

そのコードはJavaでは次のようになる(Rubyには型アノテーションがないため、これを見るのは難しい)。

public class Client {
  private Server server;

  public Client() {
    this.server = new Server();
  }

  public void saveSomeData(String data) {
    this.server.post("/foo", data);
  }
}

public class Server {
  public void post(String url, String data) {
    // ....
  }
}

オープン/クローズドの原則によると、このクラスは、常に具象のServerインスタンスを使用するため、拡張に対してオープンではなく、また、別のタイプのサーバーに変更したい場合は、ソースコードを変更しなければならないため、修正に対してクローズドでもない。

これらの変更を行う 必要がある というのは、既定路線の結論ではない。また、実際に柔軟性が必要な場合、このクラスがその柔軟性を追加しなければならない場所であるかどうかも明らかではない。

それでも、この論文(つまり原則)では、この問題に対処する方法をこのように述べている。すなわち、サーバーの実装のための抽象基底クラスを導入し、Clientにはその基底クラスに依存させるべきだと。

A client class depends on an abstract server class with a concrete implementation of that abstract class

論文のClientAbstractServerServerの関係の再現(新しいウィンドウでより大きなバージョンを表示

Javaでは、次のようになる。

public abstract class AbstractServer {
  abstract void post(String url, String data);
}

public class Server extends AbstractServer {
  public void post(String url, String data) {
    // ....
  }
}

public class Client {
  public Client(AbstractServer server) {
    this.server = server;
  }
}

そして、Clientインスタンスを作成するときは、常に具象の実装を渡す。

Client client = new Client(new Server())

Clientは、AbstractServerの別の実装を渡すことができるため、拡張に対してオープンになり、そのためにソースを変更する必要がないため、修正に対してクローズドになった。

これはより柔軟な設計だが、果たしてより良いものだろうか。私は、これが 絶対的に より良いものだとは思えない。複数のServerが必要ない場合、コードに不要な機能を追加してしまい、それを維持しなければならなくなる。この例では些細なことに思えるかもしれないが、このようにして構築された全体のコードベースを想像してみてほしい。私はそのようなコードベースで作業したことがあるが、楽しいものではなかった。そのためのコードを書くには、必要のない抽象基底クラス(またはインターフェース)を作るという余分な手順が必要だった。

しかし、そのことはシステムの振る舞いを説明し、予測することを 本当に 難しくしてしまった。

システムの振る舞いを理解することが最も重要

プログラマーとして、システムの実際の振る舞いを 頻繁に 説明し、理解しなければならない。バグを診断して修正したり、システムで何が起こったのかを他の人に説明したり、機能を追加するために変更を加えたりしなければならないのだ。

オープン/クローズドの原則に違反している最初の実装では、柔軟性がないため、システムの振る舞いを説明するのは非常に簡単だ。Clientは常にServerを使用するため、コードを通るパスは明確である。

しかし、2番目の実装では、より難しくなる。ServerAbstractServerの唯一の実装かどうかわからないと仮定すると、システムの観測された振る舞いを理解するためには、Clientのすべての使用箇所を追跡して、どのAbstractServerの実装が使用されたかを把握し、どのパスがどれを使用したかを把握しなければならない。

AbstractServer の実装が1つしかないことを発見するためにそれを行うことを想像してみてほしい。

システムが必要とする以上に柔軟になるようにクラスを設計すると、複雑さが生まれる。プログラマーが必要になるかもしれないと考えている柔軟性を追加する場合、今の時点で柔軟性を追加することで後で時間を節約しようというアイデアがある。しかし、どのような柔軟性が必要なのかは、常にはわからないのだ。システムを 本当に 柔軟にするためには、必要なことだけを実装し、十分にテストされていなければならない。

私のアドバイスは次のとおりだ。オープン/クローズドの原則は完全に無視すること。目の前の問題を解決するためのコードを書くこと。

残りのSOLID原則を見ていくと、必要のない場合にも柔軟性を追加するという繰り返しのテーマが見えてくるだろう。それは一体何のためなのか...よくわからない。

次はリスコフの置換原則だ。

Footnotes

  1. ロバート・マーチン(通称「アンクル・ボブ」)は、私の個人的な価値観と一致しないようなオンライン上の発言をしているので、私は彼の仕事を熱心にフォローしておらず、彼を高く評価してもいない。それでも、彼はソフトウェアやオブジェクト指向設計の世界で影響力を持っており、多くの開発者に教えられている彼のアイデアを批判することには価値がある。アンクル・ボブのオンライン上の行動について詳しく知りたい方は、Twitterで彼を探してみてほしい。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment