はじめに
ういっす✌️ 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でも解説されます)。
テストコードはないのでテストを書いてリファクタリングします。
最終的にはDeliveryCharge
とShoppingCart
から配送料が計算されます(図14.1)。
この例は「あるべき構造がある程度わかっている場合」のリファクタリング手法を紹介する例であり、DeliveryManager
は消え去ります。
結論:理解したこと
- 最初にある構造(
DeliveryManager
)をリファクタリングの結果、削除したいという状況 - 削除したいので
DeliveryManagerTestCase
は追加しない - あるべき構造のテストコードを書く
- あるべき構造のテストコードを使って
DeliveryManager
が仕様を満たしていることを確認している
写経(中の)コードは以下に置いています:
リファクタリング手順に沿った写経
理解したことをnikkieの言葉で整理していきます。
1.あるべき構造の雛形を作る
DeliveryManager
がリファクタリング対象でテストがないので、「まずはDeliveryManagerTestCase
を書くのかな」と思ったnikkie。
ところが、まずはあるべき構造(DeliveryCharge
とShoppingCart
)の雛形の実装でした!
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つずつ商品が追加されるというのがうまく表されているな〜と感心しました
DeliveryCharge
はShoppingCart
をもとに配送料を計算します- 現在の買い物かごの状態に基づいて配送料を計算するということですね
2.雛形に対してテストコード追加
あるべき構造のDeliveryCharge
のテストを追加します。
class DeliveryChargeTestCase(TestCase): def test_pay_charge(self): # 買い物かごに商品を追加し、配送料の額を検証 def test_free_charge(self): # 買い物かごに商品を追加し、配送料が無料(額が0円)を検証
DeliveryCharge
はself.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のですが、)マーティン・ファウラーの『リファクタリング』がオススメです。
- その理由は 読書ログ | #ミノ駆動本 14章「リファクタリング」の読書会予習に着手しました - nikkie-ftnextの日記 をどうぞ↩
- 5章 低凝集で取り上げられます↩
- ここで言う「正しく」は開発者が開発を進める上での正しさで、QAとは別物です。詳しくは 第3回 「テスト」という言葉について ── Developer Testing、Customer Testing、QA Testing | gihyo.jpをどうぞ(この記事で言うDeveloper Testingの意味でnikkieは使いました)↩
- 『Clean Craftsmanship』第5章 リファクタリングで言っています↩