はじめに
service 層のメソッドを static に統一するか、instance に統一するか、それとも混在を許容するか。
あるいは、共通処理を utils に切り出すべきなのか、それとも service 層に残すべきなのか。
多くのチームで一度はこのような議論が展開されたことがあるのではないでしょうか。
- 「状態を持たない処理なのだから、static メソッドにした方が良いのでは?」
- 「将来的に差し替える可能性があるのだから、instance メソッドにしておくべきでは?」
- 「共通処理は utils にまとめた方が良い」
- 「共通処理ではあるが、ドメイン依存のものまで utils に置くのは違和感がある」
これらは一見すると「コードスタイルの違い」に思えますが、実際には「設計方針や運用ルールの違い」が背景にあります。
本記事では、
- static メソッドと instance メソッドの使い分け
- service 層と utils の責務分担
といったテーマを「観点別」に整理し、チームとして合意形成していくための考え方について、まとめてみました。
なお、本記事は「どれが正解か」を決めることを目的としたものではなく、あくまでもチームやプロジェクトに合った方針を決める「考え方」について、整理したものとなります。
この記事を通して、メソッドの種類や配置場所をどのように扱うべきか、改めて見つめ直すきっかけになれば幸いです。
観点の整理
まずは、「static にすべきか」「utils に切り出すべきか」といった判断をしていくための観点を整理していきましょう。
本記事では、大きく以下の二つの軸で考えていきます。
-
メソッドの形態
- static メソッドにするのか
- instance メソッドにするのか
-
処理の配置場所
- service 層に残すのか
- utils に切り出すのか
これらの選択は単なる好みの話ではなく、設計や運用に直結する判断です。
例えば static メソッドはテストの容易性や依存関係の制御に影響を与えますし、utils に置くかどうかはドメイン知識の所在やコードの見通しやすさに関わります。
以下では、それぞれの「メリット・デメリット」「判断の基準となる観点」を整理しながら、それらの立ち位置について考えていきます。
static メソッドと instance メソッド
まずはメソッドの形態、「 static / instance どちらにすべきか」という観点で整理していきましょう。
基本的な内容の確認になりますが、これらの形態の選択は テスト容易性・依存関係・拡張性といった設計上の要素に直結します。
static メソッドの特徴
メリット
-
呼び出しがシンプル
- インスタンスを生成せずに呼び出せるため、補助的な処理や純粋関数といった処理を実装しやすい
-
依存を持たない処理に適している
- 計算やフォーマットなど、外部の状態に依存しない処理であることを明示できる
-
パフォーマンス上のオーバーヘッドが少ない
- インスタンス生成のコストが不要
デメリット
-
差し替え・モックが難しい
- static 呼び出しは依存の差し替えがしづらいため、テストでスタブ化したいケースには不向き
-
依存関係が隠れやすい
- 内部で外部 API や DB に依存していても、static の呼び出しだけを見ると気づきにくい
staticメソッド例(補助的な純粋処理)
// utils/date-util.ts
export class DateUtil {
static formatHyphen(date: Date): string {
return date.toISOString().split("T")[0]; // "2025-08-18"
}
}
// 呼び出し
const today = DateUtil.formatHyphen(new Date());
instance メソッドの特徴
メリット
-
DI(依存性注入)と相性が良い
- コンストラクタ経由で依存を注入できるため、モック化や差し替えが容易
-
将来的な拡張に強い
- 状態や依存関係を持たせる余地があるため、要件変更に対して柔軟に対応できる
-
テストしやすい
- 依存を差し替えられるため、ユニットテストや統合テストの設計がシンプルになる
デメリット
-
記述が冗長になりやすい
- 呼び出し前にインスタンスを生成する必要があるため、補助的な処理の実装では煩雑に感じられることもある
-
状態を持たない処理には過剰
- 純粋に入力から出力を返すだけの関数には、わざわざ instance 化する意義が薄い
instance メソッドの例(依存を注入するサービス)
// services/user-service.ts
import { UserRepository } from "../repositories/user-repository";
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getUser(id: string) {
return this.userRepository.findById(id);
}
}
// 呼び出し(依存を差し替え可能)
const service = new UserService(new UserRepository());
const user = await service.getUser("u123");
判断の観点
static と instance をどう使い分けるかは、以下の観点で考えると整理しやすいです。
| 観点 | static | instance |
|---|---|---|
| 依存関係を持つかどうか | 依存を持たないなら static | 外部 API や DB などに依存するなら instance |
| 純粋関数かどうか | 入力に対して決まった出力を返すだけの「純粋関数」であれば static | 外部リソースや状態に依存する処理は instance の方が柔軟 |
| テストのしやすさをどう担保するか | 単体で閉じた処理なら static | モックやスタブを多用するなら instance |
| 依存関係差し替えの可能性があるか | 差し替える必要がないなら static | 将来的に実装を切り替える可能性がある場合は instance |
このように整理すると、「補助的な純粋処理は static」「依存や差し替えの余地がある処理は instance」 という切り分けが一つの目安になります。
チームとして開発方針を揃える際にも、一度このようにメンバー間で整理しておくと、共通言語として役立つのではないでしょうか。
utils と service の責務分担
続いて、処理を「service 層に残すか、utils に切り出すか」という観点で整理していきます。
単なるフォルダ配置の話に見られがちですが、ここにも「ドメイン知識をどこに置くか」「再利用性をどの範囲で考えるか」といった設計上の判断が関わります。
utils に置くべき処理
特徴
-
汎用的で文脈を選ばない処理
- 日付フォーマット、文字列操作、数値計算など、特定のドメインに依存しないもの
-
再利用性を重視する処理
- 複数のモジュール・サービスから共通的に呼ばれることを前提にしているもの
注意点
-
ドメイン依存の処理を utils に混在させない
- 「会員ランクを算出する」「注文金額を計算する」といった処理は、汎用的なものではなくドメイン固有の処理
- 上記のような処理が utils に置かれると、役割が曖昧になり見通しが悪くなってしまうおそれがある
service に置くべき処理
特徴
-
ドメインやユースケースに依存する処理
- ビジネスロジック、業務ルール、ユースケース特有のフローをもつもの
-
外部依存を伴う処理
- DB、API、外部サービスとのやり取りなど、ドメイン内の振る舞いを支えるもの
注意点
-
サービス内だけで完結する処理を utils に安易に移動しない
- 「共通だから utils」と短絡的に判断すると、逆に可読性が下がったり、どこで使われているか追いにくくなる場合がある
- その処理が「ビジネスルールを表している」なら service に残す方が適切
判断基準
| 観点 | utils | service |
|---|---|---|
| 汎用性 | ドメインに依存しない純粋処理 | ドメイン固有の文脈を持つ処理 |
| 再利用範囲 | プロジェクト全体で共通利用を想定 | 特定のドメイン(ユースケース)に限定 |
| 依存関係 | 外部リソースを持たない | DB, API, 他サービスなど外部依存を持つ |
| ドメイン知識 | 不要 (アルゴリズム・フォーマット系中心) |
必須 (ビジネスルールを表す) |
このように整理すると、utils はあくまで「汎用的な部品置き場」、service は「ドメインを担う場所」という切り分けが見えてきます。
チームとして「どこまでを utils に寄せるのか」「どの責務は service に残すのか」を明確にしておくと、クラスやメソッドの配置に迷いが減り、コード全体の見通しも良くなります。
static と instance の混在を許容するか
ここまで、static と instance、そして utils と service の切り分けを整理してきました。
その延長でよく議論になるのが、「static と instance を同じクラスに混在させて良いのか」という点です。
これには大きく分けて「分けるべき派」と「混在でもよい派」のふたつの立場があります。
分けるべき派の意見
-
役割が明確になる
- static は「純粋な補助処理」、instance は「状態や外部リソースに依存する処理」と整理され、ファイルを見たときに迷いにくい
-
責務の分離がしやすい
- 純粋な補助処理が自然に分離され、クラスの役割が単一になりやすい
-
テストやリファクタリングがしやすい
- 依存関係を持つ処理と持たない処理が分かれているため、モジュール単位で扱いやすい
→ 「可読性・責務の明確さ」を重視する立場
混在でもよい派の意見
-
利用者にとっては「どちらも同じ振る舞い」
- 呼び出し側からすると
Class.method()かinstance.method()かの違いだけで、機能的には同じ
- 呼び出し側からすると
-
ファイル分離が細かくなりすぎるのを防げる
- 小さなクラスや関数が散在すると、コードを追うときにわかりにくくなり、保守性が下がる可能性がある
-
現実的には境界が曖昧な処理も多い
- クラスの役割としてまとめておいた方が、見通しが良いケースもある
→ 「実用性・開発効率」を重視する立場
このように、static / instance を完全に分けるのか、あるいは混在させるのかは「役割の明確さを重視するか」「現実的な効率を重視するか」の優先度によっても意見が分かれます。
どちらが正しいというよりは、プロジェクトやチームの方針よって最適解が変わるテーマであると言えます。
チームの落とし所を決める
ここまで、static と instance、utils と service の切り分け、そして static と instance の混在の可否について整理してきました。
実務では、どちらが正解というよりも、「チームとしてどのように方針を決めて運用するか」が重要です。
チームで落とし所を決める際は、以下の観点で考えると良いと思います。
-
可読性
- コードを読む人が処理の意図や役割を直感的に理解できること
-
保守性
- 将来的な変更やリファクタリングがしやすいこと
-
依存性管理
- 外部リソースや他モジュールとの依存関係を適切に扱えること
-
テスト容易性
- モックやスタブを使ったテスト設計が容易であること
観点別の立場
これらの観点をもとに、static / instance の分離や混在について、一般的に見られる立場を整理すると次のようになります。
この判断は、単に「分ける / 混在させる」という二択だけでなく、プロジェクトの規模やクラスのサイズ、処理の複雑さによっても変わってきます。
| 観点 | 分ける派 | 混在許容派 |
|---|---|---|
| 可読性 | static と instance が明確に分かれ、処理の役割を直感的に把握しやすい | 小規模クラスや処理がまとまっている場合は、混在でも全体を把握しやすく可読性が保たれる |
| 保守性 | 責務が明確に分かれており、変更時に影響範囲を追いやすい | ファイルやクラスを細かく分けずに済むため、小規模処理では保守性を損なわずに変更できる |
| 依存性管理 | 依存関係の有無が明確になり、外部リソースの管理がしやすい | 小規模処理で依存が少なければ、混在していても依存関係の把握は困難にならない |
| テスト容易性 | 依存を持つ処理と持たない処理が分かれており、モックや差し替えが容易 | 小規模処理では、混在していてもテスト設計や実装に大きな影響は出にくい |
プロジェクト規模による運用方針
小規模プロジェクト/クラスの場合
小規模なクラスや単純な処理が中心のプロジェクトでは、static と instance を同じクラス内に混在させる方が合理的な場合があります。
- ファイルやクラスの分割が細かくなると、かえって参照箇所や処理の流れを追いにくくなる
- 全体の処理量が少なければ、可読性や保守性の観点で混在している方がむしろメリットになる
- テストや依存管理も複雑化しないため、実務上の効率が高い
大規模プロジェクト/クラスの場合
大規模なクラスや複雑な依存関係を持つプロジェクトでは、static と instance を分けるメリットが大きくなります。
- 役割が明確になり、責務の分離がしやすくなる
- 依存関係の管理やモック化が容易になり、テスト容易性も高まる
- リファクタリング時に影響範囲が限定され、保守性が向上する
新規立ち上げ/稼働中 プロジェクトにおける優先度の違い
また、チームとして方針を決める際、プロジェクトの状況によって優先度も変わります。
新規立ち上げプロジェクト
- 設計の自由度が高く、初期段階で統一ルールを定めやすい
→可読性や責務の明確さを優先し、分ける方針を採用することで、後々の拡張や保守を容易にできる
稼働中プロジェクト
- 既存コードがあるため、大規模な変更はリスクが高い
- 小規模なクラスや補助的な処理は混在を許容して効率重視
→大きな影響がある箇所や複雑なクラスだけ分離を検討するなど、段階的にリファクタリングする
このように、チームの落とし所を決める際は4つの観点を整理しつつ、プロジェクトの規模や状況に応じて運用方針を決めることが重要です。
チームの合意形成では、プロジェクトとしてどのような立場をとるのか、を確認すると良いでしょう。
たとえば、
- 規模が大きくないから混在OKとした方が可読性や保守性が高くなりそう
- 将来的に大きなプロジェクトになりそうだからしっかり分けたい
- 分けるかどうか、という観点よりも汎用的な処理だけをutilsにおいて、ドメイン特化の処理は共通処理でもドメイン配下に置くようにしよう
- メソッドの形態をしっかり分けたいから、staticはutilsに置くようにして、utils配下のフォルダ管理でドメイン特化なのかがわかるようにしよう
など、「どのような意図で」「何を重視するか」が明確になると、納得感のある共通言語として機能します。
統一ルールや共通認識を持つことで、設計やレビューがスムーズになり、コード全体の一貫性を保ちやすくなります。
自分のプロジェクトはどうなのか気になった方がいれば、ぜひチームで確認する機会を設けてみてください。
おわりに
いかがだったでしょうか。
今回は、staticとinstance、utilsとservice をという観点をテーマに、現場で迷いがちな「どの形態のメソッドをどこに置くか」について、整理しながら改めて考えてみました。
それぞれのメリットやデメリット、考慮するべき4つの観点、そしてそれらがプロジェクトの規模や状況によって、優先順位や視点が変わることを確認しました。
このような視点を持つことで、プロジェクトに合ったより良い方針を、チームの中で見つけることができるかもしれません。
メソッドの形態や配置場所について悩んでいる方の参考になれば幸いです。
以上です。最後まで閲覧いただきありがとうございます。