nikkie-ftnextの日記

イベントレポートや読書メモを発信

読書ログ | #ミノ駆動本 14.2で紹介されるリファクタリングを手を動かして理解する

はじめに

ういっす✌️ nikkieです。

読書会に関連させて『良いコード/悪いコードで学ぶ設計入門』14章の予習事項の1つを取り上げます。

14.2を写経してミノ駆動さんのやり方を体験したい

目次

謝辞

3/3のミノ駆動本_読書pyの予習と読書会でのアウトプットで理解を深めました。
共同主催であり、参加者としてnikkieのアウトプットにお付き合いいただいたYumihiki(id:nibutan)さん、どうもありがとうございました。

14.2 ユニットテストリファクタリングのミスを防ぐ

14章「リファクタリング」は、初めて「リファクタリング」という言葉に出会ったとしても完全に理解できる章だと思います1
14.2では、現場に即したリファクタリングが解説されます。
「テストがないが仕様は明確なコード」を例に、テストを書きながらリファクタリングする、ミノ駆動さんがよくやる手法の解説です。

この手法は私にとっては初めて見るもので、一読では何をされたのかよく分かりませんでした。
「理解するために写経したい」と予習事項に挙げています。

14.2のサンプルコード

ECサイトなどの配送料計算が扱われます。
合計金額に応じて配送料が変わるというものです。

  • 合計金額がN円未満なら配送料〇〇円
  • 合計金額がN円以上なら配送料無料

サンプルコード(Javaによる実装)はサポートページから確認できます:
https://gihyo.jp/book/2022/978-4-297-12783-1/support

リファクタリング前、配送料はDeliveryManagerによって計算されます。

class DeliveryManager:
    @staticmethod
    def delivery_charge(products: Sequence[Product]) -> int:
        ...

配送料計算は、スタティックメソッドでの実装2となっています。
ここまで読んで来ると、あまりいい構造ではないという印象ですね(14.2でも解説されます)。

テストコードはないのでテストを書いてリファクタリングします。
最終的にはDeliveryChargeShoppingCartから配送料が計算されます(図14.1)。
この例は「あるべき構造がある程度わかっている場合」のリファクタリング手法を紹介する例であり、DeliveryManagerは消え去ります

結論:理解したこと

  • 最初にある構造(DeliveryManager)をリファクタリングの結果、削除したいという状況
  • 削除したいのでDeliveryManagerTestCaseは追加しない
  • あるべき構造のテストコードを書く
  • あるべき構造のテストコードを使ってDeliveryManagerが仕様を満たしていることを確認している

写経(中の)コードは以下に置いています:

リファクタリング手順に沿った写経

理解したことをnikkieの言葉で整理していきます。

1.あるべき構造の雛形を作る

DeliveryManagerリファクタリング対象でテストがないので、「まずはDeliveryManagerTestCaseを書くのかな」と思ったnikkie。
ところが、まずはあるべき構造(DeliveryChargeShoppingCart)の雛形の実装でした!

class ShoppingCart:
    def __init__(self, products: list[Product] | None = None) -> None:
        self.products: list[Product] = products or []

    def add(self, product: Product) -> "ShoppingCart":
        adding = list(self.products)
        adding.append(product)
        return self.__class__(adding)


class DeliveryCharge:
    def __init__(self, shopping_cart: ShoppingCart) -> None:
        self.amount = -1
  • ShoppingCartインスタンスにはaddメソッドでProduct(商品)を追加できます
    • ECサイトを訪れたとき買い物かごは空で、1つずつ商品が追加されるというのがうまく表されているな〜と感心しました
  • DeliveryChargeShoppingCartをもとに配送料を計算します
    • 現在の買い物かごの状態に基づいて配送料を計算するということですね

2.雛形に対してテストコード追加

あるべき構造のDeliveryChargeのテストを追加します。

