🐥

Humble Object Pattern とはなんだろか?

2024/02/23に公開

先日twadaさんが講演されていた実録レガシーコード改善 / Working with Legacy Code: the True Recordの資料内で Humble Object Pattern という用語が紹介されていました。

こちらの言葉は、Clean Architecture で紹介されているようですが、私自身がまだまだエンジニア歴が浅いこともありそのような上級者向けの本は読むのが気が引けてしまいました。

しかし、この Humble Object Pattern に興味があったので、インターネット上の情報をググりながら調べてみた内容を備忘録的にまとめていきたいと思います。

Humble Object Pattern とは

Humble Object Pattern とは、テストの実行が難しく、バグを起こしやすい構成になっているプログラムを修正するためのデザインパターンです。

テストが難しいプログラムというのは、さまざまなロジックが一箇所にまとまってしまっているケースに陥りがちです。

一例として、API 処理のファンクションの中で以下すべて行うことが挙げられるかと思います。

  • 外部 API から情報をフェッチ
  • カテゴリごとに集計
  • UI に表示するために文字列を加工

Humble Object Pattern では、このようなロジックは小さなパーツに可能な限り分割することで、それぞれの部品のテスト容易性を高めることが目的です。

ちょうどイメージは以下の絵のようになります。
image

Humble Object Pattern を使った例

では、具体的なコードで説明していきたいと思います。
上記 twada さんの講演でも紹介されていましたが、乱数を利用してその数字を元にメッセージを表示するケースを題材に考えてみたいと思います。

テストが難しいコード

ここでは具体的にテストが難しいコードとして以下の JavasScript のコードを考えてみたいと思います。なお、サンプルコードはこちらのリポジトリに準備しています。

export function say_random_message() {
  const messages = [
    "C is the origin of C++",
    "C++ is the best language",
    "Java is the great OOP language",
    "JavaScript is super cool",
    "Python is the most reasonable language",
    "Ruby is the spirit of Japanese",
  ];
  const index = Math.floor(Math.random() * messages.length);
  return messages[index];
}

console.log(say_random_message());

では、このコードのテストがなぜ難しいのでしょうか。具体的にこのコードがやっていることを整理してみましょう。

  • コードの内容
    • messages という変数に 6 個の具体的なメッセージを定義
    • index という変数で Math オブジェクトのメソッドを使い 0〜5 までの範囲で数字をランダムに定義
    • index に定義された数の messages を return する

上の内容を考えると、以下などのテストケースを作れそうです。

  • index に 0 が定義されれば、"C is the origin of C++"
  • index に 2 が定義されれば、"Java is the great OOP language"
    ...

ここで以下のようなテストコードを書いてみました。

import { expect, test } from "vitest";
import { say_random_message } from "./bad_case";

test("say_random_message generates a random message", () => {
  expect(say_random_message()).toBe("C is the origin of C++");
});

果たして、このテストは正常に通るでしょうか?結果は、通るかもしれないです。
というのもテスト実行時にsay_random_message内部でランダムに生成された数字によってメッセージが変化するので、結果を想定することはできません。

テスト実行時の一例は以下の通りです。

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  bad_case.test.js > say_random_message generates a random message
AssertionError: expected 'Java is the great OOP language' to be 'C is the origin of C++' // Object.is equality

- Expected
+ Received

- C is the origin of C++
+ Java is the great OOP language

 ❯ bad_case.test.js:5:32
      3| 
      4| test("say_random_message generates a random message", () => {
      5|   expect(say_random_message()).toBe("C is the origin of C++");
       |                                ^
      6| });
      7| 

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  21:30:10
   Duration  9ms


 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit

これがテストが難しいコードの一例と言えるかと思います。

改善してみたコード

では、ここで Humble Object Pattern に基づいて可能な限りロジックを分割するリファクタリングを行ってみました。

export function random_int(max, min) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function get_message(index) {
  const messages = [
    "C is the origin of C++",
    "C++ is the best language",
    "Java is the great OOP language",
    "JavaScript is super cool",
    "Python is the most reasonable language",
    "Ruby is the spirit of Japanese",
  ];
  return messages[index];
}

export function say_random_message() {
  const index = random_int(5, 0);
  return get_message(index);
}

上記のコードではテストが難しいコードのsay_random_message内部に所属していた以下のロジックを別々の関数に分けました。

  • 指定された範囲の乱数を生成する
  • 指定された数値に基づいてメッセージを返す

このようにロジックを分割することでテスト容易性が高まり、テストが書きやすくなっています。
具体的にget_messageでテストを書いてみます。

import { expect, test } from "vitest";
import { say_random_message } from "./bad_case";

test("say_random_message generates a random message", () => {
  expect(say_random_message()).toBe("C is the origin of C++");
});

結果は無事に以下のように通りました。

stdout | good_case.js:22:9
Java is the great OOP language

 ✓ good_case.test.js (1)
   ✓ get_message generates a message by index

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  21:38:10
   Duration  106ms (transform 12ms, setup 0ms, collect 9ms, tests 1ms, environment 0ms, prepare 33ms)


 PASS  Waiting for file changes...

Humble Object Pattern の由来 (わからなかった)

最後に本題とは少し離れるのですが、なぜ Humble Object Pattern と言われるのか言葉の由来が気になったので少し調べてみました。
Humble = 謙虚な という英語なので、謙虚なオブジェクト。。。ということですが、これはどういう意味なのだろうかと疑問に感じました。

そこで、Clean Architecture の著者である Martin Fawler さんのウェブサイトを参照しこちらのページに行き当たりました。

この Humble Object Pattern ですが、もともとは Michael Feathers という方が 20 年以上前に記事にされた The Humble Dialog Box という考えが起源になっているようです。

この The Humble Dialog Box は以下のように説明されています。

  • 原文

the idea of dealing with a hard-to-test GUI behavior by minimizing the behavior within the GUI element, moving as much as possible to a separate object that’s easier to test.

  • 日本語意訳

テストしにくいGUIの動作を対処するため、GUI要素内の動作を最小限に抑え、テストしやすい個別のオブジェクトにできるだけ分割するという考え方です。

そして、

This idea was generalized into the Humble Object pattern.

とあるように今日では Humble Object Pattern として知られているようです。
The Humble Dialog Box の記事も簡単に目を通したのですが、なぜ Humble という単語が使われているのかはわからずにおります。。。(英語のニュアンスなどなのでしょうか)

終わりに

この Humble Object Pattern は Clean Architectureという書籍の中で取り上げられているデザインパターンです。しかし、本記事はこちらの書籍の内容を読まずにインターネット上の情報からまとめています。

また、筆者自身がエンジニア経験が決して長いわけではないため、理解が誤っているかもしれないと認識しています。
色々調べたつもりで執筆しましたが、もし認識ずれやサンプルコードは Humble Object Pattern に則っていないなどあればコメントで教えていただけると幸いです。

今後更なる理解を深めるためにも Clean Architecture へチャレンジできればと思っています。

参照

https://martinfowler.com/bliki/HumbleObject.html
https://speakerdeck.com/twada/working-with-legacy-code-the-true-record?slide=43

Discussion