要約
大事なのは
- 名前から責務が読めること
- 副作用が必要以上に散らばっていないこと
- 純粋な処理と、外部とつながる処理の境界が見えること
- ドメインのルールがデータから剥がれ落ちすぎていないこと
- チームの共通ルールを人間の記憶ではなく仕組みで守れること
0. はじめに
今まで約3年ほど、エンジニアとしてプロダクト開発をしてきたのですが、たびたび
令和の時代に、こんなことあるんだ......!?
みたいな出来事に遭遇してきました。
(おかげさまで、残業ともかなり深い仲です❤️)
イヤホンですらなんかカッコいい電気を放出していそうなこの時代に、
ソフトウェア開発の現場だけ泥臭い地獄が残っているの、普通に不思議なんですよね。
そんなカオスをエンジニアとして少しでもマシにできないか。
その結果たどり着いた ٩( ᐛ )و < 僕の考えた最強のコード について、この記事ではつらつら書いていこうと思います。
「いやそれは盛ってるだろ」と思いながら、小さい子を見守るような生暖かい目で読んでいただけるとうれしいです。
1. 私がみてきたカオス
私はここ3年くらい、エンジニアとしていくつかの現場を見てきました。
その中で、「バナナ‼️」と叫びたくなるシーンがいくつもありました。
- 田中さん(癖強)が退職した瞬間、誰も保守できなくなったあの機能
- たった1行直しただけなのに、ログイン機能が全滅したあのコミット
かなり雑な例ではありますが、首が取れそうなほど激しく頷いていただいている皆様が私には見えます。
ただ、こうした事象の多くは、コードの全体像がちゃんと把握できていないこと から起きているのではないか、と私は考えています。
逆に言えば、全員がある程度理解できるコードを書き続けられれば、この地獄はもう少しマシになるのではないか。
そんなことを考えたのが、この記事を書くきっかけです。
2. 持続可能なプロダクトコードとは
ここまでカオスの話をしてきましたが、じゃあ逆に
「持続可能なプロダクトコードって何なの?」
という話をしないと始まりません。
私はひとまず、持続可能なプロダクトコードを
「特定の誰かの記憶力や根性に依存せず、チームで回し続けられるコード」
だと思っています。
もう少し雑に言うと、読むのがつらくない、直すのが怖すぎない、壊れても追える の3つがそろっている状態です。
そのうえで、この記事ではいったん次の2つを最低条件として置きたいと思います。
- 新しく入ったメンバーでも、1週間で主要機能と担当範囲の全体像をつかめること
- コードを修正したときに、「どこまで影響しそうか」がある程度読めること
1つ目が満たせていないと、コードベースは一部の古参しか進めないダンジョンになります。
2つ目が満たせていないと、毎回の修正が「祈ってデプロイ」になります。
もちろん、これだけで全部解決するわけではありませんが、今回は読める 直せるに重点を置いていければと思います。
3. 持続可能なプロダクトコードをどう作るか
「銀の弾などない」
ソフトウェアエンジニアの皆様一度は聞いたことがあるこのセリフのように、これだけやればすぐOKといったような魔法はありません。
ただ、これまで地獄をいくつか見てきた(生み出した テヘペロ)経験上、以下の5つは普遍的に重要な要素と考えています。
3-1. 正しく命名できない = まだその処理を理解できていない
シンプルではありますが、非常に大事だと思っています。
正しく命名できないということは、まだその関数や処理の責務を自分で掴み切れていないことが多いです。
たとえば handleData や processSomething と言ったような汎用的な命名は 「結局何をしているのかは分からない🤪」という状態になりがちです。
因むと、GitHub CLI経由でhandleDataを検索したところ、レートリミット超過するほどには多くヒットする物のようです。命名考えるのって実は結構大変ですもんね、
逆に、validateLoginForm や updateLoginInfo のように、
動詞 + 目的語 で責務がある程度読める名前になっていると、コードを追うコストがかなり下がります。
命名は、ただのおしゃれポイントではありません。
その処理を一言で説明できるくらいまで分解して、責務を理解できているかどうかのテストだと思っています。
type LoginForm = {
email: string;
password: string;
};
// BAD
// 何をしているのか、名前からかなり読みづらい
function handleData(form: LoginForm): boolean {
if (!form.email.includes("@")) return true;
if (form.password.length < 8) return true;
return false;
}
これだと handleData が何をしているのか分からないので、
結局中身を読みに行くことになります。
一方で、こういう名前だとかなり読みやすくなります。
// GOOD
function hasInvalidEmail(email: string): boolean {
return !email.includes("@");
}
function hasWeakPassword(password: string): boolean {
return password.length < 8;
}
function validateLoginForm(form: LoginForm): string[] {
const errors: string[] = [];
if (hasInvalidEmail(form.email)) {
errors.push("email is invalid");
}
if (hasWeakPassword(form.password)) {
errors.push("password must be at least 8 characters");
}
return errors;
}
名前がどうしても定まらないときは、命名センスの問題ではなく、
1つの関数にいろいろ詰め込みすぎている 可能性を疑った方がよいです。
3-2. 基本的には純粋関数に寄せた方がよい
純粋関数は、ざっくり言うと
「同じ入力を入れたら、同じ出力が返ってくる」「外の状態(DBなど)に勝手に触らない」関数
のことです。
純粋関数に寄せると、少なくとも次の3つがかなりうれしいです。
- 単純に読みやすい
- テストしやすい
- 副作用が散らばりにくい
入力と出力だけである程度挙動が追えるので、読む側の負担が減ります。
また、外部状態や通信結果に振り回されにくいため、テストもかなり書きやすくなります。
たとえば、こういう関数はかなり追いやすいです。
type CartItem = {
price: number;
quantity: number;
};
function calcSubtotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function applyDiscount(subtotal: number, discountRate: number): number {
return Math.floor(subtotal * (1 - discountRate));
}
function calcTotal(items: CartItem[], discountRate: number): number {
const subtotal = calcSubtotal(items);
return applyDiscount(subtotal, discountRate);
}
このコードの良いところは、
何を受け取って 何を返すのか がかなりはっきりしていることです。
しかも、テストもかなり楽です。
describe("calcTotal", () => {
it("calculates total with discount", () => {
const items = [
{ price: 1000, quantity: 2 },
{ price: 500, quantity: 1 },
];
expect(calcTotal(items, 0.1)).toBe(2250);
});
});
3-3. ただし、全部を純粋関数にしろという話ではない
とはいえ、「じゃあ全部純粋関数で書こう!」になると、それはそれで少し雑です。
世の中には、どうしても副作用を持つ処理があります。
たとえば、Repository 層のような API / DB 通信、状態更新の入口、LocalStorage 操作、時刻取得などです。
大事なのは、副作用をゼロにすることではなく、副作用がある場所をはっきり分けること です。
たとえば、こういうコードはちょっとしんどいです。
type UserResponse = {
id: string;
first_name: string;
last_name: string;
plan: "free" | "premium";
};
type User = {
id: string;
fullName: string;
isPremium: boolean;
};
// 通信、変換、保存、画面更新が全部ここに混ざっている
export async function loadUserProfile(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = (await response.json()) as UserResponse;
const user = {
id: data.id,
fullName: `${data.first_name} ${data.last_name}`,
isPremium: data.plan === "premium",
};
localStorage.setItem("lastViewedUserId", user.id);
document.title = `${user.fullName} | Profile`;
return user;
}
このコード、動くことは動くのですが、
- API 通信している
- レスポンスをアプリ用のデータに変換している
- LocalStorage を更新している
- タイトルまで更新している
と、いろいろな責務が1つの関数に全部入っています。
こういうときは、pure な処理と pure ではない処理を切り分けた方がかなり追いやすくなります。
type UserResponse = {
id: string;
first_name: string;
last_name: string;
plan: "free" | "premium";
};
type User = {
id: string;
fullName: string;
isPremium: boolean;
};
// 変換処理は pure
function mapUserResponseToUser(response: UserResponse): User {
return {
id: response.id,
fullName: `${response.first_name} ${response.last_name}`,
isPremium: response.plan === "premium",
};
}
// 外部との通信は副作用ありでOK
export class UserRepository {
async fetchById(id: string): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
return (await response.json()) as UserResponse;
}
}
// 保存処理も副作用あり
function saveLastViewedUserId(userId: string): void {
localStorage.setItem("lastViewedUserId", userId);
}
// 画面更新も副作用あり
function updatePageTitle(user: User): void {
document.title = `${user.fullName} | Profile`;
}
// orchestration だけを担当する
export async function loadUserProfile(
id: string,
repository: UserRepository,
): Promise<User> {
const response = await repository.fetchById(id);
const user = mapUserResponseToUser(response);
saveLastViewedUserId(user.id);
updatePageTitle(user);
return user;
}
この例だと、通信そのものや保存処理は副作用ありです。
でも、レスポンスをアプリ内の値に変換する処理は pure に切り出せています。
つまり、
- 計算や変換はなるべく純粋関数に寄せる
- 通信や状態変更は境界の層に閉じ込める
という分け方ができていると、コード全体がかなり追いやすくなります。
3-4. 貧血ドメインモデルをやめる
ここは少し好みが分かれるところではありますが、個人的には
ドメインのルールをデータから追い出しすぎるのは、かなりしんどい と思っています。
いわゆる貧血ドメインモデルってやつですね。
つまり、モデルはただの値の箱になっていて、肝心のビジネスルールは service や util のあちこちに散らばっている状態です。
たとえば、こういうやつです。
type Order = {
amount: number;
couponRate: number;
status: "draft" | "paid" | "cancelled";
};
function calcBillingAmount(order: Order): number {
return Math.floor(order.amount * (1 - order.couponRate));
}
function canCancelOrder(order: Order): boolean {
return order.status === "draft";
}
function markOrderAsPaid(order: Order): Order {
return {
...order,
status: "paid",
};
}
一見すると普通に見えるのですが、こういう形が増えてくると
- 金額計算のルールが別ファイル
- キャンセル条件が別ファイル
- 状態遷移のルールも別ファイル
みたいになって、「結局この Order ってどういう存在なんだっけ?」 が見えにくくなります。
なので、少なくとも重要なルールは、できるだけデータの近くに置いた方が追いやすいです。
class Order {
constructor(
readonly amount: number,
readonly couponRate: number,
readonly status: "draft" | "paid" | "cancelled",
) {}
get billingAmount(): number {
return Math.floor(this.amount * (1 - this.couponRate));
}
canCancel(): boolean {
return this.status === "draft";
}
pay(): Order {
if (this.status !== "draft") {
throw new Error("only draft orders can be paid");
}
return new Order(this.amount, this.couponRate, "paid");
}
}
こうしておくと、
- どういう値を持つのか
- どういうルールで振る舞うのか
- どういう状態遷移が許されるのか
が、ある程度まとまって見えるようになります。
もちろん、「TypeScript なら全部 class にしろ」という話ではありません。
factory function でも module でも何でもよいのですが、言いたいのは
ルールをデータから遠ざけすぎるな
ということです。
3-5. ドメイン知識は人間の記憶力ではなく lint に持たせる
プロジェクトを続けていると、
- この層からこの層を直接参照してはいけない
- この値はこの関数を通して扱う
- この文脈ではこの命名を使う
- 特定操作には必ずログ送信を紐づける
ナドナド、プロジェクト固有のルールがどうしても増えてきます。
こういうルールを全部 レビューで気づこう とか 雰囲気で守ろう に任せると、それはいつしか風化し気づけば誰も順守しなくなってしまいます。
人間は忘れるし、レビューでも漏れるし、疲れている日の我々はそんなに強くないからです。
なので、共通のドメイン知識や設計ルールは、できるだけ lint で機械的に検証した方がよい と思っています。
たとえば、「UI 層から repository を直接触らせたくない」なら、ESLint でこういう制約をかけられます。
export default [
{
files: ["src/features/**/components/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: ["@/repositories/*"],
},
],
},
},
];
このルールがあると、たとえば次のようなコードを機械的に止められます。
import { userRepository } from "@/repositories/userRepository";
export async function UserProfile() {
const user = await userRepository.fetchById("1");
return <div>{user.fullName}</div>;
}
代わりに、間に use case や service を挟むようにできます。
import { getUserProfile } from "@/features/user/services/getUserProfile";
export async function UserProfile() {
const user = await getUserProfile("1");
return <div>{user.fullName}</div>;
}
レビューは本来、もっと難しいことを見るために使いたいんですよね。
なので、機械で落とせるものは先に機械で落とす。これがかなり大事です。
lint ルールは最初から完璧である必要はありません。
まずは「毎回レビューで同じことを言っているな......」というものから機械に移していくのがおすすめです。
生成 AI を使うと、ルールのたたき台もかなり速く作れます。
4. じゃあ、なぜ AI 時代にわざわざ読みやすいコードを書くのか
ここまで読んで、
いや、でも今って AI がコードを書いてくれる時代では?
と思った方もいるかもしれません。
それは本当にそうで、今の開発現場では生成 AI を使ってコードを書くこと自体は、かなり普通のことになってきました。
いわゆるバイブコーディング的なノリで、まずは形にしてみる。
これ自体は、自分も全然アリだと思っています。速いので。
ただ、その一方で、今の仕組みを見ていると
最終的な責任を取るのは、だいたい人間です。
AI はコードを生成してくれます。
レビューの補助もしてくれます。
テストケースのたたき台も出してくれます。
でも、障害が起きたときに謝るのは AI ではありません。
本番で事故ったときに原因を追うのも AI ではありません。
「なぜこの実装でよいと判断したのか」を説明するのも、結局は人間です。
つまり、少なくとも今の多くの現場では、
AI は成果物を作ることはあっても、成果物責任までは請け負ってくれない
ということです。
だとすると、AI を使うこと自体が問題なのではなくて、
人間がレビューできる形で AI を使えているか がかなり大事になります。
言い換えると、
- 読んでも責務が分からないコード
- 副作用がどこに飛ぶか分からないコード
- ルールが散らばっていて判断根拠が追えないコード
を AI が高速に量産できてしまう状態は、普通に危ないです。
速く作れることと、責任を持てることは、残念ながら別の話なんですよね。
だから私は、AI 時代だからこそ、むしろ
- 人間が読める
- 人間がレビューできる
- 人間が説明できる
- 人間が責任を持てる
コードを書いておく必要があると思っています。
AIを使った高速バグ埋め込み型開発を逆ClaudeCodeと呼んでいる現場があると聞き爆笑しました。
5. じゃあ、その思想を agent.md にしてみる
# Readable and Sustainable Code Agent
## Goal
Write code that humans can review, explain, and take responsibility for.
Optimize for readability and maintainability over cleverness or short-term speed.
## Core Principles
### 1. Prefer explicit naming
- Avoid generic names such as `handleData`, `processSomething`, `doStuff`, `temp`, `value1`.
- Use names that express responsibility directly.
- Prefer `verb + object` for behavior and domain-specific nouns for models.
- If a good name is hard to find, assume the responsibility is still too broad and split the code first.
### 2. Default to pure functions
- Prefer pure functions for calculation, validation, mapping, and formatting.
- Pure functions should depend only on their inputs and return the same output for the same input.
- Extract pure logic first when refactoring mixed code.
- Keep function inputs and outputs explicit.
### 3. Isolate side effects
- Side effects are allowed, but they must be obvious and localized.
- Keep API calls, DB access, storage writes, time access, random generation, logging, and state mutation at the boundary layer.
- Separate orchestration code from calculation and transformation logic.
- Do not hide side effects inside utility functions with vague names.
### 4. Avoid anemic domain models
- Do not reduce domain objects to passive data containers when they have real business rules.
- Keep invariants, state transitions, and important domain behavior close to the domain type they belong to.
- Do not scatter core business rules across unrelated `utils`, `services`, and UI components without a strong reason.
### 5. Move repeated rules into tooling
- If the same review comment appears repeatedly, propose a lint rule, static check, test helper, or codemod.
- Prefer machine-enforced rules for import boundaries, naming, forbidden APIs, mandatory logging, and architectural constraints.
- Do not rely on tribal knowledge when a rule can be made executable.
## Implementation Rules
- Prefer small functions with a single clear responsibility.
- Keep data flow explicit.
- Favor predictable file placement over clever abstractions.
- Add comments only when they explain intent, tradeoffs, or domain context.
- Do not introduce abstraction before the code clearly needs it.
- When refactoring, preserve behavior first, then improve structure.
## Review Checklist
Before finalizing a change, verify the following:
- Can a new teammate roughly understand each function from its name?
- Are calculation and transformation paths mostly pure?
- Are side effects isolated to clear boundary layers?
- Are important business rules close to the domain they govern?
- Should any repeated project rule be enforced by lint or automation?
- Can a human reviewer explain why this implementation is correct?
## Output Style
When generating or refactoring code:
- Prefer readable code over terse code.
- Prefer explicitness over hidden magic.
- Prefer stable patterns already used in the codebase.
- If a tradeoff exists, choose the version that is easier for a human to review and maintain.
こちらが主題のmdファイルとなっております。
とは言っても、この記事の内容をCodex(すごい)に渡しただけのあくまでテンプレートとなっていますので、皆様のプロジェクト特性に合わせて適宜修正していただけますと幸いです。
6. 終わりに
今まで述べてきたのはペーペーエンジニアの独り言ですので、何か気になる点などあればぜひコメントいただけますと幸いです!
また、今年は週次で記事を書いて行きたいと思っており、何か気になるテーマなどあればこちらも合わせてコメントお待ちしています!