カプセル化と情報隠蔽
第2章で、可視性修飾子を学びました。 この章では、なぜprivateを使うべきなのか、その考え方を見ていきます。
public propertyの危険性
次のStockクラスを見てください。 在庫数を表すクラスです。
class Stock
{
public int $quantity = 0;
}
$quantityはpublicなので、クラスの外から自由に値を書き換えられます。
$stock = new Stock();
$stock->quantity = -5;
在庫数が負になるという、業務上ありえない状態も、PHPの文法上は防げません。
Stockクラスがどこで使われているかにかかわらず、誰でもこの不正な代入ができてしまいます。
tell, don't ask
publicプロパティをprivateにしただけでは、この問題は解決しません。 次のように、ゲッターとセッターを用意しただけでは、外から自由に値を書き換えられる状態は変わらないからです。
class Stock
{
private int $quantity = 0;
public function getQuantity(): int
{
return $this->quantity;
}
public function setQuantity(int $quantity): void
{
$this->quantity = $quantity;
}
}
// 呼び出し側
if ($stock->getQuantity() >= 5) {
$stock->setQuantity($stock->getQuantity() - 5);
}
このコードでは、「在庫を5個減らせるか」という判断を、呼び出し側が行っています。 在庫を減らす処理を書く場所が増えるたびに、この条件チェックを複製することになります。 チェックを一箇所書き忘れれば、在庫は簡単に負の値になります。
同じことを、次のように書き換えられます。
class Stock
{
private int $quantity = 0;
public function reduce(int $amount): void
{
if ($this->quantity < $amount) {
throw new RuntimeException('在庫が不足しています');
}
$this->quantity -= $amount;
}
}
// 呼び出し側
$stock->reduce(5);
呼び出し側は「5個減らしてほしい」と命令するだけです。
在庫が足りているかどうかの判断は、Stockクラス自身のメソッドの中に移りました。
内部状態を尋ねて呼び出し側で判断する(ask)のではなく、オブジェクトに何をしてほしいかを伝える(tell)、という考え方をtell, don't askと呼びます1。 判断をオブジェクトの内側に置くことで、その判断がどこか一箇所に集まり、複製されなくなります。
ゲッター・セッターの要否判断
tell, don't askは、ゲッターやセッターを全て無くすべきだ、という意味ではありません。 Fowler自身も、この原則が行き過ぎて解釈され、あらゆる問い合わせメソッドを排除しようとする風潮には懸念を示しています2。
判断の目安は、そのメソッドが呼ばれた後、呼び出し側が何をするかです。
- 呼び出し側が戻り値を使って判断や計算をするなら、その判断や計算をオブジェクト自身のメソッドに移せないか検討します。上のreduceメソッドが、その例です
- 戻り値をそのまま表示・出力するだけなら、ゲッターで十分です。合計金額をログに出力するだけの処理に、専用の判断ロジックは要りません
まとめ
- publicプロパティは外部から自由に書き換えられるため、不正な状態を防げない
- ゲッター・セッターを用意しただけでは、判断が呼び出し側に残ったままになる
- tell, don't ask原則は、内部状態を尋ねて外部で判断する(ask)のでなく、オブジェクトに判断ごと委ねる(tell)という考え方
- ゲッター・セッターが不要という意味ではなく、判断や計算を伴う操作をtellスタイルに寄せるかどうかが分かれ目
次に読む
次章では、単一責任の原則を扱います。 god classの兆候と、責務を分割する実例を見ていきます。
練習問題
次のBankAccountクラスを、tell-don't-ask原則に沿って書き換えてください
class BankAccount
{
private int $balance = 0;
public function getBalance(): int
{
return $this->balance;
}
public function setBalance(int $balance): void
{
$this->balance = $balance;
}
}
// 呼び出し側: 出金処理
if ($account->getBalance() >= $amount) {
$account->setBalance($account->getBalance() - $amount);
} else {
throw new RuntimeException('残高が不足しています');
}
解答例
class BankAccount
{
private int $balance = 0;
public function withdraw(int $amount): void
{
if ($this->balance < $amount) {
throw new RuntimeException('残高が不足しています');
}
$this->balance -= $amount;
}
}
// 呼び出し側
$account->withdraw($amount);
残高が足りているかどうかの判断を、withdrawメソッドの中に移しました。
出金処理を書く場所が増えても、この判断を複製せずに済みます。