技術ブログ

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}")

ClockRandomは非決定論的。テストで再現性を確保するにはモック/スタブが必要。だからDI必須。


シンプルなルール

ここから導かれるルールはシンプルだ。

カテゴリ内部生成OKDI必須
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,   # サービス
)

OrderValidatorReceiptFormatterの存在すら知らなくていい。それらはOrderProcessorの内部実装だ。

3. 結合度

Pure Logicとの結合は意図的だ。OrderProcessorOrderValidatorを使う。これは仕様の一部。

ロジックを変えたいなら、コードを変えればいい。それは「依存の差し替え」ではなく「ロジックの変更」だ。

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の場合、それは「設定の適用」に近い。

OrderProcessorOrderValidatorを内部で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にする。それが現実的で、かつテスト可能なアプローチだ。