はじめに
先日、「名前設計」についてに下記の記事を執筆しました。
界隈で有名な方が執筆するような大した記事ではないですが、名前を適切に設定することの重要性を私なりに解釈して執筆させていただきました。
「センス良く、かつ適切な名前を設定する」って簡単に聞こえます。
ですが、実際の開発現場では変数や関数の命名に悩まされることが多々発生するように思えます。
その問題の手助けをしてくれる本が「リーダブルコード」という本です。
エンジニア界隈で一番有名な本と言っても過言ではないですね。
もしこの本を読んだことがないという方がいるのであれば、この記事を読むよりリーダブルコードを読んだ方が有意義な時間を過ごせるでしょう。
この記事はリーダブルコードの2章から3章の一部について書いていこうと思います。
- 2章 名前に情報を詰め込む
- 3章 誤解されない名前
目次
名前に情報を詰め込む
まず具体的な動詞や名詞を選定する前に、根本的な命名の考え方を頭に入れておかなければいけません。
リーダブルコードには6点の考え方があると記載されています。
- 明確な単語を選ぶ
- 汎用的な名前を避ける
- 抽象的な名前より具体的な名前を使う
- 接尾辞や接頭辞を使って情報を追加する
- 汎用的な名前を避ける
- 名前のフォーマットで情報を伝える
その中でも
- 明確な単語を選ぶ
- 汎用的な名前を避ける
を見ていきましょう。
明確な単語を選ぶ
func getUser(from: String) -> User {
// ...中略
}
この関数はユーザーの情報を取得して返却してくれる関数だな!と読み解くことができます。
ですが、上記の関数の命名は不明確と言えます。
なぜなら、「どこからユーザーの情報を取得するのか」
の情報がgetUser(from: String)
だけでは読み取ることができないからです。
改善する
この記事を見てみると取得系だけでも14個の類義語があります。
もしこの関数が、API経由でユーザー情報を取得するような関数の場合は下記のコード適切なのではないかと考えます。
// 🐤 改善前
func getUser(from: String) -> User {
// ...中略
}
// 🐔 改善後
func fetchUser(url: String) -> User {
// ...中略
}
getXXXX
という動詞自体が便利が故に、呼び出し側の想像力を無駄に働かせてしまいます。
類語辞典で調べたり、命名についての記事を読んでみたりして適切な命名を心がけてみましょう。
汎用的な名前を避ける
func euclideanNorm(vector: [Double]) -> Double {
var returnValue = 0.0
vector.forEach { returnValue += $0 * $0 }
return sqrt(returnValue)
}
ユークリッドノルムって何だろうと思った方は下記の記事をご覧ください。
https://manabitimes.jp/math/1269
並んだ実数をそれぞれ二乗して足し合わせた値の平方根が「ユークリッドノルム」と言うものらしいです。
理系ではないので、詳しいことは良くわかりません。
さて、このコードの何が良くないでしょうか。
それはreturnValue
です。
この命名から伝わる情報は、この「関数の戻り値であること」だけです。
ですが実際にreturnValue
が格納している値は「二乗の合計値」です。
改善する
returnValue
という汎用的な名前から、具体的な名前に変更しましょう。
// 🐤 改善前
func euclideanNorm(vector: [Double]) -> Double {
var returnValue = 0.0
vector.forEach { returnValue += $0 * $0 }
return sqrt(returnValue)
}
// 🐔 改善後
func euclideanNorm(vector: [Double]) -> Double {
var sumSquares = 0.0
vector.forEach { sumSquares += $0 * $0 }
return sqrt(sumSquares)
}
sumSquares
という変数を見て、「二乗された合計」が格納されているんだなと名前だけで理解することが出来ます。
この改善には嬉しい副作用があります。それは、バグの検知がしやすくなる点です。
例えば下記のバグコードがあるとしましょう。
func euclideanNorm(vector: [Double]) -> Double {
var sumSquares = 0.0
vector.forEach { sumSquares += $0 } // 二乗されていない👿
return sqrt(sumSquares)
}
変数でsumSquares
を定義しているおかげで
「二乗された合計なのにシンプルに合計をしている・・・!?」という発見の手助けになることがあります。
returnValue, value, foo, bar, org, tmp...
汎用的な名前の変数を見つけた際には、それが具体的な名前に置換することが出来ないか常に考えるようにしましょう。
汎用的な名前って絶対に使っちゃいけない?
リーダブルコードでは汎用的な名前が全て悪だとは記載されていません。
汎用的な名前が役に立つこともある。
と記載されています。気になる方は、ぜひ読んでみてください。
誤解されない名前
自分の中で納得のいく命名を施した関数が、他人の誤解によりバグを生み出してしまうケースがあります。
- 限界値を含めるときは
min
とmax
を使う - 範囲を指定するときは
first
とlast
を使う - 包含/排他的範囲には
begin
とend
を使う - ブール値の名前
- ユーザーの期待に合わせる
- 複数の名前を検討する
最初の1つだけご紹介させていただきます。
限界値を含めるときはmin
とmax
を使う
class Cart {
let items: [Items]
let CART_TOO_BIG_LIMIT = 10
func add(item: Item) throws -> Cart {
if items.count >= CART_TOO_BIG_LIMIT {
throw CartError.exceedItems
}
// ...商品カートに商品を追加
}
}
このコードにはバグがある。
それはカートの商品に10個目を追加した時点で例外を投げてしまうからである。
修正するには下記のようにすればいいだろう。
if items.count > CART_TOO_BIG_LIMIT {
throw CartError.exceedItems
}
改善する
ロジックを変更すれば確かに意図した動きになるでしょう。
ですが、根本的な問題解決には至っていないと思います。
なぜバグを書いてしまったのか、その原因を解決することが根本的な問題解決と言えるでしょう。
原因はCART_TOO_BIG_LIMIT
です。
CART_TOO_BIG_LIMIT
この曖昧な命名が「未満なのか以下なのか」が分からないからです。
// 🐤 改善前
class Cart {
let items: [Items]
let CART_TOO_BIG_LIMIT = 10
func add(item: Item) throws -> Cart {
if items.count >= CART_TOO_BIG_LIMIT {
throw CartError.exceedItems
}
// ...商品カートに商品を追加
}
}
// 🐔 改善後
class Cart {
let items: [Items]
let MAX_ITEMS_IN_CART = 10
func add(item: Item) throws -> Cart {
if items.count > MAX_ITEMS_IN_CART {
throw CartError.exceedItems
}
// ...商品カートに商品を追加
}
}
適切な名前を付けましょう。
最後に
リーダブルコードは特にプログラマになりたての方に特に読んでほしい書籍だと考えています。
この本を読む前と読んだ後では、コードへの向き合い方がまるで変わるかと思います。
人によっては、この本を読んだ後に自分のコードを見たら恥ずかしくなって目も当てれないでしょう。