⛳

エンジニア歴1年半経って少し実装になれたので、SOLID原則を理解してみる

に公開
2025/12/25

はじめに

みなさん、おはこんばんにちは!メリークリスマスです!!

Social Databank Advent Calendar 2025 の25日目です。

エンジニアとして社会に解き放たれてから、約一年半くらい経ったので、ちょっと前々から興味があったSOLID原則について、つらつらと書いていこうと思います。

なお、本記事はまだまだ駆け出しの身分が書いている記事ゆえ、色々なところに記載ミスや認識違いなどがあるかと思います。
そのような場合は、あたたかくコメントにてご指摘いただければ幸いです。

また、この記事の執筆にあたって下記2冊を大変参考にしております。とても面白い書籍なので是非読んだことない方は、読んでみてほしいなと思っております!!

それ以外にもさまざまな記事を参考にしております。参考にした記事は全て参考文献に記載していますので、そちらも是非ご覧ください!!

SOLID原則とは

僕的な解釈としては

SOLIDとは、5つの原則の頭文字をとったもので、ソフトウェア設計の際に保守しやすいような設計のルールのこと。どこにどのソースコードを置くのか、その指標となるもの

という解釈でいます。

  • Single Responsibility:単一責任の原則
  • Open-Closed:オープン・クローズドの原則
  • Liskov Substitution:リスコフの置換原則
  • Interface Segregation:インターフェイス分離の原則
  • Dependency Inversion:依存性逆転の原則

これら5の頭文字をとってSOLIDとなっていて、これら原則を抑えながら設計をやっていこうぜ!ってことなんじゃないかなと思っています。

以下にSOLID原則が紹介されている記事や書籍から一部抜粋して、SOLIDについてつらつら書いていければと思います。

SOLID原則で検索するとヒットするであろう

これらの記事には

If you are familiar with Object-Oriented Programming, then you’ve probably heard about the SOLID principles.

These five software development principles are guidelines to follow when building software so that it is easier to scale and maintain.

オブジェクト指向プログラミングに精通している方なら、SOLID原則について聞いたことがあるでしょう。

この5つのソフトウェア開発原則は、ソフトウェア構築時に従うべきガイドラインで、ソフトウェアの拡張性や保守性を高めるためのものです。

という記載があります。また、書籍 Clean Architecture 達人に学ぶソフトウェアの構造と設計によると

よくできたソフトウェアシステムは、クリーンなコードを書くことから始まる。レンガの出来が悪ければ、その建築は優れたものにはならない。一方、たとえよくできたレンガをつかっても、ぐちゃぐちゃなものを作ってしまうことがあり得る。そこで登場するのがSOLID原則だ。

SOLID原則は、関数やデータ構造をどのようにクラスに組み込むのか、そしてクラスの相互接続をどのようにするのかといったことを教えてくれる。「クラス」という用語を使ったからといって、単にいくつかの機能やデータをとりまとめたものを指しているにすぎない。「クラス」と呼ぶかどうかは別として、どのようなソフトウェアにもそのような仕組みはあるはずだ。SOLID原則は、そうした仕組みに適用するものである。

という記載がされています。個人的には

一方、たとえよくできたレンガをつかっても、ぐちゃぐちゃなものを作ってしまうことがあり得る。そこで登場するのがSOLID原則だ。

ここの例がとても良いと思っていて、これは僕の脳内だと

つよつよエンジニアが100人いても、ぐちゃぐちゃなものを作ってしまうことがあり得る。そこでなるべくぐちゃぐちゃにならないようにするためにSOLID原則を参考にしていこうぜ!

という感じに解釈できるのではないかと思います。(もちろん強いエンジニアのする設計は本当にすごいと思います...もっと精進します...)

なぜSOLIDに従うの?

ここも個人の解釈からですが、僕としては

運用コストを下げるため

という一言に尽きるのではないかと思っています。もう少し噛み砕くと

楽に運用したい!

というふうになるのかなと思います。こちらも書籍 Clean Architecture 達人に学ぶソフトウェアの構造と設計からの抜粋ですが、

SOLID原則の目的は、以下のような性質を持つ中間レベルのソフトウェア構造を作ることだ。

  • 変更に強いこと
  • 理解しやすいこと
  • コンポーネントの基礎として、多くのソフトウェアシステムで利用できること

という記載があります。実際にプログラマーはソースコードを読む時間の方が多く、また人のコードを読む時間の方が多いのではないでしょうか?

このような時に、まずは理解しやすいと実装意図や背景など汲み取れると思うし、さらに変更しやすいということは、ソースコードの修正が容易であり、とても負担は減ると思います。

