うねりコード

結城浩さんの、かなり昔のページにこんなものがありました。

うねりコード - 【アンチパターン】遠くから見ると、画面上でコードがうねっている。
http:// www.hyuki.com/yukiwiki/wiki.cgi?%A4%A6%A4%CD%A4%EA%A5%B3%A1%BC%A5%C9

記事を引用いたします。

98/ 317 (火) うねりコード 大学時代に私の同期のN村がモニタを見ながら、「悪いコードは遠くから見てもわかるんだよ、ほら、うねってるだろ」と言っていた。当時は笑うだけだったが、今は溜め息をつくばかりである。身の回りにあるコードはこんなのばかりだ。

一応、「うねりコード」とは何であるのか説明すると、一つの関数のコードが異常に長く、インデントが異常に深く、そしてこれが一番重要なのだが、読んでいるうちに既視感 (デジャブ) に襲われるようなコードの事である。

うねりサンプル

うねりコードを具体的なサンプルで見るとこんな感じです。
(あくまでイメージとしてご覧ください。)

 1var dispShopList = function(var userId, var shopId) {
 2    var shopItem = Shop.get(shopId);
 3    if (shopItem.type == "Boost") {
 4        // 前回購入日からN日間は割引適用
 5        if (hasHistory(userId, shopId)) {
 6            var waribiki = Shop.getWaribiki(shopId);
 7            if (waribiki) {
 8                var price = shopItem.price - waribiki;
 9                if (price <= 0) {
10                    exit("無料はあり得ない");
11                }
12            }
13        } else {
14           var price = shopItem.price;
15        }
16        // Boostアイテムはリアル通貨のみ
17        if (hasMoney(userId, price)) {
18            if (confirm("購入します。よろしいですか??")) {
19                Shop.buy(userId, shopId);
20            } else {
21                dialog("キャンセルしました。");
22            }
23        } else {
24            dialog("お金が足りません");
25        }
26    } else if (shopItem.type == "Ticket") {
27        // Ticketアイテムはゲーム内通貨のみ
28        if (hasGold(userId, shopItem.price)) {
29            var itemId = Shop.toItemId(shopId);
30            if (isMax(userId, itemId)) {
31                if (confirm("購入します。よろしいですか??")) {
32                    Shop.buy(userId, shopId);
33                } else {
34                    dialog("キャンセルしました。");
35                }
36            } else {
37                dialog("これ以上所持できません。");
38            }
39        } else {
40            dialog("通貨が足りません");
41        }
42    } else if (shopItem.type == "Extend") {
43        // Extendアイテムはゲーム内通貨のみ
44        if (hasGold(userId, shopItem.price)) {
45            var itemId = Shop.toItemId(shopId);
46            if (isMax(userId, itemId)) {
47                if (confirm("購入します。よろしいですか??")) {
48                    Shop.buy(userId, shopId);
49                } else {
50                    dialog("キャンセルしました。");
51                }
52            } else {
53                dialog("これ以上拡張できません。");
54            }
55        } else {
56            dialog("通貨が足りません");
57        }
58    }
59}
60

うねっていますね。
ある程度経験があるエンジニアは既視感を感じるサンプルでしょう。

上記はネストが 4 つ程度のため、まだ読めると思います。
しかし (コードの負債が) 過酷なプロジェクトは、四万十川のように、もっともっと長大で、もっともっと深いうねりを内部にたたえています。

腐るシステム

うねったコードは絶妙なバランスの上に成り立っている場合が多いです。

たとえば、機能追加で何か変数に代入したり、処理の順序を変更した場合にすぐ何かしらの影響が出てしまいます。
修正が終わってからテストすると、不具合が連発して巻き戻す羽目になることも珍しくありません。

変更に弱く、そもそもコードを読むことがプログラマの大きな負担になってしまうため、誰も触れたがらずに塩漬けになってしまうのですね。
塩漬けになったコードがあるプロジェクトは、大抵が最終的に腐ります。

うねったコードと戦う武器を持つ

プロジェクトを腐らせずに成長させるには、、、

やはり、銀の弾丸はありませんから、地道に改善を重ねる体制を構築するしかないと考えています。

たとえ1週1時間でも、1日30分でも、継続的に手を加えていって コードと仲良く なることが大切ではないでしょうか。

具体的なテクニック

大抵のコードは パーツをメソッドに切り出して いくだけで、格段に良くなります。

これはメソッド抽出として、書籍『リファクタリング』でも第一に紹介されているパターンであり、長すぎる関数と戦うために必ず身に着けておきたいテクニックです。


先のコードに手を加えてみましょう。
ブーストアイテムの価格決定は、関数化することですっきりしそうです。

