14
0

[Ruby]オブジェクト指向設計を使って変更に強いコードを書きたい ~クラスの責任とインターフェース編~

Last updated at Posted at 2023-12-01

変更に強いコードを書きたい。
変更に強いコードを書くことで、後々修正が楽になるから。
これがオブジェクト指向設計を学ぶ目的の一つではないでしょうか?
でもオブジェクト指向設計って、色々と難しい言葉が出てきて理解しにくい...
そんな悩みを抱えている方もいらっしゃると思います。

そこで今回は、変更に弱いコードを少しずつ変更に強いコードに修正しながら、オブジェクト指向設計の考え方の一部を紹介していきたいと思います。

1.この記事の目的

変更に強いコードを書きたい。そのために、

①クラスが果たすべき責任について考えてみる
②クラス内のインターフェースを設計してみる
③クラス同士の依存関係を減らしてみる

2.クラス同士が強い依存関係にあると、コードは変更に弱くなる

ではまず変更に弱いコードから紹介していきます。

2-1.変更に弱いコード例

今回は、上司が新卒部下に会議の準備をするように指示するシチュエーションをコードとして表現してみました。

上司と新卒部下の仕事上でのやり取りって、現実でもよくありますよね。

現実でもよくあるシチュエーションをもとにオブジェクト指向設計について考えることで、オブジェクト指向設計のイメージを掴んでいきましょう。



あなたは新卒の太郎くんを部下として指導することになりました。
まだまだ仕事に慣れていない太郎くんに対しては、一つずつ仕事を指示していかないといけません。
あなたは会議の準備をするための仕事を、太郎くんに指示します。

あなた:「仕事を開始したら報告してね。」
太郎くん:「仕事を開始しました。」
あなた:「会議室を予約してね。」
太郎くん:「会議室を予約しました。」
あなた:「会議用の資料を準備してね。」
太郎くん:「資料を準備しました。」
あなた:「会議の参加者に、日程を連絡してね。」
太郎くん:「会議の参加者に連絡しました。」

new_graduate.rb
class NewGraduate
  attr_reader :name
  
  def initialize(name)
    @name = name
  end

  def report_start_works
    puts '仕事を開始しました。'
  end

  def reserve_meeting_room
    puts "#{name}が会議室を予約しました。"
  end

  def prepare_documents
    puts "#{name}が資料を準備しました。"
  end

  def contact_participants
    puts "#{name}が会議の参加者に連絡しました。"
  end
end
boss.rb
class Boss
  def give_directions(new_graduate)
    new_graduate.report_start_works
    new_graduate.reserve_meeting_room
    new_graduate.prepare_documents
    new_graduate.contact_participants
  end
end
index.rb
require './boss'
require './new_graduate'

boss = Boss.new
taro = NewGraduate.new('太郎')

boss.give_directions(taro)

# => "仕事を開始しました。"
# => "太郎が会議室を予約しました。"
# => "太郎が資料を準備しました。"
# => "太郎が会議の参加者に連絡しました。"

2-2.クラス同士が強い依存関係であることの問題点

上記のコードでは、ボスクラスのインスタンスであるあなたが、新卒クラスの太郎くんが会議の準備をするために行うべき仕事を一つずつ指示しています。

会議の準備をするために必要な仕事が何なのかを把握しているのはgive_directionsメソッドが実装されているボスクラスのあなたです。

このコードでは、新卒クラスの太郎くんは会議の準備を「どのように」進めていくのか自分で判断していません。ボスクラスのあなたの言われた通りにメソッドを実行しているだけです。

また、ボスクラスと新卒クラスの依存関係はreport_start_worksreserve_meeting_roomprepare_documentscontact_participantsの4つにまたがっています。

ではもし仮に、会議の準備をするために「飲み物を準備する」必要があるとしましょう。
皆さんはどのようにコードを追加しますか?




おそらく、以下のように新卒クラスにメソッドを追加すると思います。

new_graduate.rb
class NewGraduate
  # 省略
  
  def prepare_drink
    puts "#{name}が飲み物を準備しました。"
  end
end

しかし、新卒クラスにメソッドを追加するだけではprepare_drinkメソッドは実行されません。
会議の準備を行うために何が必要かを把握しているボスクラスのあなた側で、prepare_drinkメソッドを実行するように指示していないからです。
そのため、ボスクラスのgive_directionsメソッドの修正も必要になります。

