トイレクラスで見る凝集度と責務
社内向けに凝集度と責務について発表しました。
正直、プレゼン形式での発表は慣れないので全然うまく話せませんでしたが、調べるためのインデックスができればいいくらいの気持ちと、LTにも慣れていきたいなと思ってやってます。
折角なのでブログにも少し残すことにしました。
さて、今回の発表では「トイレ」クラスを例に使いました。どこで見たのか忘れたのですが、「凝集度が低いとは、自分の家のトイレのレバーが100m先の人の家の居間にあるような状態」みたいな例を挙げている方がいて、その印象が強かったからです。
トイレクラスに入る前に先に凝集度と責務について確認します。
凝集度
関連性の強いデータとロジックだけを集めたクラスを凝集度が高いと言います。
現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 増田 亨
責務
責務は、ソフトウェアオブジェクトについて大雑把に記述したものです。責務には以下の3つの主要な項目が含まれます。
・オブジェクトが行う動作
・オブジェクトが持つ知識
・オブジェクトが他に影響を与える主要な判断
オブジェクトデザイン レベッカ・ワーフすブラック, アラン・マクキーン 4.1 責務とは何か
下記の記事も参考になります。
具体例でイメージを掴む〜トイレクラスの例〜
私が凝集度と責務を意識するようになった例から、トイレクラスを考えます。
トイレは下記のような知識?と動作をもつと考えられます。
- トイレはタンク(便器中央の水が張っているところ)を持つ。
- トイレは流す(タンクを空にし、新たな水を張る)ことができる。
低凝集な実装が行われる時は大体、2つ目の流す機能の方が注目されて実装されます。
object HomeHelper { // 対象のトイレのタンクを空にする def flushToToilet(toilet: Toilet): Unit = { toilet.tank.dequeueAll(_ => true) toilet.tank.enqueue(Water(1.0)) } // 対象のトイレに何かを put する def putSomethingToToilet(something: Any, targetToilet: Toilet): Unit = targetToilet.tank.enqueue(something) }
低凝集な実装でよく見るパターンは上記のような実装ですね。綺麗にデータクラスと、機能クラスが分かれています。最悪です。
上記は水洗トイレではなく、中世の欧州の方の貴族は道端で用を足していたと聞きますがそんな感じです。
まず、人間が床(メモリ)の上に用を足します。
それをなんか清掃作業員みたいな人がトイレって名前のついたバケツを持ってきて回収し、最後川に流すみたいな実装です。つまりこういうこと。
// 人がまず床に便をする val poops = person.defecation // 便回収用のバケツ準備する val poopBucket = Toilet(mutable.Queue(Water(1.0))) // バケツに便を回収する HomeHelper.putSomethingToToilet(poops, poopBucket) // バケツに溜まった便を川に流す。 HomeHelper.flushToilet(poopBucket)</pre>
もはやトイレでもなんでもないただのバケツですね。
次は、責務がおかしいトイレを考えましょう。スライドにも載せているんですが、イラスト屋に責務がおかしいトイレの画像があったのでそれを例に使いましょう。
この画像は明らかにトイレの責務がおかしいですね。トイレは便や尿を流すためのものであって、本をstackするのは明らかに責務違反です。ちなみにこの状態をコードで表現すると以下のようになります。
class Toilet { private val tank: mutable.Queue[Any] = mutable.Queue(Water(1.0)) // トイレに本を積む責務を持たせてしまっている private val bookShelf: mutable.Stack[Book] = mutable.Stack() def stackBook(book: Book): Unit = bookShelf.push(book) ... }
明らかにおかしいですが、現実世界のプロダクトではわかりにくいビジネス上の概念を扱うためによく起こります。
最後、高凝集かつ責務が正しいトイレを見ていきましょう。
class Toilet { // トイレは最初水を張っている状態 private val tank: mutable.Queue[Any] = mutable.Queue(Water(1.0)) // トイレに(茶色い)何かをputする def put(something: Any): Unit = tank.enqueue(something) // トイレを流す def flush(): Unit = { // タンクに入っているものを流し tank.dequeueAll(_ => true) // 新たな水を追加する tank.enqueue(Water(1.0)) } }
私たちが普段使っているトイレが表現できていると思います。
これならば、後続の開発者が見たときに「トイレ」がどういうものかを理解できると思います。