newはいつ使っていいのか
「コンストラクタでnewするな」
ぼくがプログラミングを学び始めた頃、先輩エンジニアからそう教わった。依存性注入(DI)の原則だ。オブジェクトは外から渡せ。テストが書きやすくなる。結合度が下がる。差し替えが容易になる。
真面目なぼくは、その教えを守った。Validatorも、Calculatorも、全てコンストラクタの引数で受け取るようにした。Factoryを作り、DIコンテナを設定した。
ところが、ある日Reactのコードを見て混乱した。
function Parent() {
return (
<div>
<Header />
<MainContent />
<Footer />
</div>
);
}
Reactは平気で子コンポーネントをインラインに「生成」している。これは「newするな」に真っ向から反しているように見える。しかしReactは世界で最も成功したUIライブラリの一つだ。
どちらが正しいのか。
「newは悪」の起源
この問いに答えるには、まず「newは悪」という教えの起源を知る必要がある。
2008年頃、GoogleのテストエンジニアであるMisko Heveryが「new is glue」という概念を提唱した。
“Every time you use new, you’re gluing your code to a specific implementation.” (newを使うたびに、コードを特定の実装に接着している)
これはテスト駆動開発の文脈から生まれた考え方だ。テストを書くとき、外部依存をモックに差し替えたい。しかしコンストラクタ内でnewしていると、差し替えができない。だから「newするな」となった。
この教えは広く受け入れられた。Spring、Guice、Dagger等のDIフレームワークが普及し、「依存は全てコンストラクタで注入」が標準になった。
しかし、ここで重要なニュアンスが失われた。
Hevery自身も「全てのnewが悪い」とは言っていない。問題なのは「テストを困難にするnew」であり、Value ObjectやPure Logicのnewは別の話だ。
Google Testing Blogにはこうある。
“Leave newables in the constructor. Inject injectables.” (newableはコンストラクタに残せ。injectableは注入せよ)
newable(newしてよいもの)とinjectable(注入すべきもの)は区別されるべきだった。しかし「newするな」という単純なルールだけが一人歩きした。
Pure Logicと外部依存
では、どのnewがテストを困難にし、どのnewが問題ないのか。
ぼくはこう考えている。判断基準は「モックが必要かどうか」だ。
Pure Logicとは: - 入力のみに依存する - 副作用がない(ファイル、ネットワーク、DBを触らない) - 決定論的(同じ入力に対して常に同じ出力)
Pure Logicをテストするのに、モックは必要ない。
class PriceCalculator:
def __init__(self, tax_rate: Decimal):
self._tax_rate = tax_rate
def calculate(self, base_price: Decimal) -> Decimal:
return base_price * (1 + self._tax_rate)
def test_price_calculator():
calc = PriceCalculator(tax_rate=Decimal("0.1"))
assert calc.calculate(Decimal("1000")) == Decimal("1100")
これは完全に決定論的だ。外部に依存しない。モック不要。
一方、外部依存とは:
-
I/O(ファイル、ネットワーク、データベース)
-
非決定論的な要素(時刻、乱数)
外部依存をテストするには、モックが必要だ。
class PaymentProcessor:
def __init__(self, api_client: PaymentApiClient):
self._api = api_client
def process(self, order: Order) -> PaymentResult:
return self._api.charge(order.total)
PaymentApiClientは実際にネットワーク通信を行う。テストでは本物のAPIを叩きたくない。だからモックが必要。だからDI必須。
時刻や乱数も同様だ。
class TokenGenerator:
def __init__(self, clock: Clock, random: Random):
self._clock = clock
self._random = random
def generate(self) -> Token:
timestamp = self._clock.now()
random_part = self._random.hex(16)
return Token(f"{timestamp}-{random_part}")
ClockとRandomは非決定論的。テストで再現性を確保するにはモック/スタブが必要。だからDI必須。
シンプルなルール
ここから導かれるルールはシンプルだ。
| カテゴリ | 内部生成OK | DI必須 |
|---|---|---|
| Validator, Calculator | ✅ | |
| Formatter, Parser | ✅ | |
| Value Object | ✅ | |
| Repository | ✅ | |
| API Client | ✅ | |
| File System | ✅ | |
| Clock, Random | ✅ |
言い換えると:
許可(内部生成OK):
-
Pure Logic(Validator, Calculator等)
-
設定が必要な場合はConfigオブジェクト経由
禁止(DI必須):
-
外部リソース(Store, Transport, API)
-
非決定論的(Clock, Random)
具体例
具体的なコードで見てみよう。ポイントは「値」と「サービス」の区別だ。
値 vs サービス
まず、この2つの違いを明確にしたい。
# これは「値」- ただのデータ、振る舞いを持たない
tax_rate = Decimal("0.1")
max_items = 100
# これは「サービス」- メソッドを持ち、I/Oを行う
repository.save(order) # DBに書き込む
api_client.charge(amount) # HTTPリクエストを送る
clock.now() # 現在時刻を取得する
「値」はテストで困らない。Decimal("0.1")は常に0.1だ。 「サービス」はテストで困る。本物のDBやAPIを叩きたくない。
Python
class OrderProcessor:
def __init__(
self,
tax_rate: Decimal, # 値: ただの数値
repository: OrderRepository, # サービス: DBアクセス
payment_api: PaymentApiClient, # サービス: HTTP通信
):
# Pure Logicは内部生成OK
# tax_rateは「値」なので、これを使ってnewしても問題ない
self._calculator = PriceCalculator(tax_rate)
# 設定不要のPure Logicはそのままnew
self._validator = OrderValidator()
self._formatter = ReceiptFormatter()
# サービスはDIで受け取る
# これらは「振る舞い」を持ち、I/Oを行う
self._repository = repository
self._payment_api = payment_api
def process(self, order: Order) -> ProcessResult:
# Pure Logic: 単純な計算、モック不要
if not self._validator.is_valid(order):
return ProcessResult.invalid()
total = self._calculator.calculate(order.subtotal)
# サービス: I/Oが発生、テストではモックする
payment = self._payment_api.charge(order.customer_id, total)
if payment.is_success:
self._repository.save(order.with_payment(payment))
return ProcessResult.from_payment(payment)
ポイント:
-
tax_rate: Decimal→ 値。振る舞いがない。そのまま使える -
OrderValidator()→ Pure Logic。設定不要なら引数なしでnew -
repository,payment_api→ サービス。I/Oを行う。DI必須
TypeScript
class OrderService {
private readonly calculator: PriceCalculator;
private readonly validator: OrderValidator;
constructor(
taxRate: number, // 値: ただの数値
private readonly repository: OrderRepository, // サービス: DB
private readonly paymentGateway: PaymentGateway, // サービス: API
) {
// Pure Logicは内部生成
this.calculator = new PriceCalculator(taxRate); // 値を渡すだけ
this.validator = new OrderValidator(); // 設定不要
}
async createOrder(input: CreateOrderInput): Promise<Order> {
// Pure Logic: 同期的、決定論的、モック不要
if (!this.validator.isValid(input)) {
throw new ValidationError("Invalid order");
}
const total = this.calculator.calculate(input.items);
// サービス: 非同期、I/O、テストではモック
const order = Order.create({ ...input, total });
await this.repository.save(order);
await this.paymentGateway.authorize(order.id, total);
return order;
}
}
なぜ区別できるのか
見分け方はシンプルだ。
| 受け取るもの | 特徴 | 例 |
|---|---|---|
| 値 | メソッドを呼ばない、または純粋な計算のみ | Decimal, string, number, List[Rule] |
| サービス | I/Oを行うメソッドを持つ | Repository, ApiClient, FileSystem |
tax_rate: Decimalを受け取っても、それは「ただの数値」だ。DecimalにはI/Oを行うメソッドがない。
repository: OrderRepositoryを受け取ると、それは「サービス」だ。repository.save()はDBに書き込む。テストで本物のDBを使いたくないから、モックに差し替える必要がある。
なぜこれで良いのか
このルールが機能する理由を整理しよう。
1. テスト可能性
Pure Logicはそれ自体を単体テストできる。親クラスでモックする必要がない。
def test_order_validator():
validator = OrderValidator()
assert not validator.is_valid(Order(items=[])) # 空の注文は無効
def test_price_calculator():
calculator = PriceCalculator(tax_rate=Decimal("0.1"))
assert calculator.calculate(Decimal("1000")) == Decimal("1100")
OrderProcessorのテストでOrderValidatorをモックする必要はない。OrderValidatorは別途テストされている。サービスだけモックすればいい。
def test_order_processor():
# サービスはモック
mock_repository = Mock(spec=OrderRepository)
mock_payment_api = Mock(spec=PaymentApiClient)
mock_payment_api.charge.return_value = PaymentResult.success()
# Pure Logicはモック不要(tax_rateは値)
processor = OrderProcessor(
tax_rate=Decimal("0.1"),
repository=mock_repository,
payment_api=mock_payment_api,
)
result = processor.process(valid_order)
assert result.is_success
2. シンプルさ
呼び出し側は「値」と「サービス」だけ渡せばいい。内部でどんなPure Logicを使うかは意識しなくていい。
processor = OrderProcessor(
tax_rate=Decimal("0.1"), # 値
repository=repository, # サービス
payment_api=payment_api, # サービス
)
OrderValidatorやReceiptFormatterの存在すら知らなくていい。それらはOrderProcessorの内部実装だ。
3. 結合度
Pure Logicとの結合は意図的だ。OrderProcessorはOrderValidatorを使う。これは仕様の一部。
ロジックを変えたいなら、コードを変えればいい。それは「依存の差し替え」ではなく「ロジックの変更」だ。
4. Factoryでの交換
「でも将来差し替えたくなったら?」
本当に差し替えが必要になったら、その時にリファクタリングすればいい。YAGNIの原則だ。
「いつか使うかも」でFactoryを作るのは過剰設計。必要になってから作る方が、実際の要件に合ったものが作れる。
Reactはなぜ許されるのか
ここで、冒頭のReactの疑問に戻ろう。
Reactでは子コンポーネントをインラインに生成する:
function Parent() {
return (
<div>
<Header />
<MainContent />
<Footer />
</div>
);
}
これは「newは悪」に反するように見える。しかし、実際には同じ原則に従っている。
Reactコンポーネントは、多くの場合Pure Logicだ。入力(props)のみに依存し、副作用なく、決定論的にJSXを返す。
function Header({ title }) {
return <h1>{title}</h1>;
}
propsのみに依存。副作用なし。決定論的。これはPure Logicだ。
外部依存が必要な場合は、Reactは別の仕組みを用意している。
function UserProfile() {
const user = useQuery(['user'], fetchUser);
return <div>{user.name}</div>;
}
useQueryでAPI呼び出しを行う。これはコンポーネントの「外」で管理される。テストではこの部分をモックできる。
つまり、Reactも「Pure Logicは内部生成OK、外部依存は別管理」というルールに従っている。表現が違うだけで、本質は同じだ。
反論への応答
「将来差し替える可能性があるから、全部DIにすべき」
YAGNIの原則に反する。
「いつか差し替えるかも」という可能性のために、今コードを複雑にするのは過剰設計だ。
Pure Logicを差し替えたくなるケースは稀だ。バリデーションルールを変えたいなら、コードを変えればいい。それは「依存の差し替え」ではなく「ロジックの変更」だ。
本当に差し替えが必要になったら、その時にリファクタリングすればいい。現代のIDEはリファクタリングをよくサポートしている。
「単一責任の原則に反する」
責任の定義による。
「オブジェクトの生成」は責任と言えるか?Pure Logicの場合、それは「設定の適用」に近い。
OrderProcessorがOrderValidatorを内部でnewするとき、それは「Validatorを生成する責任」ではなく「設定に基づいてValidatorを構成する責任」だ。後者はOrderProcessorの責任範囲内。
「テストしにくくなる」
逆だ。
Pure Logicは単体テストできる。親クラスのテストでモックする必要がない。テストの総量は減る。
外部依存はDIで注入するので、モック可能。テスト可能性は保たれる。
チームへの導入
このルールをチームに導入するための具体的なステップを示そう。
Step 1: 現状の棚卸し
コンストラクタで何をnewしているか、何をDIで受け取っているかを洗い出す。
Step 2: 分類
各依存を「Pure Logic」と「外部依存」に分類する。判断基準は「テストでモックが必要か」。
Step 3: Pure Logicの過剰DIを解消
Pure Logicなのに過剰にDIしているものを見直す。内部生成に切り替え、コードをシンプルにする。
Step 4: 外部依存のDI化
外部依存なのにDIしていないものがあれば対応する。テスト可能性を確保する。
判断に迷ったら:
「このクラスのテストを書くとき、モックが必要か?」
必要なら → DI
不要なら → 内部生成OK
まとめ
「newは悪」は、文脈を無視した過度な単純化だ。
本質は「テスト可能性を損なうか否か」であり、それは「モックが必要か否か」で判断できる。
| 受け取るもの | 性質 | 結論 |
|---|---|---|
| なし | Pure Logic(設定不要) | new Validator() でOK |
| 値(Decimal, string等) | データ、振る舞いなし | 値を渡して new Calculator(rate) でOK |
| サービス(Repository等) | I/Oあり、振る舞いあり | DI必須 |
| 非決定論的(Clock, Random) | 再現性なし | DI必須 |
このシンプルなルールで、テスト可能性とコードのシンプルさを両立できる。
Reactがインラインにコンポーネントを生成しても問題ないのは、コンポーネントがPure Logicだからだ。外部依存はhooksで別管理している。
「全部DIにしろ」という教条から解放されよう。必要なものだけDIにする。それが現実的で、かつテスト可能なアプローチだ。