はじめに
この記事では、ソフトウェア設計において
- 命名がなぜ重要なのか??
- 命名と小さく分けることの関係
についてまとめました。
動画でもまとめたのでこちらもご覧ください。
命名がなぜ重要性なのか??
命名が微妙だと後から見た人が混乱したり、
略語でamt(金額を表してるつもり)より
amountの方がわかりやすい。
と、ここまでは当たり前のように思われるかもしれません。
では、
もし変数名にふわっとした命名をするとどうでしょう??
いろんな意味でその変数を使いまわされてしまうかもしれません。
逆に、目的を正確に表した変数名だと使いまわされたり、
誤用される危険を減らせるかもしれません。
もしメソッド名にふわっとした命名をするとどうでしょう??
そのメソッドで複数の目的を同時に実現しようとしてしまうかもしれません。
逆に、1つの目的を表す適切な表現で命名できれば、
それ以外の目的に関するロジックが混ざる危険を減らせるかもしれません。
もしクラス名にふわっとした命名をするとどうでしょう??
そのクラスにいろんな関心事が集まってくるかもしれません。
逆に、特定の関心事を表現する命名ができれば、
似たような別の関心事が集まってくる危険を減らせるかもしれません。
このように、命名1つで
コードが良い方へ向かいやすくすることも、
コードが悪い方へ向かいやすくすることも
できてしまいます。
どこまでを1まとまりとして取り扱い、
どこからを分割して取り扱うか??
という、難しい設計判断と命名は強く結びついており、
命名は確実にコードや設計の質に影響を与えます。
命名とコードとの関係
ここからは悪い命名とコード、
良い命名とコードを比較しながら見ていきます。
悪い変数名と良い変数名
商品の金額に消費税と手数料を加算した請求金額を求める必要があるケースを考えます。
この場合、amount(金額)という変数が登場するとどうでしょう??
var amount = 1000.0
val taxRate = 0.1
amount += amount * 0.01
amount *= (1 + taxRate)
amountは商品のオリジナルな金額なのか、
それとも最終的な請求金額なのか、
手数料額は加算済みなのか??加算前なのか??
を慎重に読み進めなければなりません。
amountは商品のオリジナルな金額だったのが、
最後は請求金額に変数の意味が変わっていて
amountを使うプログラマーが混乱する可能性が高いです。
val amount = 1000.0
val taxRate = 0.1
val feeRate = 0.01
val fee = amount * feeRate
val billingAmount = (amount + fee) * (1 + taxRate)
請求金額と商品のオリジナル金額は別の変数として扱い、
途中で商品のオリジナル金額が請求金額に変わることは無くなりました。
また、オリジナル商品の1%が手数料となることもこちらの方が、
初めてこのコードを見たプログラマーでもパッと理解できると思います。
悪いクラス名と良いクラス名
例えば、クラス名がふわっとしてしまうと
いろんな関心事の属性やメソッドが集まってきてしまいます。
同一の概念のようで関心事が異なるものが集まってくると
特に厄介です。
class Goods(
val amount: Int,
val stock: Int, // 注文時のみの関心事
val size: Size, // 配送時のみの関心事
val name: String,
) {
fun calculateAmount(): Int {
// 送料の計算ポイ??
return if (size.height + size.width + size.depth > 190 || size.weight > 10) {
1000
}else {
500
}
}
・・・
}
data class Size(
val width: Int,
val height: Int,
val depth: Int,
val weight: Int,
)
商品の在庫数は注文時の関心事で、
商品のサイズは発送時の関心事で、
それらが混ざってしまっています。
さらに、
いろんな関心事が混ざっているせいで、
どの関心事に対するメソッドなのかも分かりずらいです。
金額を計算するといっても、
注文時の請求金額なのか、
発送時の送料なのかなどもメソッド名だけでは分からず混乱しそうですね。
class DeliveryItem( // 配送品
val size: Size,
) {
fun calculatePostage(): Int {
return if (size.height + size.width + size.depth > 190 || size.weight > 10) {
1000
}else {
500
}
}
}
class OrderItem( // 注文品
val amount: Int,
val feeRate: Double = 0.01,
val taxRate: Double = 0.1,
) {
fun calculateBillingAmount(): Int {
val fee = amount * feeRate
return floor((amount + fee) * (1 + feeRate)).toInt()
}
}
商品を
発送時に利用する発送品と、
注文時に利用する注文品にクラスを分けました。
これによって、
仕様変更時に、
発送に関する変更が、
注文に関する箇所に影響を与えることはなくなります。
商品クラスを小さく分けたおかげで変更時の影響範囲も小さくできたのです。
悪いメソッド名と良いメソッド名
例えば、メソッド名がふわっとしてしまうと
一つのメソッドの中でいろんな処理をする神メソッドになりがちです。
class Goods(
val amount: Int,
val stock: Int,
val size: Size,
val name: String,
) {
fun calculateAmount(mode: GoodsType): Int {
return when (mode) { // mode分けして配送料か請求金額のどちらかの算出を行う
GoodsType.DELIVERY -> {
if (size.height + size.width + size.depth > 190 || size.weight > 10) {
1000
}else {
500
}
}
GoodsType.ORDER -> {
val fee = amount * feeRate
floor((amount + fee) * (1 + feeRate)).toInt()
}
}
}
・・・
このメソッドを利用するプログラマーは、
商品のどの料金を計算するメソッドなのか推測が難しいです。
そのため、
中身のロジックを注意深く追わなければ使えるのか判断できません。
(ということは誤用される可能性が十分あるとも言えます)
また、もしこういうメソッドがあると、
何かしらの金額計算が機能追加される際、
おそらくさらにmode分けをして分岐が激しくなっていくでしょう。
テストコードも書き辛くなっていき、
配送料の計算ロジックが変更になるだけでも、
請求金額の計算ロジックへの影響を心配する必要が出てきます。
class DeliveryItem(
val size: Size,
val amount: Int,
val feeRate: Double = 0.01,
val taxRate: Double = 0.1,
) {
fun calculatePostage(): Int { // 配送料の計算のみ行う
return if (size.height + size.width + size.depth > 190 || size.weight > 10) {
1000
}else {
500
}
}
}
class OrderItem(
val amount: Int,
val feeRate: Double = 0.01,
val taxRate: Double = 0.1,
) {
fun calculateBillingAmount(): Int { // 請求金額の計算のみ行う
val fee = amount * feeRate
return floor((amount + fee) * (1 + feeRate)).toInt()
}
}
メソッド名が特定の狭い目的での金額計算を表しており、
使う側も直感的にメソッドが何をしようとしているのか理解しやすいです。
仕様変更時の影響も限定され、
テストコードもシンプルに書けます。
ここまで良い命名と悪い命名、
およびそれぞれのコードを見てきましたが、
小さな特定の目的に特化して命名するのは非常に重要です。
そうすることで、
目的から外れるもの(命名と関係ない目的のもの)は別のクラスやメソッドして切り出す方向に向かいやすくなり、
複数の目的が1つのクラスやメソッドに混ざりにくくなる方向に向かいやすくなり、
どこに何が書いてあるかを使う側も直感的に理解しやすくなる。
と僕は考えています。
その結果、
メソッドやクラスの誤用が減り、
強く関連するものが1箇所に集まり、
仕様変更時の影響範囲を狭く限定的にできます。
僕が体験した現場でのあるあるですが・・・
リリース当初は綺麗に命名できて関心事を小さく分けて整理できていても、
保守開発で仕様変更や機能追加を重ねるごとに当時のクラスやメソッドに
徐々にいろんな意味を持たせてしまって結果的に命名がぼやける。
ということがよくありました。
目の前の仕様変更や機能追加の対応を、
リリース当時の命名の意図を無視したり、
人が入れ替えが激しく引き継ぎがほぼされなかったり、
理由は様々あるでしょうが、
リリース後の保守開発においても、
命名には注意し続けましょう。
そしてその命名は引き継ぎがろくにされない現場でも、
直感的に分かる、誤用されにくい命名ができるとなお良いでしょう。
まとめ
1つの小さな目的に特化した命名をクラスやメソッドにすることで、
関心事を小さくメソッドやクラスに分けることに繋がる。
その結果、
変更時の影響の波及を小さい範囲に抑えることに繋がる。
だから命名をふわっとやっちゃダメ!!
今回のテーマは以上です。
初めてソフトウェア設計を行う方向けに
記事を作ったのでそちらも是非ご覧ください。
初めてソフトウェア設計をする方に向けて|新人プログラマー時代の自分に伝えたいこと参考文献
第1章 小さくまとめてわかりやすくする
現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法
第10章 名前設計ーあるべき構造を見破る名前ー
良いコード/悪いコードで学ぶ設計入門―保守しやすい 成長し続けるコードの書き方