🍋

Rubyでさらっと学ぶSOLID原則④「インターフェース分離の原則」

2021/12/29に公開

SOLID原則とは

ソフトウェアの拡張性や保守性を高めるための下記5つの原則のこと。

インターフェース分離の原則(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)

実際のアプリケーションの要件としてはありえませんが、上記を事前設定とします。
上記の要件は、以下のようにして実装できます。

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