🍋
Rubyでさらっと学ぶSOLID原則④「インターフェース分離の原則」
SOLID原則とは
ソフトウェアの拡張性や保守性を高めるための下記5つの原則のこと。
- S(Single-responsibility principle): 単一責任の原則
- O(Open-closed principle): オープン・クローズドの原則
- L(Liskov substitution principle): リスコフの置換原則
- I(Interface segregation principle): インターフェース分離の原則
- D(Dependency inversion principle): 依存性逆転の原則
インターフェース分離の原則(ISP)とは
「そのクラスにとって必要なメソッドだけを定義しましょう」という原則。
言い換えると、「そのクラスにとって不要なメソッドは定義してはいけない」ともいえます。
インターフェース分離の原則に違反している例①
class Animal
def eat
puts '食べる'
end
def fly
puts '飛ぶ'
end
end
class Bird < Animal
def cry
puts '鳴く'
end
end
class Human < Animal
def walk
puts '歩く'
end
end
bird = Bird.new
bird.cry # => '鳴く'
bird.eat # => '食べる'
bird.fly # => '飛ぶ'
human = Human.new
human.walk # => '歩く'
human.eat # => '食べる'
# 人間が飛ぶのはおかしい
human.fly # => '飛ぶ'
人間がflyメソッドを持っているのはおかしいので、適切なクラスにメソッドを配置します。
①の解決策
class Animal
def eat
puts '食べる'
end
end
class Bird < Animal
def cry
puts '鳴く'
end
def fly
puts '飛ぶ'
end
end
class Human < Animal
def walk
puts '歩く'
end
end
bird = Bird.new
bird.cry # => '鳴く'
bird.eat # => '食べる'
bird.fly # => '飛ぶ'
human = Human.new
human.walk # => '歩く'
human.eat # => '食べる'
human.fly # => undefined method `fly' (NoMethodError)
無事に人間は飛べなくなりました。
インターフェース分離の原則に違反している例②
より実践的な例として、Railsの実装パターンとしてよく使われるForm Objectを例にあげます。
前提
- Userクラスがある
- Userクラスは、名前(name)と年齢(age)を属性として持つ
- このアプリケーションを大きく2つに分けると、一般ユーザーに公開されているWebアプリケーションと、社内の人間だけがアクセスできる管理画面に分けられる
- 一般ユーザーに公開されているWebアプリケーションでの要件
- Userを作成・編集するとき、nameのNULLを許容しない(ageはNULLでもOK)
- 管理画面での要件
- Userを作成・編集するとき、ageのNULLを許容しない(nameはNULLでもOK)
- 一般ユーザーに公開されているWebアプリケーションでの要件
実際のアプリケーションの要件としてはありえませんが、上記を事前設定とします。
上記の要件は、以下のようにして実装できます。
class User < ApplicationRecord
validates :name, presence: true, on: :web
validates :age, presence: true, on: :admin
end
# 一般ユーザーに公開されているWebアプリケーションからUserを作成するとき
user = User.new(name: nil, age: 20)
user.valid?(:web) # => false
# 管理画面上からUserを作成するとき
user = User.new(name: 'user_name', age: nil)
user.valid?(:admin) # => false
上記の実装の問題点(将来的に問題になりそうな点)は以下のとおりです。
- 今はまだシンプルだが、さらにWebアプリからUserを編集するときと管理画面上からUserを編集するときの要件が違ってくると、どんどんUserクラスが複雑になる
- 仮に「APIを提供して、APIからUserの情報を変更できるようにしたい」という要件が出た場合、さらに複雑性が増す
- 「web上から編集するときだけ必要なメソッド」や「管理画面上からUserを編集するときだけに必要なメソッド」がUserクラスに集まると、インターフェース分離の原則に違反してくる
②の解決策
各ユーザーインターフェースごとにクラスを定義する(Form Objectを作る)ことで、問題点を解消できます。
class User < ApplicationRecord
end
class Web::UserForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :age, :integer
validates :name, presence: true
end
class Admin::UserForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :age, :integer
validates :age, presence: true
end
# webから登録しようとしたとき
form = Web::UserForm.new(name: nil, age: 20)
form.valid? # => false
# 管理画面から登録しようとしたとき
form = Admin::UserForm.new(name: 'user_name', age: nil)
form.valid? # => false
上記の実装のメリットは以下のとおりです。
- 「Web上から作成・編集するときのインターフェース(Web::UserForm)」と「管理画面上から作成・編集するときのインターフェース(Admin::UserForm)」を分離できた(Userクラスから、「UserがWebから作成・編集されるのか、管理画面上から作成・編集されるのか」という知識を取り除くことができた)
- 「新しくAPIを作って、APIからUserの情報を変更できるようにしたい」という要件が出てきた場合も、既存のコードに修正を加えることなく新しくApi::UserFormを作ればよい(オープン・クローズドの原則にものっとっている)
各コンテキストごと(ユーザーインターフェースごと)にクラスを分離することができました。一見、実装するコードの記述量が増えただけのように見えますが、実装が進んで要件が複雑になってくると徐々に恩恵を感じられます。
インターフェース分離の原則違反に気付くための質問
- そのクラスにとって不要なメソッドを定義していないか?
- そのクラスがそのメソッドを持つのは不自然ではないか?
Discussion