『Tidy First?』の第Ⅰ部「整頓」の全15章について、コーディングの Tips としてまとめます。
本書は各章が短くまとまっている一方で、サンプルコードが少なく、説明不足を感じるところがありました。そこで、必要に応じて ChatGPT-5 と壁打ちをして、サンプルコードを生成しています。
第Ⅰ部
リファクタリングとは「振る舞いを変更することなく構造を変更する」ことである。そして、整頓はリファクタリングのサブセット(一部)である。
第Ⅰ部では、リファクタリングの一環である「整頓」の方法について、その小技をまとめる。
1章 ガード節
ネストしたガード節は読みづらい。
fun process(value: Int) {
if (value > 0) {
if (value < 100) {
println("処理を実行します")
}
}
}
ネストを解消して早期 returnにすることで、コードの意図がすぐ読める。
fun process(value: Int) {
if (value <= 0) return
if (value >= 100) return
println("処理を実行します")
}
2章 デッドコード
実行されないコードは消す。
「もしあとで必要になったら?」と考える必要はない。そのためにバージョン管理がある。
3章 シンメトリーを揃える
コードには一貫性が必要だ。同じ問題は同じやり方で解決しよう。
やり方を1つ選び、他もそれに合わせることで、読みやすさが向上する。
不要なバリエーションを見つけたら、1つずつ整頓しよう。
一貫性がない変数の遅延初期化の例:
// 前提: foo は null 許容の変数
var foo: String? = null
// パターン1: すでに値があれば返す、なければ初期化して返す
fun getFoo1(): String {
if (foo != null) return foo!!
foo = "computed value"
return foo!!
}
// パターン2: null チェックを先に書く方法
fun getFoo2(): String {
if (foo == null) {
foo = "computed value"
}
return foo!!
}
// パターン3: 三項演算子的にまとめた書き方(ここではエルビス演算子を利用)
fun getFoo3(): String {
foo = foo ?: "computed value" // null なら代入して返す
return foo!!
}
// パターン4: 代入が式であることを利用(ややトリッキー)
fun getFoo4(): String {
return foo ?: run { // foo が null なら run ブロックで代入して返す
foo = "computed value"
foo!!
}
}
// パターン5: もっとトリッキーで読みづらい
fun getFoo5(): String {
return foo ?: "computed value".also { foo = it }
}
4章 新しいインターフェイス、古い実装
古くて複雑なインターフェイスを持つルーチン(継続的かつ小規模なコードのまとまり≒関数)は、そのまま使わず、新しいインターフェイスを用意し、その中で古いインターフェイスを呼び出そう。
例えば、古いコードがこうだとする。
// 古い実装(複雑)
fun legacyProcess(input: String, flag: Boolean, options: Map<String, Any>?): String {
// 何かややこしい処理
return "result"
}
呼び出すたびにフラグやオプションを意識するのは面倒。
そこで「新しいインターフェイス」を作る。
// 新しい使いやすいインターフェイス
fun process(input: String): String {
return legacyProcess(input, flag = true, options = null)
}
これで呼び出し側はシンプルに書ける。
val result = process("data")
こうすることで、呼び出し側のコードは分かりやすく、実装の複雑さは新しいインターフェイスの中に隠すことができる。
5章 読む順番
コードは、読み手が遭遇したい順番に並べ替えよう。
もしファイルを最初から最後まで読んで、ようやく欲しかった情報を見つけたとしたら、それは遅すぎる。
整頓前:
class ReportGenerator {
private fun formatReport(data: List<String>): String {
return data.joinToString(", ")
}
private fun fetchData(): List<String> {
return listOf("A", "B", "C")
}
// 実はこれが一番知りたかったエントリーポイント
fun generate(): String {
val data = fetchData()
return formatReport(data)
}
}
整頓後:
class ReportGenerator {
// まず入口(最も重要な関数)を先に書く
fun generate(): String {
val data = fetchData()
return formatReport(data)
}
// 詳細処理はその後に書く
private fun fetchData(): List<String> {
return listOf("A", "B", "C")
}
private fun formatReport(data: List<String>): String {
return data.joinToString(", ")
}
}
最初に generate() が目に入るので、「このクラスの役割はレポート生成で、fetchData() と formatReport() を使っている」とすぐ理解できる。
6章 凝集の順番
変更を簡単にするには、コードの順番を入れ替えて、変更対象の要素を隣接させよう。
- 結合しているルーチンは、隣り合わせにする
- 複数ファイルがあるなら、結合しているものを同じディレクトリに置く
- もしリポジトリをまたいでいたら、同じリポジトリにまとめる
コードを読んで「この振る舞いを変えたい」と思ったとき、以下のような状態はつらい。
- その変更箇所があちこちに散らばっている
- 変更のために複数ファイルや複数リポジトリをまたいで触らないといけない
整頓によって凝集度を高めれば、振る舞いの変更が簡単になる。
7章 変数宣言と初期化を一緒の場所に移動する
変数の宣言と初期化が離れていると、コードは読みづらくなる。
初期化の位置にたどりつくころには「この変数は何のためだったか?」という文脈を忘れてしまうからだ。
例えば、整頓前のコードはこうだ。
fun fn() {
var a: Int // a を宣言
// ...aを使わない何らかのコード
a = 10 // a を初期化
var b: Int // b を宣言
println("ここで a を使う: $a")
b = a * 2 // b を初期化
println("ここで b を使う: $b")
}
宣言と初期化を近づけて整頓する。
fun fn() {
val a: Int = 10 // a を宣言と同時に初期化
// ...aを使わない何らかのコード
println("ここで a を使う: $a")
val b: Int = a * 2 // b も使う直前で宣言と同時に初期化
println("ここで b を使う: $b")
}
整頓後も改善の余地はあるが、ここでのポイントは 「宣言と初期化を近づける」という小さなステップ に絞って変更していることだ。
順番を変えて試すのもよい。
- 変数をそれぞれ使う直前に宣言・初期化するのか
- 関数の先頭でまとめて宣言・初期化するのか
どちらが読みやすく理解しやすいか、読み手の体験を想像して判断するとよい。
もし構造を直そうとして振る舞いを誤って変えてしまっても、コードを元に戻せばよいだけだ。大切なのは、整頓は小さなステップで進めるということだ。
8章 説明変数
大きくて複雑な式の一部を理解したら、その部分を意図がわかる変数に抽出し、適切な名前をつけよう。
fun createInvoiceSummary(quantity: Int, price: Int): InvoiceSummary {
return InvoiceSummary(
(quantity * price), // 小計
((quantity * price) * 0.1).toInt() // 消費税(10%)
)
}
変更を加える前に、まず整頓する。
fun createInvoiceSummary(quantity: Int, price: Int): InvoiceSummary {
val subtotal = quantity * price // 小計
val tax = (subtotal * 0.1).toInt() // 消費税(10%)
return InvoiceSummary(subtotal, tax)
}
こうして式を分離すれば、変更が簡単になるし、次にこのコードを読むときの理解も早くなる。
そして重要なのは、整頓のコミットと振る舞いを変更するコミットは必ず分けること。
9章 説明定数
リテラル定数(数値や文字列を直接書くこと)は避け、意味のある名前を持つシンボリック定数に置き換えよう。
これによりコードの意図が明確になり、読みやすく、変更しやすくなる。
-
シンボリック定数: 意味のある名前をつけた定数(例:
PAGE_NOT_FOUND) -
リテラル定数: コード中に直接書かれた数値や文字列(例:
404、"admin")
リテラル定数を直接使った例:
fun handleResponse(response: Response) {
if (response.code == 404) {
println("ページが見つかりません")
}
}
シンボリック定数を使った例:
const val PAGE_NOT_FOUND = 404
fun handleResponse(response: Response) {
if (response.code == PAGE_NOT_FOUND) {
println("ページが見つかりません")
}
}
10章 明示的なパラメーター
データ(引数)はルーチン(関数)に明示的に渡そう。
そうすることで必要な情報が一目でわかり、コードは読みやすくなる。
例えば、パラメーターをマップでまとめて渡すと、どんなデータが必要なのか呼び出し側から見えにくい。
fun main() {
val params = mapOf("a" to 1, "b" to 2)
foo(params)
}
fun foo(params: Map<String, Int>) {
// 何かしらの処理...
// params["a"]を使った何かしらの処理...
// 何かしらの処理...
// params["b"]を使った何かしらの処理...
// 何かしらの処理...
}
ルーチンを分割し、上部でパラメータを集めて、次の部分に明示的に渡すようにすると、意図がはっきりする。
fun main() {
val params = mapOf("a" to 1, "b" to 2)
foo(params)
}
// foo では必要なパラメーターを明示的に渡す
fun foo(params: Map<String, Int>) {
fooBody(params.getValue("a"), params.getValue("b"))
}
// 主要な処理は fooBody に分離する
fun fooBody(a: Int, b: Int) {
// 何かしらの処理...
// a を使った何かしらの処理...
// 何かしらの処理...
// b を使った何かしらの処理...
// 何かしらの処理...
}
11章 ステートメントを小分けにする
大きなコードの塊は、空行で区切るだけで読みやすくなる。
例えば次のようにすべてを詰め込んで書くと、処理の流れが見えにくい。
fun processOrder(order: Order) {
val validated = validate(order)
val priced = calculatePrice(validated)
val discounted = applyDiscount(priced)
saveOrder(discounted)
notifyCustomer(discounted)
}
空行を入れて「まとまりごと」に分けるだけで、処理の段階がはっきりする。
fun processOrder(order: Order) {
val validated = validate(order)
val priced = calculatePrice(validated)
val discounted = applyDiscount(priced)
saveOrder(discounted)
notifyCustomer(discounted)
}
このようにステートメントを小分けにすると、どの処理がひとまとまりか直感的にわかる。
そのうえで、さらに 説明変数(8章)、ヘルパー抽出(12章)、説明コメント(14章) などにつなげていける。
12章 ヘルパーを抽出する
ルーチン内で目的がはっきりしていて他とあまり関わらない処理は、ヘルパールーチンとして切り出そう。
名前は「どう機能するか」ではなく、「何のための処理か」を表すようにつける。
ケース1: 大きなルーチンの一部を切り出す
ルーチンの一部だけを修正したい場合、その部分をヘルパーに抽出すれば、変更が局所化される。
fun routine() {
// ...変更しない部分...
// ...割引計算の処理...
// ...変更しない部分...
}
抽出したヘルパーに「処理内容」ではなく「目的」を表す名前をつける。
fun applyDiscount() { // 「割引を適用する」という目的
// ...割引計算の処理...
}
fun routine() {
// ...変更しない部分...
applyDiscount()
// ...変更しない部分...
}
ケース2: 時間的な結合を表す
「ある処理は必ず別の処理の前に実行される必要がある」という場合も、ヘルパーにまとめて表現できる。
val foo = Foo()
foo.a() // 先に呼ばないと…
foo.b() // …正しく動かない
↓
val foo = Foo()
foo.ab() // ab() の中で a() → b() の順序が保証される
class Foo {
fun a() { println("a 実行") }
fun b() { println("b 実行") }
fun ab() {
a()
b()
}
}
ヘルパー抽出のメリット
- 変更箇所を局所化できる
- 時間的な結合を安全に表現できる
- 名前が目的を伝えるのでコードの意図が明確になる
13章 ひとかたまり
コードが細かく分割されすぎると、逆に理解しづらくなる。そんなときは一度まとめて「ひとかたまり」にし、そこから整頓していこう。
小さな部品化は結合を減らし凝集を高める効果があるが、やりすぎると理解を妨げることがある。まずはコードを集めて読みやすくし、その後で必要な単位に分割すればよい。
特に以下の兆候が見えたら整頓を考えよう。
- 長くて繰り返しの引数リスト
- 繰り返しのコード(特に繰り返しの条件式)
- ヘルパールーチンのよくない名前付け
- 共有の可変データ構造
1. 長くて繰り返しの引数リスト
悪い例: 何度も同じ引数リストを繰り返している
fun createUser(name: String, age: Int, email: String, phone: String) { /*...*/ }
fun updateUser(name: String, age: Int, email: String, phone: String) { /*...*/ }
fun deleteUser(name: String, age: Int, email: String, phone: String) { /*...*/ }
改善例: データクラスを導入してまとめる
data class User(val name: String, val age: Int, val email: String, val phone: String)
fun createUser(user: User) { /*...*/ }
fun updateUser(user: User) { /*...*/ }
fun deleteUser(user: User) { /*...*/ }
2. 繰り返しのコード
悪い例: 同じ処理をあちこちで繰り返している
fun saveFileA(path: String) {
val file = File(path)
if (!file.exists()) file.createNewFile()
file.writeText("A data")
}
fun saveFileB(path: String) {
val file = File(path)
if (!file.exists()) file.createNewFile()
file.writeText("B data")
}
改善例: 共通化
fun saveFile(path: String, content: String) {
val file = File(path)
if (!file.exists()) file.createNewFile()
file.writeText(content)
}
3. 繰り返しの条件式
悪い例: 同じ条件をあちこちで書いている
if (user.age >= 18 && user.isVerified) {
println("アクセス許可")
}
if (user.age >= 18 && user.isVerified) {
println("購入可能")
}
改善例: 条件を関数に切り出す
fun canAccess(user: User): Boolean = user.age >= 18 && user.isVerified
if (canAccess(user)) println("アクセス許可")
if (canAccess(user)) println("購入可能")
4. ヘルパールーチンのよくない名前付け
悪い例: 名前があいまいで何をしているのか分からない
fun doStuff(user: User) { /*...*/ }
fun handleIt(data: String) { /*...*/ }
改善例: 意味が分かる名前にする
fun sendWelcomeEmail(user: User) { /*...*/ }
fun saveLog(data: String) { /*...*/ }
5. 共有の可変データ構造
悪い例: グローバルで可変のリストを共有している
val sharedList = mutableListOf<String>()
fun addItem(item: String) {
sharedList.add(item) // どこからでも変更できる
}
fun removeItem(item: String) {
sharedList.remove(item)
}
改善例: 不変にする、または責務を限定する
class ItemRepository {
private val items = mutableListOf<String>()
fun add(item: String) = items.add(item)
fun remove(item: String) = items.remove(item)
fun getAll(): List<String> = items.toList() // 外部には不変リストを返す
}
14章 説明コメント
コードだけでは伝わらない意図や背景を、未来の読み手のためにコメントとして残そう。
- 「なぜこの処理が複雑なのか」「何を避けるための工夫なのか」を説明する
- 自分にとっては明白なことでも、第三者が疑問を抱きそうなことを先回りして説明する
- ヘッダーコメントを書く
/**
* PaymentProcessor
*
* このクラスはクレジットカード決済の処理を担当する。
* 外部サービス Stripe API をラップしており、
* 支払い成功時には InvoiceService に通知する。
*
* 将来、決済サービスを切り替える際はこのクラスを中心に修正する。
*/
class PaymentProcessor {
fun process(amount: Int) { ... }
}
- コードの欠陥や結合を見つけたら、すぐに修正できない場合は注意書きを残す
fun handleStatus(status: String) {
when (status) {
"NEW" -> println("新規処理")
"DONE" -> println("完了処理")
// 注意: 別のケースを追加する場合は必ず foo() も修正すること
}
}
fun foo(status: String) {
// handleStatus と同じステータス分岐をここでもしている
// 本来は重複をなくすべきだが、今はコメントで依存関係を明示しておく
when (status) {
"NEW" -> println("foo: 新規処理")
"DONE" -> println("foo: 完了処理")
}
}
15章 冗長なコメントを削除する
コードに書いてあることを繰り返すだけのコメントは不要。
たとえば次のようなコメントは無意味で、読み手の時間を無駄にする。
fun isAdult(age: Int): Boolean {
// 20歳以上ならtrueを返す
return age >= 20
}
以上です。