はじめに
最近、「オブジェクト設計スタイルガイド」を読んでいるので、本書の中で自分が疑問に感じたポイントをQ&A形式で振り返ってみたいと思います。
この記事では、各セクションの簡単な説明の後に、[Q]で疑問に感じたこと、[A]で調べてわかったこと、そして[NOTE]で覚えておきたいことをまとめています。
なお、サンプルコードにPHPを使用していますが、言語を問わず広く有効な設計手法なので、他言語でも応用できる内容になっています。
1~3章については、すでに別の記事にまとめているので、こちらもぜひご覧ください!
それでは、4〜6章を振り返りながら、一緒に「オブジェクト設計」について学んでいきましょう!
エンティティ
エンティティは、アプリケーションの中核となるオブジェクトです。
予約、注文、請求書、製品、顧客など、ビジネスドメインの重要な概念を表します。
エンティティは開発者がそのビジネスドメインについて得た知識のモデルです。
エンティティは、そのモデルに関連するデータを保持します。
加えて、そのデータを操作する方法を提供し、そのデータに基づいた有用な情報を公開する場合もあります。
class User
{
private string $id; // 識別子
private string $name;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
public function getId(): string
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function changeName(string $newName): void
{
$this->name = $newName;
}
}
エンティティは時間の経過とともに変化しますが、変化するのは常に同じオブジェクトであるべきです。
そのため、エンティティは識別可能である必要があります。
識別子($id
)は、エンティティのリポジトリでオブジェクトを保存するために使用できます。
のちほど、同じ識別子を使ってリポジトリからオブジェクトを取得し、再び修正できます。
// まずUserを作成し、保存する。
$user = new User("1234", "Taro");
$this->userRepository->save($user);
// その後、再度取得し、さらに変更を加える。
$user = $this->userRepository->getBy("1234");
$user->changeName("Jiro");
$this->userRepository->save($user);
エンティティの状態が時間とともに変化することを考えると、エンティティはミュータブルオブジェクトです。
[NOTE]
エンティティは、「同じ内容でも“別物”として区別すべきもの」として扱います。
例えば、識別子がないと次のようなコードが区別できなくなります。
$user1 = new User("Taro");
$user2 = new User("Taro");
同じ名前の「Taro」ですが、実際には別人の場合があります。
この場合、区別がつかなくなってしまいます。
そこで、識別子を追加して次のように書くことで、同じ内容でも別物として扱えるようになります。
$user1 = new User("1234", "Taro");
$user2 = new User("5678", "Taro");
バリューオブジェクト
バリューオブジェクトはまったく異なるものです。
バリューオブジェクトもドメインの概念を表す場合もありますが、その場合は、エンティティの一部または一側面を表します。
+ class UserId
+ {
+ // ...
+ }
+ class UserName
+ {
+ // ...
+ }
class User
{
- private string $id;
- private string $name;
+ private UserId $id;
+ private UserName $name;
- public function __construct(string $id, string $name)
+ public function __construct(UserId $id, UserName $name)
{
$this->id = $id;
$this->name = $name;
}
- public function getId(): string
+ public function getId(): UserId
{
return $this->id;
}
- public function getName(): string
+ public function getName(): UserName
{
return $this->name;
}
- public function changeName(string $newName): void
+ public function changeName(UserName $newName): void
{
$this->name = $newName;
}
}
バリューオブジェクトは、プリミティブ型の値をラップしたあらゆるイミュータブルオブジェクトです。
もし、イミュータブルオブジェクトをほかの値に変換したければ、Money
クラスの add
メソッドのように、新しいコピーを作成し、それが変更後の値を表すようにすべきです。
class Money
{
private int $amount; // 金額
private string $currency; // 通貨コード
public function __construct(int $amount, string $currency)
{
if ($amount < 0) {
throw new InvalidArgumentException("金額は0以上である必要があります");
}
$this->amount = $amount;
$this->currency = $currency;
}
// ...
// 等価性の判定
public function equals(Money $other): bool
{
return $this->amount === $other->amount &&
$this->currency === $other->currency;
}
public function add(Money $other): Money
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException("通貨が異なるため加算できません");
}
return new Money($this->amount + $other->amount, $this->currency);
}
}
バリューオブジェクトは、ドメインの概念を表すだけではありません。
アプリケーションのどこにでも現れます。
[NOTE]
バリューオブジェクトは、「中身が同じなら“同じもの”とみなす」オブジェクトです。
エンティティとは異なり、バリューオブジェクトは識別できる必要はありません。
バリューオブジェクトは、新しい型を追加するようなもので、int
や string
のようなプリミティブ型と同じように、値が等しければ同一のものとして扱います。
$price = new Money(1000, "JPY");
$fee = new Money(1000, "JPY");
$salary = new Money(5000, "JPY");
var_dump($price->equals($fee)); // true
var_dump($price->equals($salary)); // false
名前付きコンストラクタを持つ例外クラス
名前付きコンストラクタを使用すると、クライアント側のコードが非常にすっきりします。
final class CouldNotPersistObject extends RuntimeException
{
public static function becauseDatabaseIsNotAvailable(): CouldNotPersistObject
{
return new CouldNotPersistObject(/* ... */);
}
public static function becauseMappingConfigurationIsInvalid(): CouldNotPersistObject
{
return new CouldNotPersistObject(/* ... */);
}
// ...
}
こうすることで、例外メッセージをクライアント側にハードコーディングする必要がなくなります。
try {
- throw new RuntimeException("データベースに接続できません");
- } catch (RuntimeException $e) {
+ throw CouldNotPersistObject::becauseDatabaseIsNotAvailable();
+ } catch (CouldNotPersistObject $e) {
echo $e->getMessage();
}
[NOTE]
実行時例外の名前を考える際、「すいません、...でした(Sorry, [I] ...)」という文を考えることが非常に役立ちます。
「...」の部分に入る言葉を、例外クラスの名前にします。
これは、システムが要求された仕事をどのように実行し、その結果うまく終了できなかったのかを伝えるので、良い名前となります。
たとえば、CouldNotFindProduct
、CouldNotStoreFile
、CouldNotConnect
などです。
おわりに
今回は、「オブジェクト設計スタイルガイド」の4~6章を題材に、自分が疑問に感じたポイントを紹介しました。
今回の内容を意識すると、オブジェクトの扱いや例外の設計を整理しながら、より安全で読みやすいコードを目指せます!
なお、続きとなる1~3章の記事も公開しているので、ぜひあわせてご覧ください!