SOLID原則
SOLID原則
SOLID原則とは、次の五つのソフトウェア設計の原則に習ったもの。
単一責任の原則:Single Responsibility Principle(SRP)
概要
文字通り一つのクラス、関数は単一の責任を持つように実装するという原則。
一つの責任を持つということは、実装の変更理由が一つであるということ。
目的
-
変更容易性
クラスの責任が大きいと、それだけ多くのアクターと関わりを持つことになる。
アクターは要件変更の理由となるため、一つのクラスが一つのアクターと関わりを持つようにすれば要件変更時が発生したときの影響範囲が狭くなる。 -
可読性向上
責任の範囲が狭いほど持っているロジックがシンプルになるため可読性が良い。 -
再利用性
責任が小さいと、持っているロジックの独立性が高くなるため、再利用しやすくなる。
ただし、今後変更の予定がほぼないことが確定している状況では、この原則を無視することもある。
参考
解放閉鎖の原則:Open/Closed Principle(OCP)
概要
拡張に開かれており、変更に閉じられているということ。
機能を追加しても、その機能を使う側のソースコードに変更がないこと。
USBの例
USBは規格に準拠していればどのデバイスでもさせるようになっている。(拡張に開かれている)
新しいデバイスを受け付けるために何かを変更する必要がない。(変更に閉じられている)
Rubyでの例
# 図形を追加することになっても、他の実装に影響しない
# 図形を描画するクラス
class Drawer
def draw(shape)
shape.draw
end
end
# 図形の基底クラス
class Shape
def draw
raise NotImplementedError, "#{self.class} には draw メソッドが実装されていません。"
end
end
# 円を描画するクラス
class Circle < Shape
def draw
puts "円を描画します。"
end
end
# 四角形を描画するクラス
class Rectangle < Shape
def draw
puts "四角形を描画します。"
end
end
# 新しい図形を追加するクラス(三角形)
class Triangle < Shape
def draw
puts "三角形を描画します。"
end
end
# メインの実行
drawer = Drawer.new
drawer.draw(Circle.new)
drawer.draw(Rectangle.new)
drawer.draw(Triangle.new)
目的
-
変更容易性
拡張に開放的なので、要件追加に容易に対応できる。
修正も閉鎖的であれば同様。
拡張しやすくするための基礎部分が実装されていて、それに影響を与えることなく機能が拡張されていく設計のイメージ。 -
保守性の向上
開放閉鎖原則に従えば自然と疎結合になるため、ロジックが分散することなく可読性が高くなることが見込まれる。
また、機能追加や修正に対しても管理がしやすい設計のため管理にかかるコストが低くなる。
参考
https://www.ogis-ri.co.jp/otc/hiroba/others/OOcolumn/single-responsibility-principle.html
リスコフの置換原則:Liskov Substitution Principle(LSP)
概要
派生型は基本型と置換可能でなければならないという原則。
もう少し具体的にいうと、派生クラスは規定クラスにないインターフェースやメソッドを持たないように設計する方法。
スマホの充電何%と聞いてIphoneでもAndroidでも答えられるのと似た感覚。どちらもスマホという基底クラスで定義した充電容量というプロパティを持っている。
目的
-
コードの柔軟性
基底クラスの実装を変更することなく、新しい派生クラスへの拡張が可能。
また、それぞれの派生クラスの修正が他のクラスへ影響を与えることがないため、実装の修正にも強くなる。
基底クラスで決めたルールに忠実に従うことで、継承したインスタンスをどこでも使うことができる。 -
再利用性の向上
基底クラスの実装を、派生クラスでも使用することができるので記述量が少なくて済む。
参考
インタフェース分離の原則:Interface Segregation Principle(ISP)
概要
クライアントに、クライアントが利用しないメソッドへの依存を強制してはならないという原則。
そもそもインターフェースとは、昨日の使い方のみを定義していて、データ処理を持たないもの。
インターフェースを共通化しようとするあまり、仕事が増えて、インスタンスに不要なメソッドを持たせることがないように気をつけなければいけない。
参考記事にもあるとおり、DRYの意味をしっかりと理解して設計する必要がある。
目的
-
独立性の向上
インスターフェースが必要最低限に、過不足ない機能を提供することで、凝集度が高まり、他のモジュールとも疎結合になる。
結合度が低いコードは、実装も読みやすいし、変更があった際の影響範囲も小さいし、テストもしやすい。
参考
依存性逆転の原則:Dependency Inversion Principle(DIP)
概要
上位モジュールは下位モジュールに依存すべきではないという原則。
モジュール同士の結合度が高いと、柔軟性、拡張性に欠けるため良くない。
参考記事にあるように、間に抽象クラスを用意して、お互いのモジュールをそこに依存させる。
Rubyでの例
# 抽象クラスやモジュールの定義
module Formatter
def format(text)
raise NotImplementedError, "You must implement the format method"
end
end
# 低レベルの具象クラス
class TextFormatter
include Formatter
def format(text)
"Text: #{text}"
end
end
# 高レベルのモジュールは抽象クラスやモジュールに依存
class Printer
def initialize(formatter)
@formatter = formatter
end
def print(text)
formatted_text = @formatter.format(text)
puts formatted_text
end
end
# 依存性注入を利用して具象クラスを注入
text_printer = Printer.new(TextFormatter.new)
text_printer.print("Hello, world!")
目的
-
独立性の向上
モジュール同士の結合度が低くなる。