Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2020 9日目の記事です。
所属チーム内でも保守性の向上について注目される機会が多くなりました。その中でも、「良い設計」って何だろうと考えるようになり、設計面のノウハウや書籍に関心を持つようになりました。
今回はその中でも有名なSOLIDの法則
の1つである単一責任の原則(SRP)
について書いてみようと思います。
そもそも良い設計とは何か?
最初に良い設計とは何かを定義していきます。
結論から書くと、変化に柔軟に対応できる設計だと考えています。
私は自社が提供するサービスの開発をしています。ユーザはもちろん、社内に法人の方も利用するサービスなので日々様々な要件を取り入れて、常に変化をしながらアプリケーションを成長させています。
この前提に立った時、良い設計とは変更がしやすく、変更コストが最小ないしは少なく済む形を指すと考えています。逆に言えば、悪い設計はこうした変更によるコストが高く、変更には時間がかかると考えています。
単一責任の原則とは?
本題の単一責任の原則についてです。
SOLID原則のうちの1つです。英語ではSingle Responsibility Principle
と呼ばれます。
クラスを変更する理由は1つ以上存在してはならない
という原則になります。
もう少し噛み砕くと1つのクラス(ないしはメソッド)には1つの役割しか持たせないようにする原則です。以下で例をいくつか紹介します。
責任が単一でないクラスの例
ちょっと雑ですがサンプルコードです。
# ペットクラス
class Pet
def initialize(name, animal_type)
@name = name
@animal_type = animal_type
end
# ペット共通の振る舞い
def eat_food
# なんか食べる処理
end
def bark
if @animal_type == 'cat'
'nyanyanya'
elsif @animal_type == 'dog'
'wanwanwan'
else
'gaoo!'
end
end
end
一見問題なく見えるかもしれませんが、もし新しいanimal_typeが増えて鳴き声も変えたいという要件が来た場合、単純に増やそうとすると下記のように書くことになります。
def bark
if @animal_type == 'cat'
'nyanyanya'
elsif @animal_type == 'dog'
'wanwanwan'
elsif @animal_type == 'bird'
'chunchun'
else
'gaoo!'
end
end
今のペットクラスは、猫、犬、鳥などの振る舞いを変える際にもペットクラスに手を加える必要があります。クラスを変更する理由が複数存在するため、このクラスは複数の責任を持っていると言えます。
単一責任がなぜ重要なのか?
barkは動物の種類が増えるたびに肥大化し、猫のなき声を変えるだけでも、影響範囲が犬や鳥にまで及んでしまう可能性があります。
単一責任の原則に則るのであれば、ペットクラスには全てのペットに共通の振る舞いが置き、犬個別の振る舞いなどはdogクラスなどに切り出すことで、影響範囲をサブクラス内に限定することができます。
# ペットクラス
class Pet
def initialize(name, animal_type)
@name = name
@animal_type = animal_type
end
# ペット共通の振る舞い
def eat_food
# なんか食べる処理
end
def bark
'gaoo!'
end
end
class Cat < Pet
def bark
'wanwanwan'
end
end
class Dog < Pet
def bark
'nyanyanya'
end
end
class Bird < Pet
def bark
'chunchun'
end
end
この構成にしておけば、動物の種類を増やした場合、ペットクラスを修正する必要がなく、個別のクラスを拡張することで要件を満たすことができます。また、動物個別の振る舞いもクラス単位で実装ができます。
このように、影響範囲を最小限にした上で機能拡張を行うことができるようになるため、既存のコードと比べると変化に柔軟に対応できると言えるのかな、と思います。
メソッドの責任も単一化する
クラスを分割することは第一に大切ですが、もっとカジュアルにメソッドから余計な責任を抽出することも有用ですぐに実践ができます。下記のようなコードがあったとします。
def squares_area(squares)
squares.collect { |square| square.vertical * square.horizontal }
end
四角形の縦と横を元に計算し、その結果を配列で格納しています。これは計算処理と配列に格納する処理の2つを担っていると言えます。
def squares_area(squares)
squares.collect { |square| squares_area(square) }
end
def square_area(square)
square.vertical * square.horizontal
end
こうすることでメソッドも単一責任化ができました。副次的にsquare単体でも計算ができるようになり、再利用性も高まりました。
このように最小単位にしておくことで、別のクラスへ処理を移行することも容易になったり、そもそもメソッドを書くべき場所が見えてきたりするので、このようにメソッドを分割することは常に行っておくことでより良い設計に近づけることができます。
まとめ
設計は答えがなく、いつもどこかを抱えながら行っていましたが、こうした先人の知恵を知ると前よりは自信を持ってクラスの分割などができるようになりました。
まだまだ「良い設計」をできるようになるには学ぶべきことも多いですが、まずはここから設計を意識してみてはいかがでしょうか?
次回予告
Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2020 9日目の記事でした
明日はフロントエンドエンジニアの@acc1ioさんです!お楽しみに〜〜〜