オブジェクト指向

YAGNIでいいと思った

下記の記事がおもしろかった。おもしろかったんだけど、ちょっと疑問がある。

値引き条件などというものは、ビジネス上の都合により変更されやすいものです。このケースのように注文数量だけで値引き可否が決まるというケースもあるかもしれませんが、発注金額も考慮し、あるいは発注者が上得意かどうかも判断要素に含める、というように変更されるかもしれません。一方で、注文数量・金額・発注者が誰かなども含む受注内容に応じて値引き可否が決まる、という点はたぶん変わらないだろうと考えられます。

PHP Mentors -> 「現場で役立つシステム設計の原則」批判 (1) 〜何のために、「データとロジックを一体に」するのか?〜

うーん。「たぶん変わらないだろう」という楽観にもとづいている点で、批判対象の設計と同じ穴の狢のように見えるんだよなあ。

たとえば、「前回の発注に製品Aが含まれ、かつ、今回の発注に製品Bが含まれる場合、値引き可とする」という条件が追加される可能性だってあるだろうし。


かくいうぼくは、「値引きは数量のみに基づいて行われる」というビジネス要件だったら、まず下記のように書く。要件が変わったら、設計を見直す。起こるかどうか分からないことを考えるより、さくっと書いてリリースするのが、ビジネス的にもメンタル的にも吉なのでは。

class DiscountJudge
  def initialize(discount_criteria)
    @discount_criteria = discount_criteria
  end

  def discountable?(quantity)
    quantity >= @discount_criteria
  end
end

class AmountCalculator
  def initialize(discount_rate)
    @discount_rate = discount_rate
  end

  def discount(unit_price, quantity)
    unit_price * quantity * (1 - @discount_rate)
  end

  def amount(discount_judge, unit_price, quantity)
    if discount_judge.discountable?(quantity)
      self.discount(unit_price, quantity)
    else
      unit_price * quantity
    end
  end
end

discount_criteria = 5
discount_judge = DiscountJudge.new(discount_criteria)

discount_rate = 0.2
amount_calculator = AmountCalculator.new(discount_rate)

unit_price = 100
quantity = 6
amount_calculator.amount(discount_judge, unit_price, quantity)

スタティックメソッドの使いどころ

最近、スタティックメソッドの使いどころについて考えていた。結局「スタティックメソッドのラッパ」以外の用途が思いつかなかった。


スタティックメソッドA内で、インスタンスBのインスタンスメソッドを呼びたくなったとしたら、そもそもメソッドAはインスタンスBに属してしかるべきだ。簡単にいえば、Adder.add(a, b) はおかしくて、Foo.new(a).add(b) とすべきということだ。

なぜ「すべき」なのか。それがオブジェクト指向だからだ。手続き型のコードが書きたければ、オブジェクト指向言語を使う必要はない。

これまでの話を裏返せば、メソッド内でインスタンスメソッドを呼ばない場合にだけ、スタティックメソッドを使ってよいということになる。すなわち「スタティックメソッドのラッパ」である。

いったい、スタティックメソッドのラッパが欲しくなる場面がどれだけあるだろうか。


開発中のコードを上記の指針でリファクタリングしたら、スタティックメソッドが一切なくなった。よかった。

リファクタリングどこまでやるの

僕はプログラミングの際、設計についてはあまり考えず、必要な機能を満たすコードをガーッと書いて、あとでザクザクとリファクタリングすることが多い。クラス数が元の何倍になることもある。

とはいえ、完璧主義の僕にとってリファクタリングは、いくらやっても終わらないハマりポイントになりがちだ。なので、コードからなんとなく漂う違和感がなくなった段階で、思いきって終わりにするしかない。


下記は、リファクタリング途中のコードを模式化したものです。さて、僕はどこまでリファクタリングしたでしょうか。

class PetOwner
  def initialize(pet)
    @pet = pet
  end

  def work
    case @pet.type
    when 'dog'
      self.buy_dog_food
    when 'rabbit'
      self.tidy_up_room
    when 'bird'
      self.close_window
    end
  end

  ...
end

答は後日。細かい点は気にせず、インスタンス変数のプロパティに応じて異なるメソッドを呼んでいる構造にのみ注目してください。


追記(2016-03-15)

こんな感じにした。

class PetOwner
  def initialize(pet)
    @pet = pet
  end

  def work
    self.fulfill(@pet.owners_duty)
  end

  ...
end

class Pet
  def initialize(type)
    @type = type
  end

  def owners_duty
    case @type
    when 'dog'
      BuyingDogFood.new
    when 'rabbit'
      TidyingUpRoom.new
    when 'bird'
      ClosingWindow.new
    end
  end
end
  • case文をPetクラスに移動した。typeによる分岐はPetクラスの責務だと思ったので。
  • PetOwnerクラスのインスタンスをPetクラスのメソッド引数で渡すのは避けた(なんとなく)。

これでよかったのかどうか、今はまだ分からない。とりあえず寝よう。

プロフィール
知識欲と謎解き欲が旺盛なWebエンジニア。AWS認定ソリューションアーキテクト&デベロッパー(いずれもアソシエイト)。JAPAN MENSA会員
カテゴリ別アーカイブ
記事検索