何か不具合や考慮漏れなどがあった場合に、認知コストが低く修正がしやすいと、修正者の負担軽減と同時に影響範囲の特定が容易なため、修正がしやすくなる。これが結果として運用コストが下がるのではないかと思っています。

中間レベルとは何か

ここは個人で調べた範囲なので、あまり参考にならないかもですが、設計にレベルについては、

  • 低レベル
  • 中間レベル
  • 高レベル

に分けられるそうです。

低レベル

ここの関数や変数、条件分岐などソースコードの書き方などのことだそうです。if文の書き方や変数の命名規則など

中間レベル

SOLIDが対象としているレベルはここらしいです!クラスやモジュール、パッケージなどの設計などのことを指しているそうです。

もうちょい詳細を書くと、クラス同士の関係や責務の分担などがこのレベルに当てはまるそうです。

例としては、Serviceクラスの責務範囲、インターフェスの定義、依存関係の方向など

高レベル

ここはシステム全体のアーキテクチャのことを示しているそうです。

システム全体のアーキテクチャや、コンポーネント間の構造など。例としてはクリーンアーキテクチャなどなど...

また、先ほどの引用を再掲しますが、

一方、たとえよくできたレンガをつかっても、ぐちゃぐちゃなものを作ってしまうことがあり得る。そこで登場するのがSOLID原則だ。

この状況をなるべく回避したいということも、SOLID原則に従う一つの理由だと思っています。

SOLIDについて実装例をもとに理解してみる

というわけで、ここからはSOLID原則について実装例を出しながら、具体的な内容を理解していきたいと思います。

SOLID原則は以下の5つの原則の頭文字となっており、これを1つずつみていこうと思います。

  • Single Responsibility:単一責任の原則
  • Open-Closed:オープン・クローズドの原則
  • Liskov Substitution:リスコフの置換原則
  • Interface Segregation:インターフェイス分離の原則
  • Dependency Inversion:依存性逆転の原則

Single Responsibility:単一責任の原則

単一責任の原則は、僕は

1つのクラスは1つの役割(責務とも言う?)のみ持つ

と解釈をしています。書籍 アジャイルソフトウェア開発の奥義 第2版には

クラスを変更する理由は1つ以上存在してはならない

となっており、また、Clean Architecture 達人に学ぶソフトウェアの構造と設計では

モジュールはたった一つのアクターに対して責務を負うべきである

と言うふうに記載されています。僕の場合は読んですぐに理解できなかったため、今回は下記図と実装をもとに理解していきます。

違反例

SRP違反1

Rectangle.phpの実装
Rectangle.php
<?php

declare(strict_types=1);

namespace Packages\Rectangle\Domain;

class Rectangle
{
    // プロパティなどは省略
    // 〜〜〜〜〜
    public function draw(): void
    {
        // Draw rectangle (implementation omitted)
    }

    public function area(): float
    {
        // Calculate area (implementation omitted)
        return 0.0;
    }
}

というクラスがあったとしてそれぞれ

  • draw : スクリーンに四角形を描画する
  • area : 四角形の面積を計算する

という構成になっていたとします。GraphicaApplicationはスクリーンに描画するために、Rectangleクラスのdrawメソッドを利用し、ComputationalGeometryApplicationは図形の計算処理のためにRectangleクラスのareaメソッドを利用していたとします。この場合Rectangleクラスには

  • 図形の描画をする役割
  • 面積を計算する役割

の二つの役割を担っています。これはSRPの

1つのクラスは1つの役割(責務とも言う?)のみ持つ

に違反することになると思われます。また、ここでは2つのアクターを抱えていることになります。つまり、

  • グラフィックチームが「描画方法を変更したい」と要求した時
  • 計算チームが「面積計算のロジックを変更したい」と要求した時

両方の変更理由でこのクラスが変更される可能性があり、これがSRP違反となります。

では、これに対する対策を考えていきたいと思います。

対策

下記ソースコードで、SRP違反を回避することができるかと思います。

SRP違反の修正

  • Rectangle.phpは、描画の役割のみを持つ
  • GeometricRectangle.phpは、面積計算の役割を持つ

このようにすることで、これら2つのクラスはそれぞれ1アクターと、1つの役割のみを持っていることになるため、SRP遵守できている状態なのかなと思います。

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

オープン・クローズドの原則は僕は

ソフトウェアに新しい機能追加があった場合に、

  • 新しいコードを追加すれば機能を追加できる状態である
  • また、追加した際に既存ソースコードは修正されない・影響を受けない

これが良いよね!

