きれいなコードを書くためにSOLID原則を学びました① ~単一責任の原則~


マネージャ「鈴木くん、SOLIDって知ってる?」
私「もちろん知ってますよ!僕は特に2が好きっすね!スネークもいいっすけど、雷電もかっこよかったなぁ...」
マネージャ「...」

以上は、私と私のプルリクをみたマネージャとの会話です。
よくよく聞いたら、「TypeScriptとPHPを使うのであれば、オブジェクト指向言語のSOLID原則を抑えた方がいいよ!」というマネージャからの温かいアドバイスだったのですが、入社2ヶ月目(といってもアラサー)だった当時の私には、「何の脈略もなくいきなりゲームの話をはじめるなんて、きっとマネージャは話相手がいなくて寂しいんだな...」としか思えなかったわけです。

とはいえ、当時は無限プルリクでレビュワーの方の工数を圧迫してしまうという大事態を起こしており、きれいなコードを書かなければという思いが強かったので、このSOLIDたるものを学ぶことにしました。

今更ではあるのですが、メモとして残そうと思い記事にまとめた次第です。

SOLID原則って何?

SOLIDはメンテナンス性が高く(変更に強くて理解しやすく)、寿命の長いソフトウェアをつくるための、オブジェクト指向言語における5つの重要な原則です。

・S:Single Responsibility Principle (単一責任の原則)
・O:Open Closed Principle (オープン・クローズドの原則)
・L:Liskov Substitution Principle (リスコフの置換原則)
・I:Interface Segregation Principle (インタフェース分離の原則)
・D:Dependency Inversion Principle (依存関係逆転の原則)

今回は、単一責任の原則についてまとめました。

その他の記事は以下。
きれいなコードを書くためにSOLID原則を学びました② ~オープン・クローズドの原則~
きれいなコードを書くためにSOLID原則を学びました③ ~リスコフの置換原則~
きれいなコードを書くためにSOLID原則を学びました④ ~インターフェース分離の原則~
きれいなコードを書くためにSOLID原則を学びました⑤ ~依存関係逆転の原則~

単一責任の原則

Every software component should have one and only one responsibility.

直訳すると、「すべてのソフトウェアのコンポーネントは、一つのことだけ責務を負うべきである。」という意味になります。
これは、「ソフトウェアの各コンポーネントは1人のアクターに対して責務を負うべきである。」ともいいかえることができます。

ソフトウェアのコンポーネントとは、オブジェクト指向でいうクラスを指します。
つまり、この原則は、クラスは一人のアクターが使うように設計されていなければならないという意味になります。

もしクラスが複数のアクターに使用されると以下のような問題が起こってしまいます。
1. クラス内のメソッドが2人のアクターに共有して使われると、1人のアクターがメソッドの内容を変更したとき、もう片方のアクターが変更に気が付かず、メソッドがアクターの想定とは異なる挙動をしてしまう。
2. 各メソッドの使用者を1人のアクターに固定したとしても、各アクターがたまたまメソッドを同時に修正した場合に、同じクラスの修正となるためコンフリクトが起こってしまう。

以下で、単一責任の原則の肝となる、凝集度と結合度について説明します。

凝集度 (Cohesion)

凝集度(cohesion)はソフトウェアのコンポーネントの関連度合いのことをいいます。

ん?...何言ってるかわかんないですね。

わかりやすさのために、ゴミの分別を例に考えてみます。

みなさんみたいな紳士淑女とは違い、世の中にはマナーを知らない大人がそこそこいます。そんな人たちはゴミの分別などには気もとめないので、資源ゴミやもやせるゴミやもやせないゴミを全部まとめて捨ててしまいます。

この分別されていないゴミの山は大体の場合回収されません。もやせるゴミとして運良く回収されたとしても、処理されたときに環境へ影響を与えてしまうでしょう...よくない!非常によくない!!

このように処理の仕方が異なるゴミが混ざりあった状態は凝集度が低いと呼ばれます。一方、ゴミがしっかり分類された理想的な状態は凝集度が高いと呼ばれます。

ソフトウェアのコンポーネントというのはオブジェクト指向でいうクラスにあたります

つまり、ソフトウェアのコンポーネントの凝集度が高いというのは、クラスが似た性質のメソッドで構成されているという意味に置き換えられます。

具体例として、以下のSquareクラスを考えてみます。

public Square {
  int side = 5;

  public int calculateArea() { // ①面積計算メソッド
    return side * side;
  }

  public int calculatePerimeter() { // ②外周計算メソッド
    return side * 4;
  }

  public void draw() { // ③画像レンダーメソッド
    if (highResolutionMonitor) {
      // Render a high resolution image of a square
    } else {
      // Render a normal image of a square
    }
  }

  public void rotate(int degree) { // ④回転レンダーメソッド
    // Rotate the image of the square clockwise to
    // the required degree and re-render
  }
}

