はじめに
業務をこなしていく中で、皆さん以下のように感じたことはないでしょうか?
「システムは動くけど、コードの設計は正しくできているんだろうか...」
仕様を満たす実装は、ある程度経験を積んだエンジニアであれば問題ないと思います。
ただ、それでもレビューで「このメソッドは切り分けて...」など、設計に関する指摘が多いなと最近感じています。
なので、次にステップアップするためには、設計
だと思い、以下の本を読み、学習しました。
オブジェクト指向設計実践ガイド 〜Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方〜
自分は業務で主にRailsを扱っています。先輩エンジニアにもオブジェクト指向言語を扱う上での設計の良書とお聞きしました。
学んだ内容を、自分なりに言語化して、噛み砕いて整理しました。
その内容をアウトプットいたします。
目次
- オブジェクト指向設計
- 単一責任のクラスを設計する
- 依存関係を管理する
- 柔軟なインターフェースをつくる
- ダックタイピングでコストを削減する
- 継承によって振る舞いを獲得する
- モジュールでロールの振る舞いを共有する
- コンポジションでオブジェクトを組み合わせる
- 費用対効果の高いテストを設計する
1. オブジェクト指向設計
設計は本当に必要か?
- アプリケーションに変更がなければ、設計は不要
- ただ、そんなことはありえない
- 変更が容易なアプリケーションにするためにも、設計が必要
- アプリが小さければ、設計が貧弱でも耐えられる
- 全てを頭に入れられるから
- 設計が貧弱で大きいアプリだと、単純な変更でも、広範囲で影響してくる
- 開発するのも楽しくなる
- アプリが小さければ、設計が貧弱でも耐えられる
未来のことも考慮する
- ここでいう未来とは、まだ知られていない要件を想定して、実装しておくと言うことではない
- 将来何かが起こる、ということを認めて、未来を受け入れるための設計をする
- じゃあどうやって設計するの?というために、原則とパターンがある
設計原則と設計(デザイン)パターン
- SOLID原則
- 単一責任(Single Responsibility)
- オープン・クローズド(Open-Closed)
- リスコフの置換(Liskov Substitution)
- インターフェース分離(Interface Segregation)
- 依存性逆転(Dependency Inversion)
- パターン
- 理解するための準備と、適切に選び使うための知識が必要
- そうしないと、独自の使い方をして、混乱を招いてしまう
- 理解するための準備と、適切に選び使うための知識が必要
上記で理論は学んだので、あとはそれを実践に移していきましょう。
2. 単一責任のクラスを設計する
変更がかんたんであること
「変更がかんたんであること」を、本書では以下のように記載しています。
- 変更は副作用をもたらさない
- 要件の変更が小さければ、コードの変更も相応して小さい
- 既存のコードはかんたんに再利用できる
- 最もかんたんな変更方法はコードの追加である。ただし追加するコードはそれ自体変更が容易なものとする
上記を満たすために、クラスが単一の責任を持つように徹底する。
クラスが単一かを見極める方法
以下2つの方法で質問してみてください。
クラスの持つメソッドを質問に言い換えたとき、意味をなす質問になっているか
例として...
- 机クラスさん、あなたのサイズを教えて → OK
- 机クラスさん、あなたに付いている椅子のサイズを教えて → NG
1文で、クラスを説明してみる
「それと」や「または」が含まれていると、責任が2つ以上になっている。
3. 依存関係を管理する
クラス同士の依存が強いと、些細な変更でも多大な影響をアプリに与えてしまいます。
なので、依存関係を管理する考え方を、以下にまとめています。
自身より変更の少ないクラスに依存する
- そうしないと、影響範囲が大きくなってしまい、クラスの変更が難しくなってしまう
- また、変更の多いクラスに依存する、それ自体の影響も大きい
- アプリ全体に影響を及ぼす可能性もあるので、要注意
なので、設計で依存関係になる際、「自身より変更の少ないクラスに依存する」を忘れないようにする。
(※ 本書では、図を用いて、もっとわかりやすく説明しています。)
4. 柔軟なインターフェースをつくる
ここで説明するインターフェースとは、「クラスに定義するメソッド」 になります。
先ほどで、クラスには単一の責任が重要と説明しました。
その責任を説明するのに、パブリックインターフェースで表現されます。
なので、パブリックインターフェースをどのように実装していくかが、クラスの責任を表現することにおいて、重要になってきます。
シーケンス図を使う
設計を行う上で、文章だけだと、相手に伝わらない or 伝えられないことがあると思います。
そこで視覚的に分かりやすいシーケンス図を使用するよう、本書では記載されています。
簡単な例を、以下に記載しています。
図にすることで、どのオブジェクトが、どんなメッセージを交わしているのかが、視覚的にわかるようになります。
使い方に関しては、特に明記されていなかったので、自分のやりやすいように使えば良いかなと思っています。
明示的なインターフェースをつくる
明示的にわからないと、自分は分かったとしても、引き継ぐエンジニアに伝わらなければ、不用意な使われ方をされてしまいます。
- 名前は、考えられる限り、変わり得ないものである
- オプション引数として、ハッシュをとる
この辺りは意識して、設計をしようと思います。
5. ダックタイピングでコストを削減する
ダックタイピングとは、いかなる特定のクラスとも結びつかないパブリックインターフェースです。
例えば、以下のコードがあります。
def test(params)
params.each { |class|
case class
when A
class.a_method(some)
when B
class.b_method(hoge)
when C
class.c_method(fuga)
end
}
end
# それぞれのクラスで、メソッドを実装する。
それぞれのクラスで、具象的なパブリックインターフェースを使用しています。
これだと、新しくクラスが追加されたりすると、変更が大きく、依存度が高くなています。
これをダックタイピングを使うと、以下になります。
def test(params)
params.each { |class|
class.common_method(self)
}
end
# それぞれのクラスで、common_methodを実装する。
class A
def common_method(some)
...
end
end
class B
def common_method(some)
...
end
end
このように、具象から抽象的なインターフェースに変更して、各クラスでメソッドを実装するようにします。
そうすると、わかりやすく、変更に強い設計ができます。
6. 継承によって振る舞いを獲得する
継承とは、「メッセージの自動委譲」 の仕組みです。
例えば、果物屋があるとして、そこでは、りんごとオレンジを扱っています。
となると、用意するクラスとしては、Apple
・Orange
となりそうです。
ただ...果物屋が新しく桃も、扱うようになりました。今後も新しい果物が増える可能性はありそうです。
なので、果物
という抽象的なスーパークラスを用意して、具象的なサブクラスに分けた方が良さそうです。
分ける線引きとしては、具象クラスが3つ以上あると、分けた方が良いと本書では書かれています。
コードにすると、以下になります。
class Fruit
# 抽象的なスーパークラス
end
class Apple < Fruit
# Fruitのサブクラス
# 具象クラス
end
class Orange < Fruit
# Fruitのサブクラス
# 具象クラス
end
class Peach < Fruit
# Fruitのサブクラス
# 具象クラス
end
テンプレートメソッドパターンを使う
テンプレートメソッドパターンとは、スーパークラス内で基本の構造を定義し、サブクラス固有の貢献を得るためにメッセージを送るというテクニックです。
class Fruit
def initialize
template_method
end
end
class Apple < Fruit
def template_method
'apple_test'
end
end
サブクラス側に実装することで、固有のメッセージを得ることができます。
ただ、こうなるとFruit
を継承しているクラスは、全てtemplate_method
を実装する必要があります。
なので、以下のようにして、明示的にエラーを提供する必要があります。
class Fruit
def initialize
template_method
end
def template_method
raise RuntimeError
end
end
フックメッセージを使ってサブクラスを疎結合にする
継承で、super
などを使用してオーバーライドする実装をすると、スーパークラスとサブクラスの依存度が高くなってしまいます。
なので、サブクラスで合致するメソッドを実装して、情報を提供するようにします。
class Fruit
def initialize
hook_method
end
def hook_method
nil
end
end
class Apple < Fruit
def hook_method
'apple_test'
end
end
こうすることで、サブクラス側で実装がなくても、依存することなく、処理を行うことができます。
継承を使用すると、依存度は高くなってしまうので、これらのテクニックを使用して、設計を行っていきたいです。
7. モジュールでロールの振る舞いを共有する
ここでは、モジュールを利用して、ロール(役割)を共有する実装をします。
module Testable
# 振る舞い(メソッド)を実装
def display_number
'100'
end
end
class Example
include Testable # こうすると、振る舞いを共有することができる
end
example = Example.new
example.display_number
=> 100
上記ではざっくりとした実装ですが、これが例えば購入する役割を持ったモジュールなど、責務をしっかり与えることができます。
階層構造は浅くする
モジュールは、無限にインクルードできてしまいます。
モジュール同士でインクルードや、メソッドを上書きすることもできます。
なので、階層構造は浅くして、可読性を上げて、メンテしやすい実装を心がけましょう。
8. コンポジションでオブジェクトを組み合わせる
コンポジションとは、2つ以上のクラスから新しい機能を組み合わせる(コンポーズする)デザインパターンです。
has-a
の関係にあり、他のクラスのオブジェクトを含み、それらの機能を利用することで実現します。
class Engine
def start
puts 'エンジンが始動しました。'
end
def stop
puts 'エンジンが停止しました。'
end
end
class Car
def initialize
@engine = Engine.new
end
def start
@engine.start
puts '車が走り始めました。'
end
def stop
@engine.stop
puts '車が停止しました。'
end
end
Car
クラスが、Engine
クラスを持っているhas-a
の関係性になっています。
利点
- 小さなオブジェクトに切り分けられるので、単一の責任を保てる
- 他のクラスでも使用できるので、再利用性が高い
- 各クラスで柔軟にインターフェースを拡張できる
関係性の選択
-
is-a
なら、継承 -
behaves-like-a
(振る舞う)なら、ダックタイプ
9. 費用対効果の高いテストを設計する
テストがあることによって、自信を持って絶えず継続的にリファクタリングできます。
価値の高いテストを書く能力があれば、全体のコストを上げることなく、コードが正しく振る舞ってくれることを証明してくれます。
意図を持ったテスト
- テストの利点として、以下が一般的に言われている
- バグを減らす
- 文書になる
- 「最初」に書くことでアプリケーションの設計がより良いものになる
- ただ...
- テストをすることの真の目的は、コストの削減
- テストの記述・メンテナンス・実行に時間がかかれば、テストを書く必要はない
- だからと言って、テストをやめることではない
- 意図を理解し、何を、いつ、どのようにテストするかを知る必要がある
プライベートメソッドをテストする
パブリックメソッドは、必ずテストすると思いますが、ここは悩む方も多いと思います。
本書では以下のように記載がありました。
プライベートメソッドは決して書かないこと
書くとすれば、絶対にそれらのテストをしないこと
ただし、当然のことながら、そうすることに意味がある場合を除く
まとめ
こうして改めて学んでみると、「あ、あそこの実装は〇〇の設計になっている!」など、自分で少しずつ言語化できるようになりました。
言語化はできるようになってきたので、次は実装です。
自分が実装する際に、設計を意識して、楽しみつつ、変更に強いアプリケーションを作っていきます。
参考文献
最後までご覧いただき、ありがとうございました。