AI 任せにできない。Bad / Good で整理するリネームのパターン
こんにちは、あるいはこんばんは。まっくすです。
今日は私がよく指摘されるリネームについて書こうかと思います!
コードを書いていると、名前を変えたくなる場面はよくありますよね?
一見するとリネームは簡単そうに見えますが、単なる文字列置換では終わらないことが多いです。
この記事で扱うリネームのパターン
この記事では、リネームを次の5つのパターンに分けて考えます。
- 表記ゆれを揃えるリネーム
- 意味の変化に追従するリネーム
- 型の変更に伴うリネーム
- 利用側の命名も揃える必要があるケース
- 仕様変更に伴うリネーム
命名は難しい
命名が難しいのは、単に識別しやすい名前をつければよいわけではないからです。
名前には、その値が何者で、どのような責務を持ち、どのような文脈で使われるのかを表す役割があります。
たとえば user は一見自然な名前ですが、実際には非常に広い概念です。
読む側は user という名前から、少なくとも次の3つを解釈しようとします。
- 役割: 利用者、管理者、契約者、組織メンバーのどれか
- 状態: ログイン中、招待中、退会済みなどのどれか
- 表現: API の生データなのか、画面表示用に加工したデータなのか、入力・送信用に整えたデータなのか
読む側は毎回「これは何を表しているのか」を確認しなければなりません。
命名が難しいのは、こうした解釈の手がかりを名前に持たせつつ、長すぎず、過不足のない名前をつける必要があるからです。
リネームはさらに難しい
命名自体が難しい以上、すでについている名前をあとから変えるリネームはさらに難しくなります。
リネームでは、今の実態に合った名前を考えるだけでなく、過去の文脈でつけられた名前とのズレを見極める必要があります。
さらに、宣言元だけでなく利用側や関連するファイル名、テスト名、Story 名なども含めて整えないと、語彙の不一致が残ることがあります。
つまりリネームが難しいのは、新しい名前を考える難しさと、既存の文脈全体を揃える難しさの両方があるからです。
ここからは、サンプルを見ながらリネームのパターンについて見ていきます。
1. 表記ゆれを揃えるリネーム
ここで扱うのは、意味の変更ではなく、同じ概念に対する表記ゆれをなくすためのリネームです。
実務では、値や関数の責務は変わっていないのに、名前だけが少しずつ揺れていることがあります。
たとえば、signin と signIn のように単語の区切り方が揺れていたり、login と signin のように同じ操作を別の表現で呼んでいたりするケースです。
こうした揺れは、動作には直接影響しないこともあります。
ただ、読む側にとっては「これは同じものなのか」「何か意図的に使い分けているのか」を余計に確認する必要が生まれます。
このパターンのリネームでやっているのは、名前が表す意味を変えることではなく、同じ概念に対する表記や表現を統一することです。
極端な例ですが、コード量が膨大になってくるとこういった命名のブレが起こりがちです。
🙅 Bad
const signin = () => {
...
}
const handleSignIn = () => {
signin()
...
}
const LoginButton = () => {
...
}
🙆 Good
const signIn = () => {
...
}
const handleSignIn = () => {
signIn()
...
}
const SignInButton = () => {
...
}
どれも「サインインする」という概念を表していますが、同じ概念に対する表記と表現の統一しています。
この種のリネームでは、何が正しい名前か以上に、どう揃えるかが重要になります。
たとえば、
-
camelCaseの区切り方に合わせてsignInにする - 画面や API、ドキュメント上の用語も
signInに統一する - 逆に、プロダクト全体で
loginを正式な用語としているならloginに寄せる
といった判断がありえます。
大事なのは、どちらを採用するかよりも、同じ概念を複数の書き方で残さないことです。
2. 意味の変化に追従するリネーム
ここで扱うのは、値や関数、型の実態が変わったので、それに合わせて名前も更新するリネームです。
コードを書いていると、最初は実態に合っていた名前が、その後の改修によって少しずつズレていくことがあります。
このとき必要になるのは、表記を揃えるためのリネームではなく、今の実態に合わせて名前を更新することです。
たとえば、移行中は旧実装と新実装を区別するために useNewProfile というカスタムフックを
用意していたとします。
移行が完了した後は New という語は実態を表さなくなります。
この場合、名前に残っているのは現在の意味ではなく、移行途中だった頃の事情です。
🙅 Bad
export const useNewProfile = () => {
return profile
}
🙆 Good
export const useProfile = () => {
return profile
}
この変更でやっているのは、現在の実装の前提に合わせて、名前から不要になった区別を外しているということです。
同じようなことは、返り値の意味が変わった場合にも起こります。
たとえば、もともとは profile オブジェクトを返していた useProfile が、改修によって profile.configured だけを返すようになったなら、その時点でフックが表しているものは「profile」ではなくなっています。
🙅 Bad
export const useProfile = () => {
return profile.configured // 改修によって configured を返すようになった
}
🙆 Good
export const useIsProfileConfigured = () => {
return profile.configured
}
この例で重要なのは、返しているものの意味が変わったことを名前に反映していることです。
useProfile という名前のままだと、読む側は profile オブジェクトが返ってくると考えやすくなります。
一方で useIsProfileConfigured にすると、このフックが返しているのは profile そのものではなく、profile が configured かどうかを表す boolean だとわかりやすくなります。
このパターンのリネームでは、元の名前をそのまま保つことよりも、今その値や関数が何を表しているかを優先して考える必要があります。
実装の変更に名前が追従していないと、コードの見た目と実態がずれていき、読む側は毎回中身を確認しなければなりません。
3. 型の変更に伴うリネーム
ここで扱うのは、TypeScript の型が表す責務や粒度が変わったので、それに合わせて型名も更新するリネームです。
実装を進めていると、最初はレスポンスそのものを表していた型が、途中から画面表示用に整形された型になったり、一覧用に項目を絞った型になったりすることがあります。
このとき型の中身は変わっているのに、型名だけが以前のままだと、その型が何を表しているのかがわかりにくくなります。
たとえば、もともとは商品そのものを表していた型を、カード表示用に必要な項目だけ持つ型へ作り替えたとします。
さらに価格も数値ではなく表示用の文字列に変わっているなら、それはもう「商品そのもの」ではなく、商品カード表示用の型です。
🙅 Bad
// 改修によって Product 型はカード表示用に必要な項目だけ持つようになった
type Product = {
id: string
name: string
thumbnailUrl: string
priceLabel: string
}
type Props = {
product: Product
}
export const ProductCard = ({ product }: Props) => {
return (
<article>
<img src={product.thumbnailUrl} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.priceLabel}</p>
</article>
)
}
🙆 Good
type ProductCardItem = {
id: string
name: string
thumbnailUrl: string
priceLabel: string
}
type Props = {
product: ProductCardItem
}
export const ProductCard = ({ product }: Props) => {
return (
<article>
<img src={product.thumbnailUrl} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.priceLabel}</p>
</article>
)
}
型が表している対象が、商品そのものから商品カード表示用のデータに変わっています。
Product のままだと、読む側は「商品全体を表す型なのかな」と受け取りやすくなります。
一方で ProductCardItem にすると、この型が、カード表示用に切り出された型であることが名前からわかります。
たとえば Product という型名がついていても、用途が変われば別の型として考えたほうがよいことがあります。
- ドメイン上の商品そのものを表す型なのか
- 一覧画面に表示するために項目を絞った型なのか
- API レスポンスを画面用に整形した型なのか
では、型が表しているものは同じではありません。
そのため、型の用途や責務が変わったときは、型名もそれに合わせて見直したほうが、コードの意味が伝わりやすくなります。
4. 利用側の命名も揃える必要があるケース
ここで扱うのは、関数やカスタムフックの名前を変えたときに、その利用側の変数名やハンドラ名もあわせて見直したほうがよいケースです。
リネームは宣言元だけを変えれば終わりに見えることがあります。
ただ実際には、関数やカスタムフックの名前を変えても、それを受け取る変数名や、その変数を使った関数名までは自動で自然に揃わないことがあります。
たとえば、useLogin を useSignIn にリネームしたとします。
このとき、宣言元の名前は signIn に変わっていても、利用側で login や handleLogin という名前が残っていると、コード全体ではまだ語彙が揃っていません。
🙅 Bad
const login = useSignIn() // useLogin から useSignIn にリネームされた
const handleLogin = async () => {
await login()
}
🙆 Good
const signIn = useSignIn()
const handleSignIn = async () => {
await signIn()
}
この例で変えているのは、宣言元の名前だけではありません。
利用側でその値をどう受け取り、どう使っているかまで含めて見直しています。
useSignIn というフックから受け取った値を login と呼び続けることもできます。
ただ、その状態だと
- フック名は
useSignIn - 受け取る変数名は
login - 実行する関数名は
handleLogin
となり、同じ処理を指しているのに場所ごとに語彙がずれてしまいます。
こうしたズレがあると、読む側は「これは同じ操作なのか」「login と signIn を意図的に使い分けているのか」を余計に確認しなければなりません。
この種のリネームで注意したいのは、一括置換だけでは十分でないことがある という点です。
たとえば、お使いのエディタのリネーム機能で useLogin を useSignIn に変えることはできても、
const login = useSignIn()const handleLogin = () => {}onLoginloginLabel
のような、利用側で意味的につけられた名前までは自動で揃わないことがあります。
大事なのは、すべてを機械的に同じ名前へ置き換えることではありません。
見るべきなのは、宣言元の変更によって、利用側の語彙が不自然にずれていないかです。
リネームは宣言元だけを変えて終わりではなく、その名前が使われている周辺まで含めて揃えてはじめて、コード全体の意味が統一されることは頭に入れておきたいです。
5. 仕様変更に伴うリネーム
ここで扱うのは、業務やプロダクトへの理解、あるいは仕様変更によって、より正確な用語へ名前を更新するリネームです。
このパターンでは、フロントエンドのインターフェースや実装が大きく変わるとは限りません。
受け取る Props や型の形はそのままでも、仕様変更によって、実際に入ってくる値の意味が変わることがあります。
たとえば、受け取った contents を表示するコンポーネントがあったとします。 Story 名として WithContents を使っていたとします。もともとは contents にさまざまな内容が入る想定だったなら、この名前でも不自然ではありません。
ただ、仕様変更によって、その contents には サポートメッセージだけが入るようになったとします。
このとき、フロントエンドの受け口は contents のままだとしても、その Story が表している状態は、単なるコンテンツありの状態ではありません。
🙅 Bad
export const WithContents: Story = {
args: {
contents: "お困りの場合はサポート窓口までお問い合わせください",
},
}
🙆 Good
export const WithSupportMessage: Story = {
args: {
contents: "お困りの場合はサポート窓口までお問い合わせください",
},
}
contents という Props 名やインターフェースが変更しているわけではありません。
変わっているのは、その値を業務上どう解釈するべきかです。
WithContents という名前は、広い意味では正しいかもしれません。
ただ、仕様変更によって contents に入るものがサポートメッセージに限定されたなら、その Story を表す名前としては広すぎます。
WithContents のままだと、
- どんな内容が入るのか
- 他の種類のメッセージも含むのか
- 何の状態を表す
Storyなのか
が名前からわかりません。
一方で WithSupportMessage にすると、その Story がサポートメッセージを表示する状態を表していることが名前からわかりますし、Story を見た際に、コンテンツ部分がある場合にはサポートメッセージ以外が入らないことがわかります。
このパターンで大事なのは、インターフェース上の名前をそのまま使い続けることではなく、現在の仕様に照らして、その状態を最も正確に表す名前に更新することです。
フロントエンド側の受け口は変わらなくても、バックエンドの仕様変更によって、そこに入る値の意味が変わることがあります。
そのときに、最初の広い名前を残すのではなく、今のドメインに合った名前へ更新するのが、この種類のリネームです。
まとめ
- リネームは、単なる文字列置換ではなく、コードの実態と名前を揃える作業
- リネームでは、宣言元だけでなく利用側や周辺の語彙まで含めて整えることが大事
- AI や IDE の支援で変更しやすくなっても、文脈や責務、ドメイン上の自然さは人間が確認する必要がある
名前は、読む側がコードを理解するための入口です。
だからこそリネームでは、「昔の事情を引きずった名前になっていないか」「今の責務や文脈をちゃんと表せているか」などを確認することが大事だと思っています。
自分自身も、こうした視点を忘れずに、コードに向き合っていけたらなと思っています。
ここまで読んでいただいてありがとうございます!
いかがだったでしょうか?私が入社してから、レビューでフィードバックをいただいた内容を、5パターンにまとめて見ました!
この記事が誰かの役に立てると幸いです☺️
Discussion