【SOLID原則】新人エンジニアが教える「単一責任の原則(SRP)」

Swift

こんにちは。新人プログラマーの岩本です。

今回はSOLID原則の1つ「単一責任の原則(Single responsibility principle」について、自分なりに調べたことをまとめます。

つよつよなエンジニアの方は意識せずにやっていることですが、我々新人エンジニアは少しでも気を抜くとこの原則に反したコードを書いてしまいます。(僕だけかもしれませんが😓)

この原則を意識するだけ、今よりちょっと良いコードが書けるようになると思います。

ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。

実装環境

  • 言語…Swift 5.8.1
  • OS… macOS Ventura 13.1
  • アプリ… Xcode 14.3.1

単一責任の原則(SRP)とは

単一責任の原則とはWikiでは以下のように説明されています。

単一責任の原則 (たんいつせきにんのげんそく、英: single-responsibility principle) は、プログラミングに関する原則であり、モジュール、クラスまたは関数は、単一の機能について責任を持ち、その機能をカプセル化するべきであるという原則である。モジュール、クラスまたは関数が提供するサービスは、その責任と一致している必要がある。

Wikiより引用

………?🤔

少し込み入っていて難しいので、簡単に説明します。

そもそもクラスにおける責任とはなんでしょうか?『良いコード/悪いコードで学ぶ設計入門』では以下のように説明されています。

ある関心ごとについて、不正な動作にならないよう、正常に動作するよう制御する責任

『良いコード/悪いコードで学ぶ設計入門』P148より引用

クラスにおける責任とは、「これだけは任せろ!」となる関心ごとのことです。

そして単一責任の原則とは「クラスが担う責任を、たった1つに限定しようぜ!」という原則です。

ポケモンの世界を考えてみましょう。

基本的にはどの作品にも共通して8つのジムがあります。それぞれのジムリーダーには1つのタイプが割り振られています。ジムリーダーは担当のタイプ以外のポケモンは使ってきません。

つまりポケモンのジムは単一責任の原則が守られていると言え、クラスも担当タイプのように1つの責任だけを持つべきということです。

なぜ責任を1つに限定するべきなのか?

現実世界の会社を考えてみましょう。通常、会社内は役割や目的に合わせて、経営部、人事部、技術部など、複数の部署に分かれています。

つまり責任を部署ごとに割り振っているわけです。

どうしてそうするのでしょうか?

仮に経営もできて人事もできて開発もできるスーパー人間が会社内にいたとします。

ほとんど全ての責任をたった1人の人間に負わせている状態です。短期間ではうまくいくかもしれません。

しかし、もしこの人が何らかの理由で欠席したらどうでしょう?業務が回らなくなるはずです。

たった1人の欠席が会社全体に大きな影響を与えてしまうのです。

これをコードに置き換えてみましょう。データの表示、保存、加工など複数の責任を負っているクラス(神クラス)を考えてみましょう。

これも短期間はうまくいくかもしれません。

しかし仕様が変更になり、コードを書き換えたとしましょう。ほんの少しの変更でも、その影響範囲は広大です。そのクラスを使っているほとんどのコードに影響が出てしまいます。

こうなってしまってはコードを変更することは困難です。

これは逆のことも言えます。

つまりクラスが担う責任を1つに限定すれば、変更の影響範囲を限定することができるのです。

これが単一責任の原則の重要性です。

SRPに反したコード

ではここからは単一責任の原則に反したコードを紹介します。これを見れば1つのクラスに複数の責任を負わせることの危険性がわかると思います。

今回はあらゆるペンの処理を行うクラスを例に説明します。

Pen.swift
struct Pen {
    // シャーペンで書く
    func mechanicalPencilWrite() {
        write("Hello, World")
    }

    // ボールペンで書く
    func ballpointPenWrite() {
        selectColor(color: "Black")
        write("Hello, World")
    }

    private func write(_ string: String) {
        print(string)
    }

    // ボールペンで使う色を選択
    private func selectColor(color: String) {
        // ...
    }
}

シャーペンで書く場合も、ボールペンで書く場合も、同様のwrite関数を使用しています。

一見問題はなさそうに思えます。

しかし特定のペン特有の処理が必要になった場合はどうでしょうか?

例えば、1文字書くごとにシャーペンの芯が短いかどうかを調べ、短ければノックするという処理を追加することになったとします。

Pen.swift
struct Pen {
    // シャーペンで書く
    func mechanicalPencilWrite() {
        write("Hello, World")
    }

    // ボールペンで書く
    func ballpointPenWrite() {
        selectColor(color: "Black")
        write("Hello, World")
    }

    private func write(_ string: String) {
        for c in string {
            // シャーペンの芯が短ければ、ノックする
            if isShortMechanicalPencilCore() {
                knock()
            }
            print(c, terminator: "")
        }
    }

    // ボールペンで使う色を選択
    private func selectColor(color: String) {
        // ...
    }

    // シャーペンをノックする
    private func knock() {
        // ...
    }

    // シャーペンの芯が短いかどうか
    private func isShortMechanicalPencilCore() -> Bool {
        // ...
    }
}

このコードの問題点を考えてみます。

まずPenクラスがシャーペンとボールペンという2つの責務を負っていることです。今後も万年筆やマジックペンなど、ペンが追加されればより複雑になります。

またボールペンで書く際にもシャーペンの芯の長さの判定を行なう、という問題もあります。

まだ簡単な処理だからいいですが、より複雑な処理になるに従ってバグが発生することも考えられます。

ではここからはこのコードをSRPに則って修正していきます。

SRPに則ったコード

復習ですが、単一責任の原則とは「クラスが持つ責任を1つにする」というものです。

これを意識して、先ほどのコードを修正していきましょう。

Pen.swift
protocol Pen {
    func write(_ string: String)
}

まずPenをプロトコルとして定義します。こうすることで、ペンの種類によって異なる処理を実装することができるようになります。

MechanicalPencil.swift
// シャーペン
struct MechanicalPencil: Pen {
    func write(_ string: String) {
        for c in string {
            // 芯が短ければ、ノックする
            if isShortMechanicalPencilCore() {
                knock()
            }
                print(c, terminator: "")
            }
    }

    // ノックする
    private func knock() {
        // ...
    }

    // 出てる芯が短いかどうか
    private func isShortMechanicalPencilCore() -> Bool {
        // ...
    }
}
BallpointPen.swift
// ボールペン
struct BallpointPen: Pen {
    private var selectedColor: String

    init(selectColor: String) {
        self.selectedColor = selectColor
    }

    func write(_ string: String) {
        print(string)
    }
}

それぞれのクラスはシャーペン、ボールペンと1つの責任のみを持っています。

writeの処理も書き分けられています。これで仕様が変更になっても、その影響範囲を限定することができます。

これが「単一責任の原則」です。

まとめ

今回はSOLID原則の1つ「単一責任の原則」を紹介しました。

繰り返しますが、単一責任の原則とは「クラスが持つ責任を1つにする」というものです。

これを意識してコードを設計するだけでも、かなり良いコードが書けると思います。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

参考にした記事

単一責任原則で無責任な多目的クラスを爆殺する - Qiita
この記事は クラウドワークスアドベントカレンダー2020 8日目の記事です。概要こんにちは、クソコードを爆殺リファクタリングするのが大好きなミノ駆動です。今回は単一責任原則の話です。単一責任…

コメント

タイトルとURLをコピーしました