boss.rb
class Boss
  def give_directions(new_graduate)
    new_graduate.report_start_works
    new_graduate.reserve_meeting_room
    new_graduate.prepare_documents
    new_graduate.contact_participants
    new_graduate.prepare_drink # 追加
  end
end

このように、新卒クラスはボスクラスのあなたがいないと何もできません。新卒クラスを修正するとボスクラスの修正も必要になるという点で、ボスクラスと新卒クラスには強い依存関係があります。

もしボスクラスのコードの修正を忘れてしまうと、新卒クラスの太郎くんはprepare_drinkメソッドを実行できず、バグを生む原因となってしまいます。

上記のコードは、あるクラスのコードを修正すると他のクラスのコードの修正も必要になってしまう変更に弱いコードです。

3. 変更に強いコードに修正していく

では、上記コードを変更に強いコードに修正していきましょう。
修正する上で考えるべきことは、大きく分けて3つです。

①クラスが果たすべき責任について考える
②オブジェクト間のメッセージについて考える
③クラス内のインターフェースについて考える

一つずつ順を追って考えていきましょう。

3-1.クラスが果たすべき責任について考える

まず、クラスが果たすべき責任について考えていきます。

3-1-1. 新卒クラスが果たすべき責任

今回、新卒クラスが果たすべき責任は何でしょうか?
それは「会議の準備をすること」でしょう。

新卒クラスの太郎くんは「会議の準備をしてね」と言われたら、自身の責任において会議の準備を行わなければいけません。
そのため、仮に会議の準備に「飲み物を準備すること」が必要なのであれば、新卒クラス自身が判断して飲み物を用意する必要があります。

「飲み物を用意してね」と他のクラスから言われないと飲み物を準備できないクラスは、「会議の準備をする」という責任を自身で果たせない未熟なクラスです。

3-1-2. ボスクラスが果たすべき責任

一方でボスクラスが果たすべき責任は何でしょうか?
それは新卒クラスに「仕事をしてね」と指示を出し、新卒クラスに仕事をさせることです。
そのためにgive_directionsメソッドが実装されています。
今回の場合であれば、このgive_directionsメソッド内で新卒クラスの太郎くんに会議の準備をさせたいわけです。

3-2.「どのように」してほしいのか伝えるのではなく、「何を」してほしいのかを伝える

ボスクラスと新卒クラスの責任は以下のとおりでした。

  • ボスクラス:仕事の指示を出す(今回の場合は、新卒クラスに会議の準備をしてもらう)
  • 新卒クラス:会議の準備を行う

ここで注意すべきことは、ボスクラスはあくまでも「会議の準備をしてね」という願いを新卒クラスに伝えるだけであり、「どのように」会議の準備をするかの責任は新卒クラスにあるということです。

そのため、ボスクラスは会議の準備を「どのように」行うのかについて、新卒クラスに詳細な指示を出してはいけません。
ボスクラスは例えば、new_graduate.prepare_meetingのように、ただ「何を」をしてほしいのかということだけを新卒クラスに伝えれば良いのです。

3-3.インターフェースを考えることでクラス間の依存関係を管理する

ボスクラスと新卒クラスが果たすべき責任と、ボスクラスと新卒クラス間のメッセージについて考えてきました。
これらをもとに、新卒クラスのインターフェースについて考えていきましょう。

3-3-1.新卒クラスのインターフェースを考える

それぞれのクラスが果たすべき責任と、ボスクラスが新卒クラスに出すべきメッセージは「会議の準備をしてほしい」ということを踏まえた上で、新卒クラスのインターフェースについて考えていきます。

インターフェースという言葉にはさまざまな概念がありますが、クラス内のインターフェースは「パブリックインターフェース」と「プライペートインターフェース」に分けることができます。
2つのインターフェースの特徴は以下の通りです。

パブリックインターフェース

  • クラスの主要な責任を明らかにする
  • 外部から実行されることが想定される
  • 気まぐれに変更されない
  • 他者がそこに依存しても安全
  • テストで完全に文書化されている

プライベートインターフェース

  • 実装の詳細に関わる
  • ほかのオブジェクトから送られてくることは想定されていない
  • どんな理由でも変更され得る
  • 他者がそこに依存するのは危険
  • テストでは、言及さえされないこともある

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」より引用

クラスは自身の責任を果たすために多くのメソッドを定義します。それらのメソッドは、外部から使用されることを想定した広く一般的なメソッドから、クラスの内部でのみ使用されることを目的としたメソッドまで色々です。