※実際にはテストを書いてから、リファクタリングすることを推奨します!

ステップ1. 切り出す部分のマーキング

先ほどのサンプルで、切り出す部分をコメントアウトしました。

 1if (shopItem.type == "Boost") {
 2/*
 3    // 前回購入日からN日間は割引適用
 4    if (hasHistory(userId, shopId)) {
 5        var waribiki = Shop.getWaribiki(shopId);
 6        if (waribiki) {
 7            var price = shopItem.price - waribiki;
 8            if (price <= 0) {
 9                exit("無料はあり得ない");
10            }
11        }
12    } else {
13        var price = shopItem.price;
14    }
15*/
16    // Boostアイテムはリアル通貨のみ
17    if (hasMoney(userId, price)) {
18        if (confirm("購入します。よろしいですか??")) {
19            Shop.buy(userId, shopId);
20        } else {
21            dialog("キャンセルしました。");
22        }
23    } else {
24        dialog("お金が足りません");
25    }
26    :
27}
28

ステップ2. 切り出しの実行

マーキング部分を、単純に別関数として切り出します。

このステップではブロックを移動するだけなので、当然まだ動きません。

 1if (shopItem.type == "Boost") {
 2/*
 3
 4*/
 5    // Boostアイテムはリアル通貨のみ
 6    if (hasMoney(userId, price)) {
 7        if (confirm("購入します。よろしいですか??")) {
 8            Shop.buy(userId, shopId);
 9        } else {
10            dialog("キャンセルしました。");
11        }
12    } else {
13        dialog("お金が足りません");
14    }
15    :
16}
17var getPrice = function() {
18    // 前回購入日からN日間は割引適用
19    if (hasHistory(userId, shopId)) {
20        var waribiki = Shop.getWaribiki(shopId);
21        if (waribiki) {
22            var price = shopItem.price - waribiki;
23            if (price <= 0) {
24                exit("無料はあり得ない");
25            }
26        }
27    } else {
28        var price = shopItem.price;
29    }
30}
31

ステップ3. 前後処理を整える

移動元、移動先の処理を整えます。

 1if (shopItem.type == "Boost") {
 2
 3    var getPrice(shopId, shopItem);
 4
 5    // Boostアイテムはリアル通貨のみ
 6    if (hasMoney(userId, price)) {
 7        if (confirm("購入します。よろしいですか??")) {
 8            Shop.buy(userId, shopId);
 9        } else {
10            dialog("キャンセルしました。");
11        }
12    } else {
13        dialog("お金が足りません");
14    }
15    :
16}
17var getPrice = function(var shopId, var shopItem) {
18    // 前回購入日からN日間は割引適用
19    if (hasHistory(userId, shopId)) {
20        var waribiki = Shop.getWaribiki(shopId);
21        if (waribiki) {
22            var price = shopItem.price - waribiki;
23            if (price <= 0) {
24                exit("無料はあり得ない");
25            }
26        }
27    } else {
28        var price = shopItem.price;
29    }
30    return price;
31}
32

とりあえずこれでコードが動くようになりました。

ほんのわずかですが、本体コードを短くすることができ、 そのコードが何をやっているか 把握しやすくなりましたね。

このように 1つ 1つ のコードを短くすることに成功すれば、コードの把握が容易になり、改善しやすさは格段に向上します。


ついでに切り出した関数をもうちょっとだけ良くしてみましょう。

if elseを逆にして、すぐreturnする ことで、さらにコードを整理することができます。

 1var getPrice = function(var shopId, var shopItem) {
 2    // 前回購入日からN日間は割引適用
 3    if (hasHistory(userId, shopId) == false) {
 4        return shopItem.price;
 5    }
 6    var waribiki = Shop.getWaribiki(shopId);
 7    if (waribiki == false) {
 8        return shopItem.price;
 9    }
10    if (shopItem.price <= waribiki) {
11        exit("無料はあり得ない");
12    }
13    return shopItem.price - waribiki;
14}
15

終わりに

先頭でご紹介した結城浩さんの記事日付は 1998年 です。
私がプログラミングを勉強するずっとずっと前から、コードと戦ってベストプラクティスを模索してきた方がおられることに感動、感謝いたします。

しかし、上記の記事から 20年 近く経過した今でも、うねったコードは量産され続けていると感じます。

プログラミングと作文は似ています。

想いを書きなぐっただけでは良いものにはなりません。
ですから、推敲に推敲を重ねて良くしていくステップが必要なのですね。

コードは3回書き直す 」ぐらいがちょうどいいのです。


レガシーコード改善ガイド (Object Oriented SELECTION)

マイケル・C・フェザーズ
出版社:翔泳社  発売日:2009-07-14

Amazonで詳細を見る