1-2-3で、エクササーイズ(オブジェクト指向エクササイズ)

オブジェクト指向エクササイズでオブジェクト指向を体に覚えさせる。今回は、実際に書いたコードを全ては載せず、概念をざっくり理解するための記事としたい。

オブジェクト指向エクササイズとは?

『ThoughtWorksアンソロジー』 ThoughtWorks Inc. (著) の第5章には、オブジェクト指向を理解するための9つのルールが出てくる。

その9つのルールを体に覚えさせるエクササイズをオブジェクト指向エクササイズと言う。

9つのルールとは以下の通りである。

  1. 1つのメソッドにつきインデントは1段階までにすること
  2. else 句を使用しないこと
  3. すべてのプリミティブ型と文字列型をラップすること
  4. 1行につきドットは1つまでにすること
  5. 名前を省略しないこと
  6. すべてのエンティティを小さくすること
  7. 1つのクラスにつきインスタンス変数は2つまでにすること
  8. ファーストクラスコレクションを使用すること
  9. Getter, Setter, プロパティを使用しないこと

1つのメソッドにつきインデントは1段階までにすること

条件分岐の中に条件分岐のコードを書くことはついついやってしまう。

条件分岐を繰り返した結果、複雑すぎるメソッドが出来上がってしまう。これでは凝集度が低い。

そのため、インデントは1段階までにするという対策がとられている。

一つの振る舞いだけするメソッドに切り出すことによって、複雑すぎるメソッドが出来上がってしまう現象を回避することができる。

すべてのプリミティブ型と文字列型をラップすること

そもそもプリミティブ型とは

もともと備わっている型。具体的には下記の型を指している。

bool型,char型,double型,float型,int型,long型

例えば数字の大きさによって条件分岐するプログラムをテストするコードで、数字をべた書きしている場合。

# 数字をべた書きしている
def test500円でコーラを購入
    drink = @vm.buy(500, Drink::COKE)
    change = @vm.refund

下記のようにenumを使って型指定をしてあげる。

class Coin
  ONE_HUNDRED = 100
  FIVE_HUNDRED = 500
end

そうすると、テストコードにべた書きしていた数字を以下のようにリファクタできる。

def test500円でコーラを購入
    drink = @vm.buy(Coin::FIVE_HUNDRED, DrinkType::COKE)
    change = @vm.refund

このように意味のある値に対して型を定義することで、コードの可読性を高め、変更に強くし、拡張しやすくしている。一般的にValueObjectと呼ばれるテクニックで以下の特徴を持っている。

  • 一意性を持たない
  • 計測/定量化/説明を責務とする

ドメイン内の何かを計測したり定量化したり説明したりする

  • イミュータブルオブジェクト

作成後にその状態を変えることができないオブジェクト

  • 交換可能

計測値や説明が変わったときには、全体を完全に置き換えられる

  • ふるまいに副作用がない

ValueObjectという考え方 - Qiita

1行につきドットは1つまでにすること

その名の通り、1行につきドットは1つまでにすることを指す。

ドットが重なるようなメソッドはデメテルの法則(最小知識の法則)に違反している。

名前を省略しないこと

引数の名前は、英文字1文字や数字1字など、省略してはいけない。

理由:

コードを他の開発者に見せたときにその引数が何を示しているか理解するために多く時間を要するから。貴重な脳のメモリを名前の解読に使うのはもったいない。

すべてのエンティティを小さくすること

エンティティとは

モデルのこと。E-R図で出てくる箱のことを指している。

開発を進めていくと、クラスが大きくなってしまう。そこで下記規約が暗黙の了解で定義されている。

  • 1ファイル50行まで
  • 1パッケージ10ファイルまで

大きくなってしまったクラスやパッケージは、複数の責務を持つことが往々にしてある。

オブジェクト指向設計をするときは、単一責任の原則を採用しているので、一つのクラスが複数の振る舞いをすることは好ましくない。ただ、他の8つのルールを守っていると自然とクラスが小さくなるので、9つのルール一つ一つを忠実に守ることが大切である。

else句を使用しないこと

else句を多用して複雑なコードを書くことは可読性を下げかねない。というか、下げる。

だから、極力else句を使わないことが推奨されている。

じゃあどうやってelse句を回避するか

  • ガード節(ある条件を満たしていない時にreturnする or 例外を投げる)を使う
  • 早期returnを使う。
  • ポリモフィズムを使用する

1つのクラスにつきインスタンス変数は2つまでにすること

 

インスタンス変数を2つ以上作りたくなったら、最も重要な1つと、それ以外のグループの2つに分類するとよい。

下記のように、1つのクラスにインスタンスを5つ作成してしまっている場合、

それぞれのコードを大きく二つのクラスに分けて、呼び出すインスタンスは2つにしたい。