と言う理解でいます。

違反例

ClientクラスとServerクラスという二つのクラスについて、下記依存関係があるとします。

OC違反

Client.phpの実装サンプル
Client.php
<?php

declare(strict_types=1);

namespace Packages\Oc\Before;

class Client
{
    public function execute(): void
    {
        $server = new Server();

        $server->execute();
    }
}
Server.phpの実装サンプル
Server.php
<?php

declare(strict_types=1);

namespace Packages\Oc\Before;

class Server
{
    public function execute(): void
    {
        // Server execution logic (implementation omitted)
    }
}

今この段階ではServerへの依存ですが、これが何かしらのタイミングでServer2を利用しないといけないことになったとします。そうすると、Clientクラスは下記修正が入ると思います。

Client.php
class Client
{
    public function execute(): void
    {
-         $server = new Server();
+         $server = new Server2();

        $server->execute();
    }
}

これは変更(Server1からServer2への変更)に対して既存コードの修正が走っているためOC違反していると思われます。

対策

対策として鍵になるのは、抽象(Interface)だと思います。

OC遵守

こうすると、Client.phpは下記ソースコードになるかと思います。

Client.php
<?php

declare(strict_types=1);

namespace Packages\Oc\After;

class Client
{
    public function __construct(
        private ClientInterface $server
    ) {
    }

    public function execute(): void
    {
        $this->server->execute();
    }
}

その他の実装も一緒に載せておきます。

Server.phpの実装
Server.php
<?php

declare(strict_types=1);

namespace Packages\Oc\After;

class Server implements ClientInterface
{
    public function execute(): void
    {
        // Server execution logic (implementation omitted)
    }
}
ClientInterface.phpの実装
Server.php
<?php

declare(strict_types=1);

namespace Packages\Oc\After;

class Server implements ClientInterface
{
    public function execute(): void
    {
        // Server execution logic (implementation omitted)
    }
}

こうすることで、Client.phpはInterfaceにのみ依存するので、例えば別のServerになっても、Client.phpはソースコードを変更する必要はありません。

Liskov Substitution:リスコフの置換原則

僕としては

「親クラスとして使っているコードに、子クラスを差し込んでも挙動が壊れてはいけない」

という認識でいます。

書籍アジャイルソフトウェア開発の奥義 第2版では

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

という記載がされています。しかし、書籍Clean Architecture 達人に学ぶソフトウェアの構造と設計では

オブジェクト指向の黎明期には、リスコフの置換原則(LSP)は継承の使い方の指標となるものだと考えられていた。だが、時間をかけてその適用範囲は広がり、今ではインターフェースと実装に関するソフトウェア設計の原則となっている。

というふうに年々変わってきているそうです。

違反例

よくある例ですが四角形クラスと、それを継承した正方形クラスがあるとします。

alt text

これの実装を見ていきます。

Rectangle.php
<?php

declare(strict_types=1);

namespace Packages\Lsp\Before;

class Rectangle
{
    protected int $width;

    protected int $height;

    public function setWidth(int $w): void
    {
        $this->width = $w;
    }

    public function setHeight(int $h): void
    {
        $this->height = $h;
    }

    public function area(): int
    {
        return $this->width * $this->height;
    }
}

続いてこれを継承した正方形クラスを実装します。

Square.php
<?php

declare(strict_types=1);

namespace Packages\Lsp\Before;

class Square extends Rectangle
{
    public function setWidth(int $w): void
    {
        $this->width = $w;
        $this->height = $w;
    }

    public function setHeight(int $h): void
    {
        $this->width = $h;
        $this->height = $h;
    }
}

そして、面積を計算する処理があったとします。

<?php

function printArea(Rectangle $r): void
{
    $r->setWidth(5);
    $r->setHeight(10);
    echo $r->area() . PHP_EOL;
}

これを実行すると、異なる結果が出てきます。

printArea(new Rectangle()); // 50
printArea(new Square());    // 100(期待外れ)

これはSquareはRectangleとして扱われたときの契約(幅と高さが独立に変えられる)を破っていることになるかと思います。つまり

  • Square は 「Rectangleの振る舞い」を満たしていない
  • 親クラスとして期待されている前提が崩れる

となるため、LPS違反となります。

対策

これに対する対策は、振る舞いを共通定義して、それを実装する形にすれば回避できるかと思います。

  • Shapeの契約はarea(): intを提供するだけ
  • RectangleもSquareも、その契約を満たす

となるため、LSP違反が回避できるかと思います。

LSP遵守

では実装を見ていきます。

まずは、Interfaceの定義をしていきます。

