コードの共通化について語るときに僕の語ること
似たようなコードを繰り返し書いている感覚になると、コードを共通化したくなりますね。
しかし闇雲に共通化してしまうと変更を許容しないコードになります。それらのコードを変更しようものなら無数のバグを生み出す危険があります。複雑で読めない、安全に変更する自信が持てないコードと対峙すると、うんざりしますよね。
今日はコードを共通化するときは考慮したい点を3つ紹介します。
- SRPを意識してカオスな共通化を回避しよう
- 共通化の2つのアプローチ
- 共通化されたコードはOSSのライブラリーのように扱おう
SRPを理解してカオスな共通化を回避しよう
まず最初に共通化してはいけない判断基準としてSRPの話をします。
私はこちらのラジオで「アクターが違うならコードを共通化してはいけない」ということを学びました。
(SRPについて#14,15,16と続きます。)
SRPは日本語で単一責任原則と訳されます。
そのように訳されますが、リファラジでは「単一責任原則ってどういうこと?って絶対なるよね」「単一責任原則という名前は勿体無い」と語られています。私もそう思います。
このラジオを聴いてコードは単一のアクターに対して責任を持つようにするべきということを理解し、やっと設計で使える知識になりました。
SRPを意識せずに共通化されたコードには次のような課題があります。
- 複雑、見通しが悪い、デットコード(使われなくなったコード)が溜まりやすい
- テストが大変
複数のアクターから呼ばれることを前提としたコードは、その内部できっとアクターの数だけ分岐しています。地獄です。
以下のコードはアクターA, B, Cから呼ばれるコードが一つの共通コードにまとめられています。
上記のコードをSRPに従ってリファクタリングするなら、まずは以下のように小さなプログラムを作成します。
そして、元のコードをそれぞれのアクターごとに分けると
こんな感じでスッキリします。大切なことなので繰り返します。
「アクターが違うならコードを共通化してはいけない」
共通化の2つのアプローチ
共通化の2つのアプローチを紹介します。
- 小さな部品を共通化する
- 引数として処理を受け取れるようにする
「小さな部品を共通化する」のは基本方針で、「引数として処理を受け取れるようにする」のはテクニックという感じです。
小さくプログラムを組む
このアプローチはSRPの説明で実践しています。より小さな部品は、より少ない前提で機能するので小さくプログラムを組むだけで共通化しやすいコードとなります。
ちなみに、プログラムを小さな部品から組み上げていくプログラミングスタイルをボトムアッププログラミングといいます。ボトムアッププログラミングが実践できていると、「共通化し過ぎて苦しくなってきたな」と感じたときに、共通化されているパーツをバラして組み直すことが簡単です。
引数として処理を受け取るようにする
いくつかのコンテキストで、小さなプログラムを呼び出すコードを書いていると、その小さなコードを扱う同じパターンが登場することがあります。
上記のコードで「処理X, 処理Yはコンテキストによって変わるけど前後の処理は共通化したい」というケースですね。そういったケースでは引数として処理を受け取るように設計するとスッキリします。
次のような共通コードを用意します。
このコードをそれぞれのコンテキストで処理X, 処理Yを与えます。
ここで紹介している設計が使用されているケースとして、JavaScriptのArray
のmap
, filter
, reduce
などが挙げられます。
他にはReactコンポーネントで、親からprops
としてコンポーネントを受け取ってレンダリングするコンポーネントを作ることができますね。それもここで紹介しているアプローチの実践例だと言えます。
共通化されたコードはOSSのライブラリーのように扱おう
具体的には次のことをしましょう。これらが実践されているとチームメンバーは安心してあなたのコードを使うことができます。
- テストを書く
- 引数をチェックする
- コメントを書く
テストを書く
テストはバグがないことを保証するだけでなく、ドキュメントとして機能します。コードの作者がどのようなケースに対応できるようコードを書いたのかはテストを読めば判断できます。
チームの開発ルール次第ですが、メインブランチにマージされているコードは何らかの方法で正しく機能することが確認されているコードだと思います。しかし、テストが書かれていないと何を確認したのかについて知ることは困難です。すでにテストが書かれている場合、試してみたいケースを手軽に追加できます。自分が呼び出そうとしているケースで使えるコードかどうかの判断材料になります。
引数をチェックする
コメントを丁寧に記述しても読まずに使用されるケースはたくさん経験しています。
引数を厳密にチェックして、想定から外れる引数が渡されていればエラーで知らせましょう。
TypeScriptを使用している場合なら引数の型を処理内容に合わせて厳密に定義しておくことで、使い方を間違えていれば実行前にフィードバックできます。
コメントを書く
コメントではコードの使い方について説明しましょう。以下のことが説明されていると使いやすいと思います。
- コードが適切に機能する条件
- 想定している使用方法(サンプルコードを示せるとより良い)
- 引数、戻り値の説明
コードが適切に機能する条件がある場合、関数やメソッド名の命名だけでは伝えるのが難しいこともあるので、私の場合はできるだけ書くようにしています。また、コードの使い方が難しいと判断した場合は、サンプルコードをコメントに含めます。
他の内容は、関数やメソッド、引数の命名が分かりやすいなら省略しています。関数やメソッド、引数の命名を日本語訳しただけのコメントは書きません。
まとめ
この記事は「コードの共通化について語るときに僕の語ること」というタイトルを最初に決めて、約半年前に書き始めました。
しかし共通化の条件について着地地点が見えてこなくて、書いては消してを繰り返していました。他の内容はタイトルを決めたときに書くと決めていました。
そういう経緯でずっと下書きのままだった記事ですが、リファラジによってSRPを再発見したことで「これがコードの共通化について語るときに僕の語りたいことだ!」と思い、記事がまとまりました。
リファラジをより多くの人が聴いて、より多くの人がリファクタリングについてチームで話せるようになるといいなと思います。ぜひ、聴いてみてください。
後日談
仕事の半分以上がコードレビューという状況が、毎日のように良いコードについて考える機会になっています。
最近ルール・オブ・プログラミングという本を読み、チームで共有したい内容がたくさんありました。そのなかでも「ルール4 一般化には3つの例が必要」では、著者が書いたコードの例を読みながら一般化の誘惑とそれらが負債になるリスクについて理解することができます。
書き手には一般化が魅力的に感じ、読み手には一般化にうんざりさせられるというのは、面白いですね。
今回書いた記事のテーマは共通化であり、まだ必要ではないケースにも対応させる一般化については触れませんでしたが、「一般化には3つの例が必要」、そして「YAGNI」についてこの記事に織り交ぜて語るべきだと思いました。いつか時間を見つけて書き足したいと思います。
Discussion