class DeliveryChargeTestCase(TestCase):
    def test_pay_charge(self):
        # 買い物かごに商品を追加し、配送料の額を検証

    def test_free_charge(self):
        # 買い物かごに商品を追加し、配送料が無料(額が0円)を検証

DeliveryChargeself.amount = -1と初期化されますから、2つともテストは落ちます。
落ちるべきときに落ちるわけです。

次にDeliveryCharge__init__でテストを通すだけの実装をします。
テスト駆動開発』で言う「仮実装」のような印象を受けました。
テストが通るようになったので、このテストは正しく実装できています。

ここまでで、DeliveryChargeTestCaseテストコード)が正しく実装できていることが確かめられたのだと思います3
DeliveryCharge__init__が誤った実装(はじめの雛形)であればテストは落ちますし、そこからテストを通すように持っていけました。

3.DeliveryManagerを使った実装に変えてリファクタリング開始

class DeliveryCharge:
    def __init__(self, shopping_cart: ShoppingCart) -> None:
        self.amount = DeliveryManager.delivery_charge(shopping_cart.products)

仮実装から、リファクタリング対象のクラスを使った実装に置き換えます。
DeliveryChargeTestCase(テストコード)が正しく実装できていることは2で確認できていて、ここでDeliveryManagerを使った実装に書き換えても引き続きテストは通ります。
なので、仕様を満たしていることが確認できているという理解です。

最初、DeliveryManagerTestCaseを用意しないことにとまどいましたが、

  • DeliveryManagerクラスはあるべき構造ではなく、消し去りたいこと
  • あるべき構造のDeliveryChargeのテストを使って、DeliveryManagerの実装も仕様を満たしていることを確認できていること

から用意していないと理解しました。

これ以降はDeliveryManager.delivery_chargeメソッドのロジックをShoppingCartに移して、DeliveryManagerは消えます。
(時間の成約と、一番気になっていた箇所は解決したので、これ以降の写経はまたの機会に)

感想(終わりにに代えて)

こういうリファクタリングもあるんですね。
最初にAS ISの実装のテストを書かずに、TO BEの雛形追加だったので、「私の知っているリファクタリングともしかして違う?」と分からなさを覚えましたが、写経を通してリファクタリングの手順には則っているんじゃないかと理解しました。
テストがないコード(DeliveryManager)に対して、テストでハーネスを作っていると言えると思います。

ミノ駆動本という書籍の性質(設計入門)上、限られたページ数で現場のリファクタリングの流れを伝えるためにこの例になっているのだと思います。
一方でこれまでの経験から「テストがない既存実装を消し去るリファクタリングって現場でやることがあるのかな?」という疑問もわきました。

現場のコードではDeliveryManager.delivery_charge呼び出している既存の実装があるように思われ、そうなると初手はdelivery_chargeメソッドへのテストコード追加だと思います。
14.2の方法は、既存実装(呼び出し箇所)が壊れてしまうので、そのままでは現場で使えないのではないでしょうか。
(これに対処するアイデアとしては、DeliveryManager.delivery_charge呼び出しを関数に抽出し、その関数のテストコードを書いてからリファクタリング。あるべき構造に置き換えたら関数をインライン化すればDeliveryManagerを消せそうに思います。ただこれは脳内だけで未検証です)

ミノ駆動本でリファクタリングを知り、もっと深く学びたいとなったら(Uncle Bobも言っている4のですが、)マーティン・ファウラーの『リファクタリング』がオススメです。


  1. その理由は 読書ログ | #ミノ駆動本 14章「リファクタリング」の読書会予習に着手しました - nikkie-ftnextの日記 をどうぞ
  2. 5章 低凝集で取り上げられます
  3. ここで言う「正しく」は開発者が開発を進める上での正しさで、QAとは別物です。詳しくは 第3回 「テスト」という言葉について ── Developer Testing、Customer Testing、QA Testing | gihyo.jpをどうぞ(この記事で言うDeveloper Testingの意味でnikkieは使いました)
  4. Clean Craftsmanship』第5章 リファクタリングで言っています