Shape.php
<?php

declare(strict_types=1);

namespace Packages\Lsp\After;

interface Shape
{
    public function area(): int;
}

続いて、それぞれの形でこのインターフェースを定義します。

Rectangle.php
<?php

declare(strict_types=1);

namespace Packages\Lsp\After;

class Rectangle implements Shape
{
    public function __construct(
        private int $width,
        private int $height
    ) {
    }

    public function area(): int
    {
        return $this->width * $this->height;
    }
}
Square.php
<?php

declare(strict_types=1);

namespace Packages\Lsp\After;

class Square implements Shape
{
    public function __construct(private int $side)
    {
    }

    public function area(): int
    {
        return $this->side * $this->side;
    }
}

こうすることで、形をかえたとしても、正しさは崩れないと思います。

Interface Segregation:インターフェイス分離の原則

インターフェース分離の原則は、僕としては

各インターフェースには、必要最低限のメソッドのみを定義して、利用しないメソッドは定義しない(もしくは分ける)ようにするのがいいよね

という認識でいます。

アジャイルソフトウェア開発の奥義 第2版では

クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない。

というふうに記載されています。

違反例

これはインターネットに乗り物の例がたくさん合ったので、それを元に例を紹介していきます。

  • 飛行機
  • 車

という乗り物があって、これらは乗り物インターフェースを実装していたとしましょう。

ISP違反

VehicleInterface.php
<?php

declare(strict_types=1);

namespace Packages\Isp\Before;

interface VehicleInterface
{
    public function 進む(): void;

    public function 止まる(): void;

    public function 曲がる(): void;

    public function 飛ぶ(): void;
}
AirPlane.phpの実装
AirPlane.php
<?php

declare(strict_types=1);

namespace Packages\Isp\Before;

class AirPlane implements VehicleInterface
{
    public function 進む(): void
    {
        // Implementation for moving forward
    }

    public function 止まる(): void
    {
        // Implementation for stopping
    }

    public function 曲がる(): void
    {
        // Implementation for turning
    }

    public function 飛ぶ(): void
    {
        // Implementation for flying
    }
}

ここまでは問題ないと思いますが、ここで仮にCarというクラス追加しようとすると、

Car.php
<?php

declare(strict_types=1);

namespace Packages\Isp\Before;

class Car implements VehicleInterface
{
    public function 進む(): void
    {
        // Implementation for moving forward
    }

    public function 止まる(): void
    {
        // Implementation for stopping
    }

    public function 曲がる(): void
    {
        // Implementation for turning
    }

    public function 飛ぶ(): void
    {
        // Implementation for flying
        // 市販されている車は今のところ飛べません(大阪万博で車飛んでましたが...
    }
}

車は飛べないはずですが、現状飛ぶメソッドを強制している形になると思います。

対策

やはりインターフェースを分けることだと思います。アジャイルソフトウェア開発の奥義 第2版にも

この原則は、「太った」インターフェースをうまくシェイプアップしてくれる。

というふうに記載がされており、この分割する動作がシェイプアップみたいなものなのかなと思っています。今回は書き分けをしてみています。

alt text

続いて具体的な実装です。まずはインターフェースを分けていきます。

MovbleInterface.php
<?php

declare(strict_types=1);

namespace Packages\Isp\After;

interface MovableInterface
{
    public function 進む(): void;

    public function 止まる(): void;

    public function 曲がる(): void;
}
FlyableInterface.php
<?php

declare(strict_types=1);

namespace Packages\Isp\After;

interface FlyableInterface
{
    public function 飛ぶ(): void;
}

そして、CarクラスとAirPlaneクラスでそれぞれ分けていきます。

Car.php
<?php

declare(strict_types=1);

namespace Packages\Isp\After;

class Car implements MovableInterface
{
    public function 進む(): void
    {
        // Implementation for moving forward
    }

    public function 止まる(): void
    {
        // Implementation for stopping
    }

    public function 曲がる(): void
    {
        // Implementation for turning
    }
}
AirPlane.php
<?php

declare(strict_types=1);

namespace Packages\Isp\After;

class AirPlane implements MovableInterface, FlyableInterface
{
    public function 進む(): void
    {
        // Implementation for moving forward
    }

    public function 止まる(): void
    {
        // Implementation for stopping
    }

    public function 曲がる(): void
    {
        // Implementation for turning
    }

    public function 飛ぶ(): void
    {
        // Implementation for flying
    }
}

このように分けることで、不要なメソッドへの依存強制をしないようにできます。

Dependency Inversion:依存性逆転の原則

DIPは僕としては