その色々なメソッドの中で、クラスの主要な責任を果たすために外部から実行されることが想定されるメソッドを「パブリックインターフェース」として、それ以外のメソッドを「プライベートインターフェース」として定義していきます。

今回の新卒クラスの場合、「会議の準備をする」ことがパブリックインターフェースであり、会議の準備をするための詳細に関わるメソッドが「プライベートインターフェース」です。

そのため、以下のように新卒クラスを修正することができます。

new_graduate.rb
class NewGraduate
  attr_reader :name
  
  def initialize(name)
    @name = name
  end

  def prepare_meeting
    report_start_works
    reserve_meeting_room
    prepare_documents
    contact_participants
  end

  private

  def report_start_works
    puts '仕事を開始しました。'
  end

  def reserve_meeting_room
    puts "#{name}が会議室を予約しました。"
  end

  def prepare_documents
    puts "#{name}が資料を準備しました。"
  end

  def contact_participants
    puts "#{name}が会議の参加者に連絡しました。"
  end
end

外部から呼びだされることを想定したprepare_meetingをパブリックインターフェースとして定義しました。このprepare_meetingは、新卒クラスが果たすべき責任は「何」なのかを定義しているメソッドです。

それ以外のメソッドはプライベートインターフェースとして、privateなメソッドに変更しました。これらは新卒クラスが「どのように」責任を果たすのかという詳細を定義しているメソッドです。

この修正によって、ボスクラスは以下のように修正されます。

boss.rb
class Boss
  def give_directions(new_graduate)
    new_graduate.prepare_meeting
  end
end

3-3-2.インターフェースを考えることで依存関係を減らせる

この修正を通して、ボスクラスのインスタンスであるあなたは「どのように」会議の準備を進めるのかについて、詳細を気にする必要はなくなりました。

あなたはただ、新卒クラスに「会議の準備をしてね」と伝えているだけです。実際にどのように準備を進める必要があるのかについては、新卒クラスの判断に任せているのです。

上記のように修正を行うことで、ボスクラスと新卒クラスの間の依存関係はprepare_meetingのみに変わりました。

では、もし仮に会議の準備をするために「飲み物を準備する」メソッドを追加する必要があるとしましょう。
皆さんはどのようにコードを追加しますか?





例えば以下のように追加します。
「飲み物を準備する」メソッドを、新卒クラスのプライベートインターフェースとして定義します。

new_graduate.rb
class NewGraduate
  # 省略

  private
  # 省略

  def prepare_drink
    puts "#{name}が飲み物を準備しました。"
  end
end

次に、prepare_meetingメソッド内で、prepare_drinkを呼び出します。

new_graduate.rb
class NewGraduate
  attr_reader :name
  
  def initialize(name)
    @name = name
  end

  def prepare_meeting
    report_start_works
    reserve_meeting_room
    prepare_documents
    contact_participants
    prepare_drink # 追加
  end

  private
  # 省略
end

今回の場合は、以前と異なりボスクラスの修正は必要ありません。
ボスクラスと新卒クラスの依存関係は新卒クラスのパブリックインターフェースであるprepare_meetingのみに限定されているため、prepare_meeting内で呼ばれるメソッドが増えたとしても、ボスクラスに影響はありません。

ボスクラスのあなたは変わらずprepare_meetingメソッドを呼び出し、ただ太郎くんに「会議を準備してね」と伝えるだけです。

そして、新卒クラスの太郎くんは上司のあなたの指示を受けることなく、自身の判断で会議の準備を「どのように」行うのか判断ができるようになり、クラスとして自立することができました。
以下のように...



あなた:「会議の準備をしておいてね。」
太郎くん:「わかりました。会議室の予約・資料の準備・参加者への日程の連絡を行いますね。あ、そういえば飲み物の準備も必要ですよね。この前、飲み物が用意されていなくて大変でしたもんね。ついでに購入しておきますね。」
あなた:「ありがとう。この頃は自分で色々と判断できるようになったね。成長を感じるよ。」

4.まとめ

今回はオブジェクト指向設計で変更に強いコードを書く流れについて説明しました。
クラス同士に依存関係が発生することは避けられません。

しかし、

 ①クラスが果たすべき責任について考える
 ②クラス内のインターフェースを設計してみる
 ③上記2つを考えることで、クラス同士の依存関係を減らしてみる

の3点を考えながらコードを書くことで依存関係を管理することは可能です。

もちろん、他にも様々なポイントを考慮する必要はありますが、まずは上記ポイントを踏まえて変更に強いコードを書いてみてはいかがでしょうか?

14
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
0