LEFログ:学習記録ノート

leflog: 学習の記録をどんどんアップしていきます

【感想】マーティン・ファウラー『リファクタリング 第2版』―― ソフトウェア設計の入り口としてのコード整理

※この記事の『リファクタリング』という二重鉤括弧は、書籍そのものを示しています。それ以外の「リファクタリング」という単語は「コードの片付け」という意味合いで用いています。

なぜリファクタリングをするのか?

開発をするうえで、実装に関しては今まで鍛えてきた自信がありました。

しかし、設計という領域については自分はまだまだ未熟だと感じています。

2023年は主にbootcamp, 引用箱, timer.teamの3つのWebアプリの開発に携わりました。それらにおいて自分は「Issueというゴールを達成することができるか」という観点で開発していました。つまり、「実装できるかどうか」が最大の関心事で「どのように実装するか」についてはあまり気にしていませんでした。

そのままではいけないと感じたのはtimer.teamの開発が始まってからです。一番難しいところ……例えばBroadcastChannelやWebSocketに関しては純粋な実装力が必要でした。ただし、その後が問題で、その実装したコードをいかに整理するかという方法論を知らなければ、どんどんコードが複雑化してしまうことが分かりました。

複雑化すると、その後の実装が難しくなります。単純な機能を追加するだけなのに、入り組んだコードを解読する羽目になってしまうかもしれません。また、既存の機能を変更するのも大変になってしまいます。

次の画像は、『リファクタリング』に登場する図です。すぐれた設計であれば、機能の追加とプログラミングに掛かる時間は比例します。しかし、まずい設計であれば、一定以上の機能を追加すると、プログラミングに掛かる時間は膨大になります。

p.50「リファクタリングはプログラミングを速める」より

困難な状況を防ぐためには、早い段階で手を打つ必要があります。そのためには、一つの機能を実装すると同時に「小さな片付け」をするのが最適です。この小さな片付けこそがマーティン・ファウラーの言う「リファクタリング」です。

リファクタリング」という言葉が、現在のシステム開発において誤解されているケースは多いです。本来は、機能追加におけるちょっとしたコードの片付けのことを指し、「1時間に1回はリファクタリングをしている(p.50)」ような小規模なものでした。しかし、この「リファクタリング」という言葉が広まるにつれ、それが大規模な改修作業であるとの誤解が広まりました。その結果、リファクタリングの効用が軽視され、機能の追加(実装)だけが優先される事例が増えてしまいました。

最近ケント・ベックが『Tidy First?(訳:最初に片付ける?)』という書籍を出版しました。これは『リファクタリング』と似通った内容なのですが、小規模なコード修正のメリットについて更にフォーカスしています。

www.oreilly.com

Tidy First? [Book]

話を戻すと、「小さな片付け」を疎かにすることで技術的負債が貯まっていきます。最初のうちは良くても、どんどんコードが複雑化し、プロジェクトの進行が遅れていきます。気づいた頃には、手を付けるのが恐ろしいスパゲッティの完成です。機能追加が困難になり、ビジネス的にもデメリットがあるでしょう。

それを未然に予防するために必要なのがリファクタリングです。当たり前ですが、一つ一つの実装が綺麗になっていれば、結果として全体のコードも良いものになります。だからこそ、リファクタリングは小さい規模でこまめにやるべきなのです。

設計の第一歩としてのリファクタリング

しかし、頭では分かっていても、それをおこなうのは難しいです。実際、良いリファクタリングをするためには、自分が書いたコードが良いものか悪いものかの判断がつく必要があります。

機能を実装すること自体は、「動いた」「動かない」というゼロイチの明確な指標があるので、自分が達成しているかどうかの判断がつきやすいです。しかし、リファクタリングをする上での「良いコード」か「悪いコード」かの判断は、それより難しいです。「悪いコード」でも、動くには動くからです。*1

とはいえ、明らかに「悪いコード」は存在します。「変数名と中身が一致していないケース」が代表的な例として挙げられるでしょう。まず、そのように基準が明確なものから改善します。

それから、プロジェクト全体を俯瞰して考えたときに「より適切なコード」に改善できないか考えます。この判断は前者よりも難しく、知識や経験を必要とします。

というのも、どんなふうにコードを書くかはトレードオフの関係にあることも多いからです。影響するファイル数が2つや3つなら問題がなくても、10や20なら問題のあるコードもあります。疎結合・密結合という言葉が有名ですが、それぞれメリット・デメリットがあります(いつでも疎結合が良いわけではありません)。