class VendingMachine

  def initialize
    @stock_of_coke = Stock.new(5) # コーラの在庫数
    @stock_of_diet_coke = Stock.new(5) # ダイエットコーラの在庫数
    @stock_of_tea = Stock.new(5) # お茶の在庫数
    @stock_of_100yen = StockOf100Yen.new(10) # 100円玉の在庫
    @change = Change.new # お釣り
  end

飲み物の在庫数とお金の数という二つにざっくり分けられそう。

そんな場合は、下記のように二つのクラスに分類する。

# 飲み物の在庫数
class Storage

  def initialize
    @stocks = {}
    @stocks[DrinkType::COKE] = Stock.new(5)
    @stocks[DrinkType::DIET_COKE] = Stock.new(5)
    @stocks[DrinkType::TEA] = Stock.new(5)
  end
# お金の数
class CoinMech

  def initialize
    @cash_box = CashBox.new(10)
    @change = Change.new
  end

そして、自動販売機クラスに5つあった変数を2つにまで削減できた。

# Before
class VendingMachine

  def initialize
    @stock_of_coke = Stock.new(5) #  コーラの在庫数
    @stock_of_diet_coke = Stock.new(5) # ダイエットコーラの在庫数
    @stock_of_tea = Stock.new(5) # お茶の在庫数
    @stock_of_100yen = StockOf100Yen.new(10) # 100円玉の在庫
    @change = Change.new # お釣り
  end

# After
class VendingMachine

  def initialize
    @storage = Storage.new # 飲み物の在庫
    @coin_mech = CoinMech.new # お金の数
  end

ファーストクラスコレクションを使用すること

配列をラップしたクラスのことで、対象の配列を全て集約したクラスのことを指している。

配列周りは、以下のように複雑な処理が行われることが多い。

  • for文などのループ処理
  • 配列やコレクションの要素の数が変換する(可能性がある)
  • 個々の要素の内容が変化する(可能性がある)
  • 0件の場合の処理
  • 要素の最大数の制限

メリット

下記のように100円の在庫に関する配列の複雑さを専用の小さなクラスに閉じ込めることで、

  • メンテナンス性の向上
  • プログラムの可読性向上

といった恩恵を受けることができる。


class StockOf100Yen
    def initialize(quantity)
        @number_of_100yen = [Coin::ONE_HUNDRED] * quantity
    end
    def add(coin)
        @number_of_100yen.push(coin)
    end
    def size
        @number_of_100yen.length
    end
    def pop
        @number_of_100yen.pop
    end
end

メインで呼び出すときは以下のようにする

if payment != Coin::ONE_HUNDRED && payment != Coin::FIVE_HUNDRED
      @change.add(payment)
      return nil
    end
# @change.add(payment)は@number_of_100yen.push(coin)を指している。
# つまり、100円と500円以外の支払いの場合nilを返すプログラムを組んでいる。

また、外部から編集ができてしまうことを防ぐために、Setterを設定しないことが好ましい。

Getter、Setter、プロパティを使用しないこと

カプセル化したメソッドは、外部からの不当な攻撃から防ぐために。GetterやSetter、プロパティを使用してはいけない。

例えば、在庫があるかを確認するメソッドにおいて、コードに0を直接書き込むのは好ましくない。

class VendingMachine

  ...

  def buy(payment, kind_of_drink)

    if kind_of_drink == DrinkType::COKE && @stock_of_coke.quantity == 0 #⇦この部分をコレクションクラスに移動させる。
      @change.add(payment)
      return nil

なので、別のクラスに在庫数を確認するメソッドを切り出して、定義したメソッド名だけ呼び出すようにコードを編集する。

class Stock
   def initialize(quantity)
     @quantity = quantity
   end

-  def quantity
-    @quantity
-  end
+  def empty?
+    @quantity == 0
+  end

 end
class VendingMachine

  ...

  def buy(payment, kind_of_drink)

    if kind_of_drink == DrinkType::COKE && @stock_of_coke.empty? #⇦empty?とするだけで良くなった!
      @change.add(payment)
      return nil

こうすることによって、VendingMachineクラスに数字をべた書きせずに済むようになった。

後述


上記9つのルールを守ることで、開発を効率よく進めるための重要な概念であるオブジェクト指向を体現できるそう。一番簡単そうな、1行につきドットは1つまでにすることから初めて行こうかな!

オブジェクト指向に近づく9つのルール (ThoughtWorks アンソロジーより) - Qiita

Convention over Configuration

Object#clone (Ruby 3.0.0 リファレンスマニュアル)

Rubyで作るファーストクラスコレクション - Qiita

 

オブジェクト指向エクササイズの陳腐化

オブジェクト指向エクササイズをやってみる - Qiita

ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション