はじめに
最近、「良いコード/悪いコードで学ぶ設計入門」を読んでいるので、本書の中で自分が疑問に感じたポイントをQ&A形式で振り返ってみたいと思います。
この記事では、各セクションの簡単な説明の後に、[Q]で疑問に感じたこと、[A]で調べてわかったこと、そして[NOTE]で覚えておきたいことをまとめています。
なお、サンプルコードにJavaを使用していますが、言語を問わず広く有効な設計手法なので、他言語でも応用できる内容になっています。
第5~8章については、すでに別の記事にまとめているので、こちらもぜひご覧ください!
それでは、第9〜12章を振り返りながら、一緒に「良いコード/悪いコード」について学んでいきましょう!
ファーストクラスコレクション
ファーストクラスコレクションとは、コレクションに関連するロジックをカプセル化する設計パターンです。
次の2つの要素で構成するクラスとして設計します。
- コレクション型インスタンス変数
- 完全性を保証するようにコレクション型インスタンス変数を操作するメソッド
例えば、メンバーのコレクション List<Member>
をインスタンス変数に持つ Party
クラスを設計することで、コレクションを操作するロジックを分散させずに一箇所にまとめられます。
class Party {
static final int MAX_MEMBER_COUNT = 4;
private final List<Member> members;
Party() {
members = new ArrayList<Member>();
}
// 中略
/**
* メンバーを追加する
* @param newMember 追加したいメンバー
* @return メンバー追加後のパーティ
*/
Party add(final Member newMember) {
// 中略
final List<Member> adding = new ArrayList<>(members);
adding.add(newMember);
return new Party(adding);
}
// 中略
/** @return メンバーリスト。ただし要素の変更はできません。 */
List<Member> members() {
return members.unmodifiableList();
}
}
[Q]
このような unmodifiableList()
を使った不変な設計って、PHPでも実現できるの?
[A]
PHPには、unmodifiableList()
のように読み取り専用のリストを作るメソッドはサポートされていないため、代わりに配列のコピーを作って実現します。
ただし、この方法だと元のデータを守れますが、返した配列は自由に書き換えられてしまいます。
public function members(): array {
return array_map(fn($member) => clone $member, $this->members);
}
技術駆動パッケージング
パッケージの区切り方、フォルダの分け方も、注意しないと大きな問題になります。
以下のように設計パターンなど、技術的な特徴が似ているものどうしでフォルダ分け、パッケージ分けするのを技術駆動パッケージングと呼びます。
├── UseCases
│ ├── 在庫ユースケース.java
│ ├── 注文ユースケース.java
│ └── 支払いユースケース.java
├── Entities
│ ├── 入庫エンティティ.java
│ ├── 出庫エンティティ.java
│ ├── 買い物かごエンティティ.java
│ ├── 注文エンティティ.java
│ ├── 発注エンティティ.java
│ └── 請求エンティティ.java
└── ValueObjects
├── 安全在庫量.java
├── 在庫回転期間.java
├── 発注金額.java
├── 注文先.java
├── 請求金額.java
├── 割引ポイント.java
└── クレジットカード番号.java
業務上の概念を表すクラスを技術駆動パッケージングでフォルダ分けすると、本来強く関係し合うファイルどうしがバラバラになり混乱します。
業務クラスは以下のように、概念として強く関係し合うものどうしが一緒になるようフォルダ分けしましょう。
├── 在庫
│ ├── 在庫ユースケース.java
│ ├── 発注エンティティ.java
│ ├── 入庫エンティティ.java
│ ├── 出庫エンティティ.java
│ ├── 安全在庫量.java
│ ├── 在庫回転期間.java
│ └── 発注金額.java
├── 注文
│ ├── 注文ユースケース.java
│ ├── 買い物かごエンティティ.java
│ ├── 注文エンティティ.java
│ └── 注文先.java
└── 支払い
├── 支払いユースケース.java
├── 請求エンティティ.java
├── 請求金額.java
├── 割引ポイント.java
└── クレジットカード番号.java
[NOTE]
このように機能ごとにパッケージを分ける構成(Package by Feature)にすることで、在庫ユースケースでしか使われない安全在庫量クラスを package private
にでき、注文や支払いなど無関係なユースケースから参照される危険性がなくなります。
また、同じ分類どうしでまとまっているので、たとえば支払い関係に仕様変更が生じた場合、支払いフォルダ内のファイルを読みに行けば良くなります。
これにより、関連ファイルをあちこち探しまわる手間が低減します。
大雑把で意味が不明瞭な名前
大雑把で意味が不明瞭な名前によって生じる問題として、以下のようなものがあります。
社員A「今度の仕様変更で、開発中のECサイトに予約機能が追加される。予約のロジック追加が商品周りでも必要だと思うけど、どこに実装しよう?」
社員B「商品クラスがすでにあるじゃないか。商品クラスに実装しちまえよ」
「商品」という名が大雑把すぎて、商品に関するあらゆるロジックが実装できそうに見えてしまいます。
大雑把で意味がガバガバな名前は、あらゆるロジックを引きつけ、あっという間に巨大化する原因となります。
そうならないように、目的駆動名前設計を用いて、「入庫品」、「予約品」、「注文品」、「配送品」のように特定の業務目的の達成に特化した、意味の狭い名前をクラスに付与しましょう。
[NOTE]
利用規約には、サービスの取り扱いやルールが極めて厳密な言い回しで書かれており、目的に特化した名前の参考になります。
以下は、書籍で紹介されている架空のフリーマーケットサービスにおける利用規約の一部です。
購入者が商品購入手続きを完了した時点をもって、売買契約が締結されたものとします。
売買契約が締結した場合、出品者は当社にサービス利用料を支払うものとします。
サービス利用料は、売買契約が締結した時点の商品の販売価格に、販売手数料を乗じた金額となります。
これらを参考にすると、利用者を表すクラスは単に「ユーザー」クラスではなく「購入者」クラスや「出品者」クラスに分けることができます。
「動詞 + 目的語」のメソッド名に注意
以下のコードは、ゲーム内の敵を表現する Enemy
クラスです。
3つのメソッドの名前に着目してください。
class Enemy {
boolean isAppeared;
int magicPoint;
Item dropItem;
// 逃げる。
void escape() {
isAppeared = false;
}
// 魔法力を消費する。
void consumeMagicPoint(int costMagicPoint) {
magicPoint -= costMagicPoint;
if (magicPoint < 0) {
magicPoint = 0;
}
}
// 主人公らのパーティにアイテムを追加する。
// 追加できた場合はtrueを返す。
boolean addItemToParty(List<Item> items) {
if (items.size() < 99) {
items.add(dropItem);
return true;
}
return false;
}
}
魔法力を扱う consumeMagicPoint
は敵の関心事と考えられますが、addItemToParty
メソッドは主人公の所持品を取り扱っているので、敵の関心事とは明確に異なっています。
このように関心事が異なるメソッドは、addItemToParty
のように「動詞 + 目的語」形式の名前になる傾向があります。
関心事の異なるメソッドの混在を防ぐには、可能な限り動詞1語で済むよう名前設計やクラス設計を行いましょう。
具体的には、「動詞 + 目的語」のメソッドがある場合、目的語の概念を表現するクラスを作成し、そのクラスに動詞1語のメソッドを追加します。
実際に addItemToParty
を当てはめてみると、動詞1語で表現された add
メソッドを持つ、PartyItems
クラスになりました。
class PartyItems {
static final int MAX_ITEM_COUNT = 99;
final List<Item> items;
// 中略
PartyItems add(final Item newItem) {
// 中略
final List<Item> adding = new ArrayList<>(items);
adding.add(newItem);
return new PartyItems(adding);
}
}
[NOTE]
「動詞 + 目的語」メソッドと同様に、boolean
型を返すメソッドも、以下のように適切ではないクラスに定義されることがよくあります。
class Common {
// メンバーが混乱状態であればtrueを返す。
static boolean isMemberInConfusion(Member member) {
return member.states.contains(StateType.confused);
}
}
こうした場合は、以下の形に読み替えて、違和感がないか確認してみましょう。
クラス名 is~ .
クラス名 has~ .
クラス名 can~ .
おわりに
今回は、「良いコード/悪いコードで学ぶ設計入門」の第9~12章を題材に、自分が疑問に感じたポイントを紹介しました。
ファーストクラスコレクションや技術駆動パッケージング、大雑把な名前の問題、そしてメソッド名の設計など、いずれも日々の開発で役立つ内容でしたね!
なお、続きとなる第13~15章の記事も公開しているので、ぜひあわせてご覧ください!