つまり、リファクタリングは小規模でありながら、そこには設計能力が必要とされます。リファクタリングは設計の第一歩なのです。良いリファクタリングこそが設計力の向上につながります。

そしてこの考え方は、どんな場面でも役に立ちます。フロントエンドのコンポーネントをどのように切り分けるか、肥大化したモデル層のコードをどのように分割するか、DBをどのように移行してパフォーマンスを上げるか……こうした場面で、適切な設計を考える必要があり、そのためには小さな改善から始めたほうが良いでしょう。*2

この本においては、汎用的なリファクタリングの方法が紹介されていますが、根底に流れている考えは、どこでも応用が効くものだと思われます。

リファクタリング上達のレベル

リファクタリングするうえではいくかのレベルがあると思います。まず、具体的にそれを挙げましょう。

  1. コードの違和感に気づく
  2. 違和感を言語化し、方針を立てられる
  3. リファクタリングの手法を使いこなす

LEVEL1: コードの違和感に気づく

そもそものところで「悪いコード」に気づかなければ、リファクタリングすることはできません。

つまり、リファクタリングをするための基礎として「コードの不吉な臭い(p.73)」を察知することが大切です。

リファクタリング』では、その不吉さの具体例がたくさん紹介されています。「不可思議な名前」「重複したコード」「長い関数」「グローバルなデータ」……。実際の場面でこれらに気づくのは、意外と難しいです。レビューを受けて初めて「あっ、そっか」と問題に気づくことも多いです。

違和感を持っているかどうかは、大きな差を生みます。違和感を持っていれば「このファイルのこのコード、もっと良く書ける気がするのだけど……」と誰かに相談することができます。しかし、違和感を持っていなければ、そうした機会すら失われてしまいます。

だからこそ、まずは違和感に気づくために、悪いコードの例を知る必要があります。その例を身近なコードの中に探すことで、察知する力は向上するでしょう。

LEVEL2: 違和感を言語化し、方針を立てられる

違和感に気づいたときに、それを上手く言語化することができたら、かなり上達していると言えます。

例えば、2つのクラスがあるとします。そこで、片方のクラスのコードの量がやけに短いとします。

ここで「クラスが非対称な気がする」「片方の責務が足りない気がする」と違和感になんとなく気づきます。しかし、この違和感だけでは、具体的に何をどうすれば良いかの方針が立ちません。

もし、ここから更に踏み込んで、違和感の理由を説明できたとしたらどうでしょうか?

例えば「デメテルの法則に違反している部分がある」→「この違反を修正するために、責務を移す必要がある」→「そう考えると、このメソッドはこちらのクラスに移行するべきだ」。このような流れで言語化し、リファクタリングの方針を立てることができます。

ここまでくれば、7割は目標を達成しています。「違和感の原因を特定し、改善する方針を立てられる」というところまで来たら、あとは具体的な手法を適用するだけです。

ただし、その具体的な手法を適用するのも、なかなか難しい場合があります。そして、次のレベルが出てきます。

LEVEL3: リファクタリングの手法を使いこなす

リファクタリング』の第5章からは、具体的な手法がサンプルコードとともにたくさん紹介されています。

この本の良いところは、リファクタリングの手法をステップ・バイ・ステップで解説しているところです。改善前のコードと改善後のコードを比較するだけでなく、どのような過程を経たのかが順番に示されています。一見大きな変更に見えるリファクタリングも、方法論に則って少しずつ変更すれば、思わぬバグを生んでしまう可能性をできる限り低くできます。

小さなステップに分けることは重要です。そのことで、コードを整理する心理的コストを減らすこともできますし*3、ステップごとにテストコードを走らせてミスに気づくこともできます。

リファクタリングの手法には、正反対のテクニックも出てきます。関数に抽出するテクニックがあれば、関数をインライン化する*4テクニックもあります。

そのため、どの手法を選択するかという状況判断が必要です。これは一つ前の項目で書いた「方針を立てる」能力です。

そのうえで、その選択した手法をどれだけ鮮やかに適用するかが、「手法を使いこなす」レベルに進むための関門となります。実際、プロダクションコードを目の前にすると、やるべきことが分かっていても、どこからどう手を付ければ良いか迷ってしまう場合があるでしょう。

とても分かりやすい例だと、コメントが書かれているコードを修正する場合です。そのコメントが、複雑な挙動を解説するために置かれていたとします。コメントを削除するには「コメントがない状態でも挙動を分かりやすく説明できる」状態までコードをリファクタリングする必要があるでしょう。その箇所を修正しなければいけないと分かっていても、どのように改善するかの手法を知らないと、歯が立たないかも知れません。*5

