あかね
設計の判断基準を言語化してみたら直しやすさで選んでたことに気づいた話
2026年03月29日
要約を生成中...
はじめに
AIに実装は任せるけど設計やレビューは人間がするということは理解していつもAIで実装していますが、よく考えると自分の設計判断などを言語化できるんかなと疑問に思いました。
振り返ってみると、その判断は理論というより、過去に踏んだ地雷やリファクタで苦労した経験から来ているものがほとんどな気がします。
つまり、「良い設計を知っている」というより、「壊れにくい選び方をしている」だけなのかもしれません。
この記事では、自分が無意識にやっていた設計判断をあらためて言語化してみます。
1. 疎結合にする = 変更の影響範囲を制御する
「疎結合にする」とよく言うようですが、自分の中では「ある変更がどこまで波及するかをコントロールすること」のように捉えています。
アンチパターン:ユースケースを子コンポーネントが知っている
Before
type Props = {
mode: "create" | "edit"
user?: User
}
const UserForm = ({ mode, user }: Props) => {
const defaultValues =
mode === "edit"
? { name: user?.name ?? "" }
: { name: "" } const onSubmit = async (data: { name: string }) => {
if (mode === "edit") {
await updateUser(data)
} else {
await createUser(data)
}
}
return
}一見よさそうに見える理由
コンポーネント1つで完結している
呼び出し側がシンプル
propsも少なく見える
でも破綻する未来も見える気がします。
実際に起きそうなこと
edit時だけconfirmダイアログ出したい
createは下書き保存したい
権限によってupdate APIが変わる
その結果、以下のようになりそうです。
const onSubmit = async (data: { name: string }) => {
if (mode === "edit") {
if (isAdmin) {
await updateUserAsAdmin(data)
} else {
await updateUser(data)
}
} else {
if (saveAsDraft) {
await saveDraft(data)
} else {
await createUser(data)
}
}
}何が起きているか
分岐がユースケースごとに増殖する
UIコンポーネントが業務ロジックを抱え始める
修正のたびにこのコンポーネントを触る必要が出る
負債の正体
edit機能を触ったらcreateが壊れる
テストケースが爆発してメンテされなくなる
「このコンポーネント触るの怖い」がチームに蔓延する
→結果的に誰も触れないブラックボックスになる
私の判断
ユースケースに関わる分岐は上に寄せる
After
type Props = {
defaultValues: {
name: string
}
onSubmit: (data: { name: string }) => Promise
}const UserForm = ({ defaultValues, onSubmit }: Props) => {
return
}const CreateUserPage = () => {
const handleSubmit = async (data: { name: string }) => {
await createUser(data)
} return (
<UserForm
defaultValues={{ name: "" }}
onSubmit={handleSubmit}
/>
)
}const EditUserPage = ({ user }: { user: User }) => {
const handleSubmit = async (data: { name: string }) => {
await updateUser(data)
} return (
<UserForm
defaultValues={{ name: user.name }}
onSubmit={handleSubmit}
/>
)
}※実際は同じファイルにコンポーネント詰め込んだりしません⚠️
何が変わるか
Formは「入力UI」だけに集中
ユースケースごとの変更は親だけ触ればいい
影響範囲が局所化される
疎結合 = 「壊れる範囲を限定する設計」
2. 肥大化していたら責務を疑う
コンポーネントやクラスが大きくなってきたら、ほぼ確実に単一責任になってるか??を疑います。
アンチパターン:全部入りコンポーネント
Before
const UserProfile = () => {
const [user, setUser] = useState<User | null>(null)
const [isEditing, setIsEditing] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchUser().then(setUser)
}, [])
const handleUpdate = async (data: User) => {
setLoading(true)
await updateUser(data)
const updated = await fetchUser()
setUser(updated)
setLoading(false)
}
if (!user) return
return (
{isEditing ? (
) : (
)}
<button onClick={() => setIsEditing(!isEditing)}>
toggle
)
}一見問題なさそう??
1ファイルで完結している
データ取得も更新もここでできる
シンプルに見える
でも、多分こうなると思う。
「一覧画面でも同じユーザー情報を使いたい」
「キャッシュを入れたい」
「ローディングUIを統一したい」
その結果👇
fetchロジックがコピペされる
状態管理が画面ごとにバラバラになる
修正のたびに複数箇所直す
何が起きているか?
責務が混ざっている状態です
データ取得
UI表示
編集状態管理
API呼び出し
負債の正体
API変更のたびに複数コンポーネント修正
状態バグが画面ごとに再発
「同じロジックなのに微妙に違う」地獄
結果→変更コストが指数的に増える
私の判断
変更理由が複数あるなら分割する
After
const useUser = () => {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
fetchUser().then(setUser)
}, [])
return { user, setUser }
}
const UserProfile = () => {
const { user } = useUser()
const [isEditing, setIsEditing] = useState(false)
if (!user) return
return (
{isEditing ? (
) : (
)}
)
}const UserEdit = ({ user }: { user: User }) => {
const handleSubmit = async (data: User) => {
await updateUser(data)
}
return (
)
}何が良くなったか
データ取得は hook に閉じる
UIとロジックが分離される
再利用できる単位になる
肥大化しているコードは「責務が混ざっているサイン」かもしれないです。(あるある)
分けることで結果的に変更の理由も分離される可能性あります
まとめ
いろいろ書きましたが、結局やっていることはシンプルです。
疎結合にする(壊れる範囲を限定)
肥大化したら責務を疑う(変更理由を分ける)
外部依存は閉じ込める(変化に強くする)
でも、必要になるまで抽象化しない(YAGNI)
おわりに
疎結合が常に正義ってわけではないので、あくまで臨機応変にでそこがまた難しいのですが、コンポーネントについてはだいぶ怪しい匂いを嗅ぎ取れるようになった気がします。
ただ、バックエンドはビジネスロジックがたくさん入ってくるので、上から下に向かって処理を書いていかずに、この処理はここにいていいのか?は常に意識していかないとって思っていますが、まぁ難易度高いな〜って思います。
設計議論もよくPRレビューで繰り広がります。
正解が常に一つではないところもまた難しいなって思いますが面白いです。
少なくとも最初から完璧な設計を目指すのは不可能です。
大事なのは、「間違っても、後から最小限のコストで直せる設計」を選択し続けることなのだと思います。