上位モジュールも下位モジュールも抽象に依存するようにしようぜ

このような認識でいます。

書籍Clean Architecture 達人に学ぶソフトウェアの構造と設計では

ソースコードの依存関係が(具象ではなく)抽象だけを参照しているもの。それが最も柔軟なシステムである。これが『依存関係逆転の原則(DIP)』の伝えようとしていることである。

と書かれています。

また、書籍アジャイルソフトウェア開発の奥義 第2版では

  1. 上位モジュールは下位モジュールに依存してはならない。どちらのモジュールも『抽象』に依存するべきである。
  2. 『抽象』は実装の詳細に依存してはならない。実装の詳細が『抽象』に依存するべきである。

というふうに紹介されています。

違反例

では、ここからは違反例を紹介していければと思っています。

Laravelでアプリケーションサービスにて、なにかしらのCRUD処理を叩くとします。

DIP違反

実際に実装していきます。

ApplicationService.php
<?php

declare(strict_types=1);

namespace Packages\Dip\Before;

class ApplicationService
{
    public function execute(): void
    {
        $repository = new EloquentRepository();

        $repository->create('some data');
    }
}
EloquentRepository.php
<?php

declare(strict_types=1);

namespace Packages\Dip\Before;

class EloquentRepository
{
    public function create(string $data): void
    {
        // Eloquentを使ったデータ保存処理
    }
}

この場合アプリケーションサービスからEloquentRepositoryに依存する形になると思います。

しかしこれでは、上位モジュールは下位モジュールに依存する形となっています。

  • 上位モジュール:ApplicationService
  • 下位モジュール:EloquentRepository

となり、DIP違反となっています。

対策

これは

上位モジュールは下位モジュールに依存してはならない。どちらのモジュールも『抽象』に依存するべきである。

とある通り、抽象に依存することで、DIP違反を回避することができます。

DIP遵守

実際の実装を見ていきましょう。

RepositoryInterface.php
<?php

declare(strict_types=1);

namespace Packages\Dip\After;

interface RepositoryInterface
{
    public function create(string $data): void;
}

続いてこれを実装したクラスを作成します。

EloquentRepository.php
<?php

declare(strict_types=1);

namespace Packages\Dip\After;

use Packages\Dip\After\RepositoryInterface;

class EloquentRepository implements RepositoryInterface
{
    public function create(string $data): void
    {
        // Eloquentを使ったデータ保存処理
    }
}

最後にアプリケーションサービスはインターフェースに依存するようにします。

ApplicationService.php
<?php

declare(strict_types=1);

namespace Packages\Dip\After;

use Packages\Dip\After\RepositoryInterface;

class ApplicationService
{
    public function __construct(
        private RepositoryInterface $repository
    ) {
    }

    public function execute(): void
    {
        $this->repository->create('some data');
    }
}

こうすることで、

  • 上位モジュール:ApplicationService
  • 下位モジュール:EloquentRepository

これらモジュールはどちらも抽象に依存する形になります。

Laravelでの実際の使い方

上記の実装では、ApplicationServiceのコンストラクタでRepositoryInterfaceを要求していますが、実際にどうやってEloquentRepositoryが注入されるのか?という疑問が出てくるかと思います。

Laravelではサービスコンテナという仕組みがあり、インターフェースと具象クラスの紐付けを管理してくれます。

app/Providers/AppServiceProvider.phpで以下のようにバインディングを定義します:

AppAppServiceProvider.php
public function register(): void
{
    $this->app->bind(
        RepositoryInterface::class,
        EloquentRepository::class
    );
}

この辺はぜひ、公式ドキュメント をご覧になっていただければと思います!

https://laravel.com/docs/12.x/container

まとめ

SOLID原則について頑張ってまとめてみました。なんとなく理解しているかな〜(多分してない)くらいの感覚だったのですが、いざ説明しようとすると全然理解できていなかったと思っています。特にリスコフの置換原則はいまだに全然理解できていないなと思います。説明も一番自信がないくらい、難しかったな〜と思いました。

抽象概念みたいなものが多いので、なかなかイメージが湧きづらいところですが、これからも継続して学習して、業務に取り組んでまいりたい所存でございます!

他にもいい例や、解釈が間違っている部分がありましたら、温かいコメントにてご教授いただけますと幸いです!!

参考文献

https://tatsu-zine.com/books/clean-architecture

https://www.sbcr.jp/product/4797347784/

https://qiita.com/baby-degu/items/d058a62f145235a0f007

https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898

GitHubで編集を提案
ソーシャルデータバンク テックブログ

Discussion