この本では、無数の事例が載っています。マーティン・ファウラーも、あらゆる手法を網羅したいという目標があったのだと想像できます。『リファクタリング』で紹介された手法を全てマスターすれば、リファクタリング・マスターも夢ではないでしょう。

読んでいて難しかったところ

最後に、自分が『リファクタリング』を読んでいて難しかったところについて、ご紹介したいと思います。

これは社内で質問することで、やっと謎が解けた部分でした。これから読む方の参考になれば幸いです。

diffを追いかけにくいコードについて

上記に書いたように『リファクタリング』ではコードの修正がステップ・バイ・ステップで掲載されています。

ただし、コード例によってはその過程を追いにくいものもあります。そのため、変更量が多いコードに関しては、手元のエディタへ写経するのがオススメです。

自分は今まで、技術書のコードを写経することがほとんどありませんでした。コードの全体像が掴めれば、特に書き写す必要を感じなかったからです。

しかし、この『リファクタリング』は、過程を説明する際に変更のない箇所については省略されています(わずかな変更ごとにコード全体が載ると冗長だからです)。そのため、コード全体のどの部分に手を加えているのか、理解しづらい場面があります。

手元で写経すれば、その問題は解決します。ちょっとした変更ごとにGitでコミットを積めば、どのような過程でリファクタリングされているのか、流れを追いやすくなります。 コード例はJavaScriptなので、開発環境を構築するのも簡単です。そして効果は大きいです。一番初めのセットアップだけは少し面倒ですが、それを過ぎれば楽です。

自分の場合、次の画像のように環境をセットアップしていました。

ローカルに写経すると全体像を掴みながら差分(diff)を確認できます

上はVSCodeを使った例ですが、CLIでも差分を確認しやすいツールがあります。

自分はこのdifftasticを愛用しています。

github.com

Wilfred/difftastic: a structural diff that understands syntax 🟥🟩

改善の目的が分かりづらかったところ

ポリモーフィズムによる条件記述の置き換え

リファクタリング』を読んでいて、理解が難しい節がありました。

第10章の「ポリモーフィズムによる条件記述の置き換え(Replace Conditional with Polymorphism)」という節です。

まず、リファクタリング前のコードをご紹介します。