Squareクラスには以下の4つのメソッドが含まれています。
①面積計算メソッド
②外周計算メソッド
③画像レンダーメソッド
④回転レンダーメソッド

これは凝集度が高いとはいえません。なぜなら計算を行うメソッドとレンダーを行うメソッドが同じクラスに含まれてしまっているからです。この状態を改善するために計算用のクラス(SquareCalc)とレンダー用のクラス(SquareUI)にわける必要があります。

public SquareCalc {
  int side = 5;

  public int calculateArea() {
    return side * side;
  }

  public int calculatePerimeter() {
    return side * 4;
  }
}
public SquareUI {
  public void draw() {
    if (highResolutionMonitor) {
      // Render a high resolution image of a square
    } else {
      // Render a normal image of a square
    }
  }

  public void rotate(int degree) {
    // Rotate the image of the square clockwise to
    // the required degree and re-render
  }
}

これで凝集度の高いクラスが完成しました。
このように、高い凝集度をもったクラスをつくることが、単一責任の原則に沿った考え方の1つです。

結合度 (Coupling)

Coupling is defined as the level of inter dependency between various software component

結合度とは、様々なソフトウェアコンポーネント間の依存の度合いのことをいいます。
依存性が小さいほど結合度は低く、理想的な状態となります。

ん?...またしても何言ってるかわかんないですね。

新幹線と線路の関係を例に考えてみます。

新幹線は在来線よりも幅の広い線路で走ります。これを言い換えると、新幹線は在来線の線路では走れず、在来線は新幹線の線路を走ることができないということになります。
つまり、車両が走るためには線路に大きく依存する(=結合度が高い)ということになります。

次に実際のソースコードで考えてみます。
以下のように、saveメソッドでMySQLへのデータ登録処理を行ってしまうと、MongoDBなどのNoSQLDBに置き換えたときにメソッド内の処理をまるごと書き換えなければなりません。

public class Student {
  private String studentId;
  private Date studentDOB;
  private String address;

  public void save () {
    // DB(MySQL)へのデータ登録処理
  }

  public String getStudentId() {
    return studentId;
  }

  public void setStudentId(String studentId) {
    this.studentId = studentId;
  }
}

ここでデータ登録処理の部分をRepositoryとして切り分けてあげると、Repositoryの切り替えのみでデータの登録先を変更することができます。

public class Student {
  private String studentId;
  private Date studentDOB;
  private String address;

  public void save () {
    //データ登録先によってリポジトリを切り替える
    new StudentRepository().save(this);
    // new StudentNewRepository().save(this);
  }

  public String getStudentId() {
    return studentId;
  }

  public void setStudentId(String studentId) {
    this.studentId = studentId;
  }
}

public class StudentRepositoty {
  public void save (Student student) {
    // DB(MySQL)へのデータ登録処理
  }
}

public class StudentNewRepositoty {
  public void save (Student student) {
    // DB(MongoDB)へのデータ登録処理
  }
}

このように結合度が低くなるようにクラスをつくると、設計の自由度が高くなります。

"責任"は"変更理由"とも置き換えられる

EverySoftware component should have one and only one reason to change.

単一責任の原則を提唱したRobert.C.Martin氏は、「すべてのソフトウェアのコンポーネントの変更理由は1つであるべきである。」とも言っています。

この言葉は以下のようなことを意味しています。
「ソフトウェアは変更し続けるものだが、コンポーネントが変更される理由は1つでなければいけない。変更理由が何個もあると、変更頻度が多くなり、バグを生み出す可能性が高くなってしまう。おまけにテストの工数も増えて金がかかってしまう。」

結合度の説明の例で、DBへのデータ登録のメソッド(save)をStudentRepositoryクラスのメソッドとしてきりわけました。
これはプロフィール情報(id, name)の変更はStudentクラス、DBの変更はStudentRepositoryクラス、というようにクラスの変更理由を1つにしたことと同義だったということですね。

おわりに

Laravelでバックエンドのプログラムを書く際に、DBデータの取得処理をサービス内で行っていましたが、単一責任の原則にのっとってリポジトリに処理をきりわけるようになりました。ダミーデータを使いたい場合にリポジトリを切り替えるだけで対応できるようになり、以前のコードよりよくなったことを感じました。

参考書籍・動画

Crean Architecture Robert.C.Martin
SOLID Principles: Introducing Software Architecture & Design Sujith George