Tbpgr Blog

元エンジニア 人事 tbpgr(てぃーびー) のブログ

Ruby | Collecting Inputs | Represent special cases as Object

概要

Represent special cases as Object

前提

Confident Rubyではメソッド内の処理を次のように分類しています。
・Collecting Inputs(引数チェック、変換など)
・Performing Work(主処理)
・Delivering Output(戻り値に関わる処理)
・Handling Failure(例外処理)

当記事は上記のうち、Collecting Inputsに関する話です。

詳細

状況

プログラムの多くの箇所で考慮すべき特別なケースがある。
例えばWeb Applicationならカレントユーザーがログインしているかどうかで
振る舞いが変わる必要があるかもしれない。

概要

一意な型のオブジェクトとして特別なケースを表す。
特別なケースがどこで見つかってもポリモルフィズムによって取り扱う。

理由

特殊なケースの取り扱いにポリモルフィズムを利用することによって、数十もの繰り返しの条件分岐を
なくすことができるため。

サンプルコード仕様

プログラマ丸投げクラス(ProgrammerProvider)は指定した名前のプログラマのスキルリストを返却します。
スキルリストを取得後、スキルリストに関する様々な処理があちこちから呼ばれます。
ここでは例としてスキルリストの出力。
一番初めのスキルの出力。
を行います。

この際に、まったくスキルを持たないプログラマは同一ケースで処理できないため
「Represent special cases as Object」の適用前は毎回ifの分岐を行い、
「Represent special cases as Object」の適用後はSkillfulProgrammerクラスとNoSkillProgrammerを作成することで
分岐を局所化し、あちこちで同じような分岐を記述しないで済むようにします。

サンプルコード(適用前)

["tanaka", "sato", "miso"].each ・・内のロジックでskillの分岐が複数回登場しています。
(実際にはもっと大量に呼び出しがあることを想定)

class ProgrammerProvider
  PROGRAMMERS = [
    {
      name: 'tanaka',
      skills: ['Java', 'Oracle', 'HTML'],
    },
    {
      name: 'sato',
      skills: ['Ruby', 'MySQL', 'CoffeeScript'],
    },
    {
      name: 'miso',
      skills: nil,
    },
  ]

  def get_programmer_skills(name)
    PROGRAMMERS.select { |v|v[:name] == name}.first[:skills]
  end
end

pp = ProgrammerProvider.new
["tanaka", "sato", "miso"].each do |name|
  skills = pp.get_programmer_skills(name)
  if skill
    puts "#{name} has '#{skill.join(' ,')}' skills"
  else
    puts "#{name} has no skill"
  end

  # some logic

  if skill
    puts "#{name}'s first skill is '#{skill.first}' skills"
  else
    puts "#{name}'s first skill is nothing"
  end
end

出力

tanaka has 'Java ,Oracle ,HTML' skills
tanaka's first skill is 'Java' skills
sato has 'Ruby ,MySQL ,CoffeeScript' skills
sato's first skill is 'Ruby' skills
miso has no skill
miso's first skill is nothing

サンプルコード(適用後)

["tanaka", "sato", "miso"].each ・・内のロジックでskillの分岐がなくなりました。)

class SkillfulProgrammer
  attr_reader :name, :skills
  def initialize(name, skills)
    @name, @skills = name, skills
  end

  def print_skills
    puts "#{name} has '#{@skills.join(' ,')}' skills"
  end

  def print_first_skill
    puts "#{name}'s first skill is '#{skills.first}' skills"
  end
end

class NoSkillProgrammer
  attr_reader :name
  def initialize(name)
    @name = name
  end

  def print_skills
    puts "#{name} has no skill"
  end

  def print_first_skill
    puts "#{name}'s first skill is nothing"
  end
end



class ProgrammerProvider
  PROGRAMMERS = [
    SkillfulProgrammer.new("tanaka", ['Java', 'Oracle', 'HTML']),
    SkillfulProgrammer.new("sato", ['Ruby', 'MySQL', 'CoffeeScript']),
    NoSkillProgrammer.new("miso")
  ]

  def get_programmer(name)
    PROGRAMMERS.select { |v|v.name == name }.first
  end
end

pp = ProgrammerProvider.new
["tanaka", "sato", "miso"].each do |name|
  programmer = pp.get_programmer(name)
  programmer.print_skills
  programmer.print_first_skill
end

出力

tanaka has 'Java ,Oracle ,HTML' skills
tanaka's first skill is 'Java' skills
sato has 'Ruby ,MySQL ,CoffeeScript' skills
sato's first skill is 'Ruby' skills
miso has no skill
miso's first skill is nothing