こんにちは。開発ブログ運営担当のktです。
先日、「オブジェクト設計スタイルガイド」という本を読みました。
この本では、オブジェクトを設計するうえでコードを読みやすく、メンテナンスしやすくするためのルールが数多く紹介されており、とても勉強になりました。
こうした本を読んだ直後は「実践してみよう!」と思うのですが、時間が経つと忘れてしまいがちです。そこで、備忘録も兼ねて私が特に気になったポイントをまとめておこうと思います。
各章ごとに「本のチャプター」「重要なポイント」「サンプルコード」「私の感想」という形式で紹介しています。
サンプルコードは Java で書いていますが、本書の内容は他のオブジェクト指向言語にも応用できる考え方です。
1章「オブジェクトを使ったプログラミング入門」
1.2「状態」
private 修飾子をつけても、同じクラス内の別インスタンス からはアクセスできる仕様になってる。
class Sample { private int value; public Sample(int value) { this.value = value; } public void compare(Sample other) { System.out.println("My value: " + this.value); System.out.println("Other value: " + other.value); // 他のインスタンスの private フィールドにアクセス可能 } } public class Main { public static void main(String[] args) { Sample s1 = new Sample(10); Sample s2 = new Sample(20); s1.compare(s2); // s1 から s2 の private フィールドにアクセス } }
なぜアクセスできるのか?
private
は「クラス単位」のアクセス制御であり、「インスタンス単位」ではない。- 同じクラス内であれば、どのインスタンスからでも
private
フィールドにアクセスできる仕様。
どう活かせるか?
equals()
メソッドやcompare()
メソッドの実装時に役立つ。- クラス内部でのデータ比較や、独自のオブジェクト管理ロジックを実装するときに使える。
✨感想
昔、Javaの資格を取った時に勉強したような気がしますが、実務ではあまり利用してこなかったので意外でした。オブジェクト設計とはあまり関係ないですが意外だったので載せました。きっと私以外にも勘違いしている人がいるはず。
2章「サービスの作成」
2.1「2種類のオブジェクト」
アプリケーションには通常2種類のオブジェクトが存在しており、タスクを実行したり情報を返すサービスオブジェクトと、もう一方はデータを保持し、データを操作したり取得する振る舞いを公開するオブジェクトがある。
1. サービスオブジェクト(Service Object)
- 役割: タスクを実行する/情報を返す。
- 特徴: アプリケーションの処理ロジックを持つが、状態(データ)はあまり持たない。
- 例:
EmailSender
:メール送信処理を行うOrderProcessor
:注文処理を実行するCustomerSearchService
:条件に応じて顧客を検索する
2. データオブジェクト(Entity / Value Object)
- 役割: データの保持と、そのデータに関する振る舞い(ロジック)を持つ。
- 特徴: 状態を持ち、操作や取得のためのメソッドを提供する。
- 例:
Customer
:名前や住所などを保持し、変更や取得のメソッドを持つOrder
:注文商品や合計金額を計算するロジックを持つ
🧠なぜこの区別が大事か?
- クラス設計の指針になる(「何をするオブジェクトか」が明確になる)。
- 責務がはっきりするため、保守性・拡張性が向上する。
- 単体テストがしやすくなる(サービスオブジェクトはスタブでテスト、データオブジェクトは状態検証など)。
💡実務で活かすヒント
- 新しいクラスを作るとき、「これはサービス?それともデータ?」と意識するだけで、設計がぶれにくくなる。
- コードレビューの時も「このクラスにサービスとデータの責務が混ざってない?」といった観点で議論できる。
- チーム内でこの考え方を共有しておくと、設計方針の統一にもつながる。
2.2「依存関係や設定値をコンストラクタ引数として渡す」
サービスはタスクを実行するために他のサービスを必要としており依存している。この依存しているサービスをコンストラクタで渡すと、サービスのインスタンス後にすぐに利用することができる。サービスに対してグローバルな設定オブジェクト全体を注入するのではなく、必要な値だけを取り出して渡すようにする。しかし、セットで扱うもの(例えばユーザー名とパスワード等)はそれ用の別のクラスを作成し、そこにセットしてサービスのコンストラクタに渡すようにするのがいい。
1. コンストラクタで依存を渡す
- 目的: サービスが必要とする他のサービス(依存)を、インスタンス化時に渡す。
- 利点:
- テストしやすい(モックやスタブを渡せる)
- 初期化が明確(DIコンテナでも扱いやすい)
- サービスが使える状態で確実に生成される
public class EmailNotificationService { private final EmailSender emailSender; public EmailNotificationService(EmailSender emailSender) { this.emailSender = emailSender; } public void notifyUser(String address, String message) { emailSender.send(address, message); } }
2. グローバル設定オブジェクトを避ける
- NGパターン:
AppConfig config
のような巨大オブジェクトを丸ごと渡す。 - 理由:
- どのプロパティを使ってるかがわかりにくい。
- 不要な依存が増えてしまう(テストが難しくなる)。
public class ReportService { private final AppConfig config; public ReportService(AppConfig config) { this.config= config; } public void output(List<String> msg) { int maxReportLines = config.getInt("maxReportLines"); // レポートを出力 } }
- OKパターン:必要な値だけ抽出して渡す。
public class ReportService { private final int maxReportLines; public ReportService(int maxReportLines) { this.maxReportLines = maxReportLines; } }
3. 複数の値を1つにまとめる
- 例:ユーザー名とパスワードなど、セットで使われるものは別クラスにする。
- 利点:
- 構造化されて扱いやすくなる。
- 意味が明確になる。
public class Credentials { private final String username; private final String password; public Credentials(String username, String password) { this.username = username; this.password = password; } // getterなど } public class AuthService { private final Credentials credentials; public AuthService(Credentials credentials) { this.credentials = credentials; } }
💡実務での活かし方
- 自分やチームのコードで「依存が隠れてないか」「巨大な設定オブジェクトをそのまま使ってないか」を見直してみる。
- DI(Dependency Injection)の考え方と合わせて、明示的な設計として使うと理解しやすくなる。
- 社内のレビュー時に「このサービス、何に依存してるかすぐ分かる?」という観点で見ると◎。
2.4「全てのコンストラクタ引数を必須とする」
オブジェクトが必要な依存や設定値は常にクラスのユーザーが提供するようにしましょうと記述されている。デフォルト値を持たない方がいいとなっている。
✅ この設計の狙い
- 未初期化のまま使われることを防ぐ。
- オブジェクトを生成した時点で、すぐに使える状態を保証する。
- どの値がどの目的か、コンストラクタの引数で明確にする。
❗デフォルト値を避ける理由
- 「意図して設定された値」と「デフォルトで入っただけの値」の区別が曖昧になる。
- テスト時にデフォルトが影響して予期せぬ結果になることがある。
- 状態の完全性を保証しにくくなる。
🤔 でも「デフォルト値を持ちたいケース」もある!
- たとえば:
- ページング処理で、デフォルトのページサイズが 20。
- リトライ回数が 3 回など、業務的に明らかな初期値がある。
- オプションの設定(使われる場面が稀だけどあってもよい)。
💡 折衷案・実践的なパターン
✅ Builderパターン or ファクトリーメソッドを使う
public class ReportConfig { private final int maxLines; private final String format; private ReportConfig(int maxLines, String format) { this.maxLines = maxLines; this.format = format; } public static ReportConfig createWithDefaults() { return new ReportConfig(100, "PDF"); } public static ReportConfig create(int maxLines, String format) { return new ReportConfig(maxLines, format); } }
→ ユーザーは意図的に「デフォルトでよい」と明示している形になるので、設計の意図も崩れません。
✅ 「設定は任意」ということを明示するクラス構造にする
例えば以下のようにオプションの設定値をラップする:
public class OptionalSettings { private final int retryCount; // default = 3 public OptionalSettings() { this.retryCount = 3; } public OptionalSettings(int retryCount) { this.retryCount = retryCount; } public int getRetryCount() { return retryCount; } }
✨感想
- 本の主張は「状態の不完全さを避けるための強いガイドライン」だと思います。
- ただし実際には、明確なデフォルト値が意味を持つケースも確実にあると思います。
- そういった場合は「意図してデフォルトを使う手段(ファクトリーメソッドやBuilder)」を用意することで、ガイドラインと実用性のバランスを取ることができると思います。
2.8「タスクに関するデータはコンストラクタではなくメソッド引数として渡す」
サービスは依存関係と設定値をすべてコンストラクタ引数として受け取る必要があるが、タスク(メソッド)に関する情報はメソッド引数で渡す。タスクに関する情報までコンストラクタで渡してしまうと、そのサービスは再利用できずに再度サービスのインスタンスが必要になる。
✅ この設計が意図していること
- サービスオブジェクトは、使い回しできる(ステートレス)状態を保つ。
- タスク(メソッド呼び出し)に必要な都度の情報は、メソッド引数で渡す。
- コンストラクタには「依存」と「設定値」だけ渡す。
❌ 悪い例(タスクのデータをコンストラクタで受け取ってしまっている)
public class ReportGenerator { private final List<String> data; public ReportGenerator(List<String> data) { this.data = data; } public void generate() { // dataを使ってレポート生成 } }
→ この場合、別のデータでレポートを作りたければ、また新しいインスタンスを作る必要が出てしまう。
✅ 良い例(タスクのデータはメソッド引数で受け取る)
public class ReportGenerator { public void generate(List<String> data) { // dataを使ってレポート生成 } }
→ これなら、1つのインスタンスを何度でも使い回せる!
🤔 もしコンストラクタにタスクデータを渡してしまうと?
- 毎回オブジェクトを作り直す羽目になり、無駄なインスタンス生成が増える。
- サービスのライフサイクルが短くなり、管理が面倒になる。
- テストがやりにくくなる(状態によって振る舞いが変わるから)。
💡実務での活かし方
- サービスを設計するとき、「これは何回も使い回せる設計になっているか?」を意識する。
- コンストラクタに渡すものとメソッド引数に渡すものをきちんと分ける。
- ステートレスなサービス設計を目指すと、結果的にテストや運用がとても楽になる。
2.10「コンストラクタの中ではプロパティへの代入以外は何もしない」と2.11「引数が無効な場合には例外を投げる」
サービスのコンストラクタでは引数が有効かチェックをして無効な場合は例外を投げることとプロパティへの代入以外の処理を行わないことがいい。
✅ 2.10「コンストラクタの中ではプロパティへの代入以外は何もしない」
- プロパティへの代入だけをする → オブジェクト生成は軽く、確実に成功するように。
- 外部リソース(ファイルシステム・DB・ネットワークなど)へのアクセスを禁止。
- 理由は、オブジェクト生成 = 副作用ゼロの操作にしたいから。
✅ 2.11「引数が無効な場合には例外を投げる」
- 引数にnullや不正な値が渡されたら、即座に例外を投げる。
- 無効なオブジェクトが生まれるのを防ぐ(Fail Fast)。
🔥 特に重要視される理由:「オブジェクト生成の副作用」
ここが最大のポイントです。
❗コンストラクタで副作用を起こすと・・・
- 例:
new FileLogger("/tmp/log")
を 生成しただけで/tmp/log
にディレクトリができる。 - もしそのあと
FileLogger
を使わなかったら、ゴミだけが残る。 - オブジェクトを作るだけで何か変わると、意図しない影響が発生する。
- テストコードでたくさんnewすると、ファイルやDBが汚れる。
- 例外が起きるタイミングが読みにくくなり、プログラム全体の予測可能性が落ちる。
🤔 でも「多少ならコンストラクタでやってもいいのでは?」という考え方
- 実務では、次のように考えることもできます。
- 「小さな検証だけ(ファイルパスの形式チェック程度)ならOK」
- 「リソース確保(例:ファイル作成)はメソッド呼び出し時にやるべき」
- つまり
- 「newしたら終わり」が理想
- どうしても必要なら「明示的な初期化メソッドを呼ばせる」
✨ 実務向きの折衷案
たとえば FileLogger
の場合:
import java.io.IOException; import java.nio.file.*; public class FileLogger { private final Path logFilePath; public FileLogger(String logFilePath) { if (logFilePath == null || logFilePath.isBlank()) { throw new IllegalArgumentException("logFilePath must not be null or blank"); } this.logFilePath = Paths.get(logFilePath); } public void writeLog(String message) throws IOException { // 副作用(ファイル・ディレクトリの作成)はこのタイミングで Files.createDirectories(logFilePath.getParent()); if (!Files.exists(logFilePath)) { Files.createFile(logFilePath); } Files.writeString(logFilePath, message + System.lineSeparator(), StandardOpenOption.APPEND); } }
コンストラクタは**「有効なlogFilePathを保持するだけ」**。
実際にディレクトリ作成などの副作用は、メソッド実行時に発生する。
LoggerFactoryにディレクトリ作成を任せるのもあり。
✨ Factoryを使うのが適しているケース
✅ オブジェクト生成に「手間」や「前処理」が必要な場合
- 例えば、Loggerを作るには「ディレクトリが存在するか確認して、なければ作る」必要がある。
- こういうオブジェクト生成時に前処理が必須なとき、Factoryに前処理を集めるとコードがすっきりする。
public class LoggerFactory { public static FileLogger createFileLogger(String filePath) throws IOException { Path path = Paths.get(filePath); Files.createDirectories(path.getParent()); return new FileLogger(path); } }
→ LoggerFactoryが、ディレクトリ作成という面倒ごとを担当。
FileLogger logger = LoggerFactory.createFileLogger("/tmp/myapp/logs/app.log"); logger.write("アプリケーション起動");
→ 呼び出し側は、Loggerの準備が完了している前提で使える!
3章「他のオブジェクトの作成」
3.1「一貫した振る舞いに最低限必要なデータを要求する」
位置を表すPositionクラスの例が挙げられており位置という概念にはxとyの両方の座標があることが必要で、コンストラクタでxとyを指定しないとインスタンスを生成することができないようにしておく。これは、コントラクタを使用したドメイン不変条件を保護する方法の例。
- オブジェクトには「成立条件(=不変条件)」がある。
- その不変条件を満たすために必ず必要なデータはコンストラクタで要求する。
- 必要なデータなしでは、オブジェクトを絶対に生成できないように設計する。
public class Position { private final int x; private final int y; public Position(int x, int y) { this.x = x; this.y = y; } // メソッド例 public int getX() { return x; } public int getY() { return y; } }
✨ これは「ドメイン不変条件を守る」ための鉄則
- ドメイン(業務・世界)で「成立しているべきルール(不変条件)」をプログラムでも常に守る。
- 不変条件を破るようなオブジェクトを作れない設計にしておく。
これによって、
- オブジェクト単体で一貫したふるまいを保証でき、
- システム全体もバグに強い設計になります。
🔥 逆に、やってはいけない例
public class Position { private int x; private int y; public Position() { // 空っぽのコンストラクタ } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } }
- インスタンス化直後はxもyも未設定(無効な状態)。
- 使う側が「xとyを両方設定したか?」を毎回意識する必要がある。
- バグの温床になる。
3.5「ドメイン不変条件が複数の場所で検証されるのを防ぐために新しいオブジェクトを抽出する」
String型のフィールドとしてメールアドレスを扱う場合、入力時にもメールアドレスが有効か検証するが更新する場合にも検証が必要で、複数の場所で検証ロジックが存在してしまうことになる。検証ロジックを別のメソッドに抽出する手段もあるが、別の方法としてメールアドレスを表すclassを作成し、そのコンストラクタで検証を行う方法が挙げられている。このclassをバリューオブジェクトという。
🔍 問題の背景:検証ロジックの重複
public class User { private String email; public void setEmail(String email) { if (!isValidEmail(email)) { throw new IllegalArgumentException("Invalid email format"); } this.email = email; } private boolean isValidEmail(String email) { // 正規表現で検証など } }
- 入力時に検証
- 更新時に再度検証
- 他の箇所でも同じ検証を繰り返す可能性
→ 同じ検証ロジックが散らばる=保守性が悪い
✅ 解決策:メールアドレスを表すクラスを作成する(=バリューオブジェクト化)
public class EmailAddress { private final String value; public EmailAddress(String value) { if (value == null || !value.matches("^[^@]+@[^@]+$")) { throw new IllegalArgumentException("Invalid email format"); } this.value = value; } public String getValue() { return value; } @Override public boolean equals(Object o) { // equals/hashCode 実装しておくと便利 } @Override public int hashCode() { // 略 } }
💡 補足:バリューオブジェクトとは?
- 値そのものに意味があるオブジェクト
- 不変(immutable)であるべき
- 比較は
equals()
で「値の等価性」を見る(インスタンスの同一性ではない)
3.8「依存関係は注入せず必要ならばメソッド引数として渡す」
サービスは依存関係にあるサービスはコンストラクタ引数として注入される必要があるが、それ以外のオブジェクトは値やバリューオブジェクト、またはそれらのリストのみを受け取るようにする。バリューオブジェクトが何かタスクを実行するためにサービスが必要な場合はメソッド引数として注入する。しかし、場合によってはメソッド引数としてサービスを渡す必要があるということは、その振る舞いをサービスとして実装すべきだと示唆されている。
🔍 背景:依存関係をどう扱うべきか?
サービスの設計原則
要素 | 扱い方 |
---|---|
恒常的に依存する他のサービス | コンストラクタで注入する(常に使うため) |
一時的に必要な処理(例えば他サービスのメソッド呼び出し) | メソッド引数で渡す(タスク実行時にのみ必要) |
単なるデータ・値・設定 | プリミティブ型やバリューオブジェクト、DTOとしてメソッド引数で渡す |
✅ バリューオブジェクトが「何か処理をする」場合
バリューオブジェクトが何らかのタスクを行うためにサービスが必要になるとき、
- その処理をバリューオブジェクトの中に書かず
- サービスとして切り出すのが正しい設計
📌 例:EmailAddressがメール送信処理をしようとするケース
❌ NGな設計(バリューオブジェクト内で処理)
public class EmailAddress { public void sendConfirmationEmail() { // ここでSMTPクライアントなどのサービスを使ってメールを送信… } }
→ 責務が重くなりすぎるし、テストもしづらい。
✅ OKな設計(サービスに処理を委ねる)
public class EmailService { public void sendConfirmationEmail(EmailAddress address) { // メール送信処理を行う } }
- EmailAddressはあくまでメールアドレスという値を保持・検証する責務だけ。
- EmailServiceが「メールを送る」という振る舞いを担当。
3.9「名前付きコンストラクタを使う」
インスタンスを返すpublic staticなメソッドを使う。単一または複数のプリミティブの値からオブジェクトを構築するケースで利用され、fromString()やfromInt()等のメソッドになる。ドメイン固有の用語を名前付きコンストラクタのメソッド名として使用する。例えば販売注文「SalesOrder」だったら注文を「出す(place)」を利用する。通常のコンストラクタメソッドはprivateにしておく。
✅ なぜ「名前付きコンストラクタ」が有効なのか?
🧩 問題点:普通のコンストラクタは意味が分かりづらい
new SalesOrder(customerId, productId, quantity);
これが「新規注文の作成」なのか、「再発注」なのか、「見積りからの変換」なのか分かりにくい。
複数のプリミティブ型の引数だと、間違って順番を入れ替えてもコンパイルが通ってしまう。
✅ 解決策:名前付きコンストラクタメソッドで明示的にする
public class SalesOrder { private SalesOrder(CustomerId customerId, ProductId productId, Quantity quantity) { // 通常のコンストラクタは private に } public static SalesOrder place(CustomerId customerId, ProductId productId, Quantity quantity) { return new SalesOrder(customerId, productId, quantity); } }
place
というメソッド名によって「これは注文を出すためのもの」と分かる。
ドメイン用語とメソッド名を合わせることで、意図と責務が明確になる。
引数のプリミティブ型も型クラス(例:CustomerId
, ProductId
)にしておけば、型安全で順序ミスも防げる。
✨感想
確かにコンストラクタをオーバーロードするより分かりやすいなと思う。
英語が不得意なためplace=注文を出すとは思わず「場所?」と思ってしまった。やはり英語力も必要。
3.10「プロパティフィラーを使用しない」
オブジェクトの名前付きコンストラクタでMapのデータを受け取って、それぞれのフィールドに対応した値をMapから取り出すようなメソッドは、便利そうに見えるがオブジェクトの内部が公開されてしまうので利用しない方がいい。例外としてデータ転送オブジェクトの場合は内部データを保護する必要はないのでプロパティフィラーを使用してもいい。
🧩 プロパティフィラーの例と問題点
public static User from(Map<String, Object> data) { User user = new User(); user.name = (String) data.get("name"); user.email = (String) data.get("email"); return user; }
一見便利に見えますが、次のような問題を含みます。
❌ 問題点
オブジェクト内部の構造(プロパティ名)を外部に公開してしまっている
name
やemail
という内部構造に依存するコードが外部に広がってしまう。- 変更しづらくなり、カプセル化が崩れる。
バリデーションが抜けやすくなる
User
の不変条件(例えば、email
が必ず有効形式であること)が保証されないまま、インスタンスが生成されてしまう。
IDE補完や型安全の恩恵を失う
- MapのkeyはStringなので、タイポによるバグが静的に検出されない。
✅ 例外:DTOはプロパティフィラーでもよい
DTO(Data Transfer Object)は、
- プレゼンテーション層(画面)
- 永続化層(DB)
- APIの入出力
などの**「外部とのやり取りのための構造体」**であり、純粋なデータの集まりとして扱います。よって、以下のような柔軟なデータ操作が許容されます。
public class UserDTO { public String name; public String email; public static UserDTO from(Map<String, Object> data) { UserDTO dto = new UserDTO(); dto.name = (String) data.get("name"); dto.email = (String) data.get("email"); return dto; } }
✨感想
そもそもプロパティフィラーって初耳でしたが言いたいことは分かりました。汎用的にしようとするとついやりがちですね。
3.12「コンストラクタをテストしない」
コンストラクタが無効な引数を受け取らないことをテストすることだけにする。テストするためだけに内部データを公開するゲッタを追加するのは、そのデータがテスト以外のクライアントで必要な場合のみにする。
コンストラクタを直接テストする必要はない。
- なぜなら、コンストラクタの主な仕事は依存の注入と不変条件の保証であり、それ以外は業務的なロジックを含まないのが理想だから。
テストでコンストラクタの正しさを確認したい場合、無効な引数に対して例外が投げられることのみを検証すればよい。
内部状態の確認のために getXxx() を作るのは、テスト以外にも必要な場合のみにする。
❌ やってはいけない例
public class Position { private final int x; private final int y; public Position(int x, int y) { if (x < 0 || y < 0) { throw new IllegalArgumentException("座標は正でなければならない"); } this.x = x; this.y = y; } // テストのためだけに追加されたゲッター(本番では使わない) public int getX() { return x; } public int getY() { return y; } }
✅ 推奨されるアプローチ
1. コンストラクタの例外処理だけテストする
@Test void invalidPosition_shouldThrowException() { assertThrows(IllegalArgumentException.class, () -> new Position(-1, 0)); }
2. オブジェクトの振る舞いを通じて間接的に状態を確認する
public class Position { private final int x; private final int y; public Position(int x, int y) { if (x < 0 || y < 0) { throw new IllegalArgumentException("invalid coordinates"); } this.x = x; this.y = y; } public boolean isRightOf(Position other) { return this.x > other.x; } }
@Test void shouldReturnTrueIfToTheRight() { Position p1 = new Position(5, 0); Position p2 = new Position(3, 0); assertTrue(p1.isRightOf(p2)); // ← 振る舞いを通してテスト }
前編は以上です。
この記事が参考になったという方は、ぜひX(https://twitter.com/softemcom)のフォローをお願いします!記事を公開した時にお知らせします。続きは後日公開予定ですが、「早く読みたい!」という方は、ぜひ本書を手に取ってみてください。学びが多く、おすすめです。
弊社に興味を持ってくださった方はこちら ☆彡
https://www.softem.com/recruitment/