function plumages(birds) {
  return new Map(birds.map((b) => [b.name, plumage(b)]));
}
function speeds(birds) {
  return new Map(birds.map((b) => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
  switch (bird.type) {
    case "EuropeanSwallow":
      return "average";
    case "AfricanSwallow":
      return bird.numberOfCoconuts > 2 ? "tired" : "average";
    case "NorwegianBlueParrot":
      return bird.voltage > 100 ? "scorched" : "beautiful";
    default:
      return "unknown";
  }
}
function airSpeedVelocity(bird) {
  switch (bird.type) {
    case "EuropeanSwallow":
      return 35;
    case "AfricanSwallow":
      return 40 - 2 * bird.numberOfCoconuts;
    case "NorwegianBlueParrot":
      return bird.isNailed ? 0 : 10 + bird.voltage / 10;
    default:
      return null;
  }
}

次に、リファクタリング後のコードをご紹介します。

function plumages(birds) {
  return new Map(
    birds.map((b) => createBird(b)).map((bird) => [bird.name, bird.plumage])
  );
}
function speeds(birds) {
  return new Map(
    birds
      .map((b) => createBird(b))
      .map((bird) => [bird.name, bird.airSpeedVelocity])
  );
}
function createBird(bird) {
  switch (bird.type) {
    case "EuropeanSwallow":
      return new EuropeanSwallow(bird);
    case "AfricanSwallow":
      return new AfricanSwallow(bird);
    case "NorwegianBlueParrot":
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}
class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  get plumage() {
    return "unknown";
  }
  get airSpeedVelocity() {
    return null;
  }
}
class EuropeanSwallow extends Bird {
  get plumage() {
    return "average";
  }
  get airSpeedVelocity() {
    return 35;
  }
}
class AfricanSwallow extends Bird {
  get plumage() {
    return this.numberOfCoconuts > 2 ? "tired" : "average";
  }
  get airSpeedVelocity() {
    return 40 - 2 * this.numberOfCoconuts;
  }
}
class NorwegianBlueParrot extends Bird {
  get plumage() {
    return this.voltage > 100 ? "scorched" : "beautiful";
  }
  get airSpeedVelocity() {
    return this.isNailed ? 0 : 10 + this.voltage / 10;
  }
}

このコードを見て、最初自分は「リファクタリング前のコード」のほうが読みやすくて良いと思いました。そのため、どうしてこのようなリファクタリングが必要なのか、理解するのに苦労しました。

この節について、社内で質問したところ、疑問が氷解しました。結論から言うと、この節の目的は次のようにまとめられます。

  • Afterではメンテナンスするときに、一箇所だけ書き換えれば良い。Beforeではたくさんのswitch文があるときに、複数の箇所を書き換える必要が出てくる。

例えば、switch (bird.type)を使っているコードが無数にある場合を想定します。そのとき、もしBirdの種類が増えた場合、それらの条件記述を一つ一つ書き換えることになるでしょう。

Afterのようにポリモーフィズムを使うと、クラスの中身を変更するだけで事足ります。

コード例ではswitch文が2つしか存在しないため、そのメリットが分かりにくかったのですが、実際のコードではswitch文がたくさん増えてしまうケースが想定されます。その場合にこの節が有効になります。*6

サブクラスによるタイプコードの置き換え

他にも理解するのがちょっと難しい箇所があります。例えば第12章に出てくる「サブクラスによるタイプコードの置き換え(Replace Type Code with Subclasses)」の2つ目の例「間接的な継承の使用」です。サブクラスを導入するメリットは1つ目の例で説明されていたため、どうして2つ目の例が紹介されているのか、理解するのに時間が掛かりました。

このコードを写経して手元でdiffを確認することで、その意図を掴むことができました。この例ではEmployeeクラスの中にcreateEmployeeType()というstaticメソッドを導入しています。それを使って、従業員の種類ごとのサブクラスを作成しています(ファクトリーメソッドの導入)。

いちばん大事なところは、クラスの中にクラスをネストしているところでした。このコードではクラスの外部にサブクラスを作ることができないという条件がありました。そのため、クラスの中にサブクラスを作成することで、見通しを良くしているのです。

おわりに

コードリーディングに関する本で一番有名なのは、やはり『リーダブルコード』でしょう。自分もこの本の輪読会を開催した*7ほど、読みやすいコードには興味がありました。

では、読みにくいコードを読みやすいコードへと変えるための手法をどうやって学ぶか……それにうってつけの本こそ、この『リファクタリング』でした。

本書を読んでいて思ったのは、「物事を理解することと、理解したものを実践のレベルに落とし込むことには、大きな差がある」ということです。

リファクタリング後のコードを見ると「明らかに良くなっている」と理解できます。しかし、いざ自分で作業するとなると、手が止まってしまいます。理解に実践が追いついていない状態です。これは試行を重ねることでしか、上達しない気がしています。

そのため、先程挙げた3段階のレベルのうち、まずは「違和感に気づく」ところから始めたいと考えています。何かがおかしいと思ったとき、その箇所にちょこっとメモを残しておいて、レビューをやり取りする際に「このコードの書き方はどう思いますか?」と質問できるだけでも、プログラマーとしてかなり上達していると言えるでしょう。

最近は、社内でケント・ベックの『Tidy First?』を読んでいます。こちらについても、読書会が終わった頃に、またブログ記事を書けたらいいなと思います。

*1:「将来的に問題を引き起こす可能性が高いコード」のことを、この記事では「悪いコード」と呼んでいます。

*2:実際、データベースのリファクタリングに関する本も存在しています。→ https://amzn.asia/d/eBb4LIU

*3:わんこそば理論

*4:関数をやめて呼び出し元のコードで展開すること

*5:自分も最近、副作用を持つ関数をどのように修正するかで苦戦しました

*6:リファクタリング第2版』では「複雑な条件分岐」を解消するための方法の一つとして紹介されています。しかし、その「複雑な条件分岐」が何であるのかが初見だと理解が難しいです。『リファクタリング第1版』では次のように書かれています。「Polymorphism gives you many advantages. The biggest gain occurs when this same set of conditions appears in many places in the program. If you want to add a new type, you have to find and update all the conditionals. But with subclasses you just create a new subclass and provide the appropriate methods. Clients of the class don't need to know about the subclasses, which reduces the dependencies in your system and makes it easier to update.」第二版では「タイプコードで分岐する switch 文を含む関数が複数存在する場合(……)共通な switch 文のロジックの重複を排除します。」と書かれてはいるのですが、意図としては第一版のほうが明快に受け取れます。

*7:リーダブルコード輪読会2023を企画・主催します!📘 - LEFログ:学習記録ノート