技術的負債がたまるとはどういう状態か
読みにくいコード、複雑なコード(Cruft)は、何も考えなくてもかける為、一応実装のスピードは出る。しかしそれも短期的なもので、時間が経つに連れて負債がたまる。具体的には、ある部分に修正の必要があったり、追加機能が必要な際に、既存のコードを読み解いたりするのに余計な時間がかかってしまう。
どのような方針で対応すべきか
すでにコーディングした部分に修正をくわえる場合
どこをどの程度修正するか
技術的負債を解消するにもコストはかかる。その為、コストとリターンを秤にかけて決定する。
例えば、将来あまり使わなそうなプロジェクトであれば修正をする意味はあまりない。
またプロジェクト内部において、将来あまりいじらない部分のモジュールであれば同じく修正をする意味はあまりなくなる。具体的には、独立性の高い部分のコードなど。
またやるにしてもどの程度やるかの問題がある。全体に対して完璧になるまで修正をすると何ヶ月もかかりかねず、過剰な措置ともなりかねない。他方、全部後回しにしては、前述のようにどんどん負債が大きくなっていってしまう。その為、コーディングする都度、関係する部分を少しずつ直し、自分が見る前よりも少し綺麗なコードにする(ボーイスカウトルール)など、日々少しずつリファクタリングしていくのが有効な場合が多いと言える。具体的には、リファクタリングをしない場合の作業に比べて最大1~2割程度の時間を余計に使い、工数へのインパクト・動作確認へのインパクトをコントロールしながらやるといい。
また、修正の後、「もしその箇所を自分が初めから設計していたとしたら、こういうデザインになっていたであろう」という状況に近づけるようにする。
コメントの修正
実装の変化に伴ってコメントを修正することもある。コメントの修正漏れを防ぐには、コメントと、そのコメントが説明している箇所のコードとをなるべく近づけるようにしておこう。
また、変更した詳細な理由をコミットログに書き込んでしまうこともあるが、コミットログは後から見づらいのでまずコメントに書き込むこと。コメントに書いた内容をコミットログにも書いておくのは問題ない。
diffで見直す時間を取る
コミットする前に数分でいいので時間をとって、gitのdiffを見てみよう。自分がした全ての変更の意義がコード自体から一見して明らかであるか、もしそうでないならきちんとコメント・TODOとして記載してあるかを確認する。gitのdiffはSourceTreeなどのツールであれば自動的に表示されるし、ツールを使っていなければコマンドでも確認できる。
これからコーディングをする場合
そもそも技術的負債が発生しないように最初にコーディングするのが一番効率が良い。また知見と技術力さえあれば、そのようなコーディングをしつつ、かかる時間もさほど増やさないようにするのは不可能ではない。
その為、そのような知見を積極的に吸収し、コーディングをするときから積極的に実行していくのが良い。
- 単純に動くコードを作るだけなら時間が10かかるという場合、1~2くらいプラスで時間を費やし、そのコードの見やすさやすでにある他のコードとの整合性などを調査したり直したりすると良い。そうすると、小さな分かり難さが累積してひどく読みづらいコードになっていくのを未然に防げる。
テクニック
モジュール化
意義
複雑な働きを遂行できるコードをなるべく抽象化し、モジュール化して簡単に外部からアクセスできるようにする(=知る必要のない部分は隠蔽する)と良い。なぜなら、プログラムのする機能が大きい割に学習コストが低くなるため、後からコードを読む人の理解が早くなるからである。
またプログラムの中身が変更されても、そのコードを使う時の約束事(インターフェース)が変わらなければ、他の部分を変更する必要はなくなるため、変化に強くなる。
方法
-
「浅い」モジュール(果たせる役割の割は少なく、覚えなければならないインターフェースが多い)ではなく、「深い」モジュール(果たせる役割が多く、覚えなければならないインターフェースが少ない)となるようにする。
- モジュールのインターフェースは、そのモジュールを使う際に最もよくある状況(デフォルトの状況)で最もシンプルに使えるようにすべき。言い換えれば、一番よくある状況に合わせて実装されるべきであり、それから外れた状況で使いたい時のみ、外部から明示に追加の指示を出すという風にすべき。例えば、最も頻繁に引数として渡すであろう値が想定できる場合には、その値をあらかじめデフォルト値として設定しておき、通常は意識せずとも済むようにしておくなど。
-
低レベルの機能は、通常モジュールが隠蔽してしまって、外部からは意識せずとも済むようにしておくと良い。
- 例:
- データベースにどうデータを保存し、取り出すか
- 通信処理の際のプロトコルをどう扱うか
- マルチスレッドでどのようなスケジューリングをするか
- どのようにJSONをパースするか
-
同じ実装は一箇所のみに定義する。複数のモジュールに同じ実装が複数回書かれているようだと、情報が上手く隠蔽できていないという事になるためである。その場合、その複数のモジュールを合体させてしまうか、あるいは重複している情報を隠蔽する専門のモジュールを一つ用意するなどが考えられる。
-
ある特定の順番で実行することを念頭においてモジュールを設計すると、特定の順番で実行することが前提となってしまい変更に弱くなる上に、一つの機能が複数箇所に定義されてしまうことがある。処理の順番に基づいてモジュールを作るのではなくて、特定の機能ごとにモジュールを作り、モジュールを使う順番は適宜変えたとしても問題のないようにしておこう。
-
あるモジュールの内部で使っているデータを、ただそのまま外部に露出させるだけのインターフェースだと、上手く情報を抽象化できていない余計なインターフェースということになってしまう。また、内部のデータ構造を変えただけでインターフェースにも変更が必要になってしまう。そうではなく、内部のデータ構造のうち、外部からどのようなデータをどのような形式で利用させたいかを考え、それのみをインターフェースとして定義するようにしよう。そうすると、内部の構造が変わってもインターフェースは変わらず、変更に強くなる。
- 例:モジュール内部で様々なデータを辞書型で保持している。この辞書型データをそのまま全部外部に露出させるのではなく、辞書型データのうち必要な情報を、文字列は文字列型として、数値データは数値型として露出させる等。
-
モジュール内でのモジュール化
- あるモジュール(例えばクラス)の中でも、内部モジュール(例えば、プライベートの関数)などを積極的に作っていき、余計な情報をモジュール内の他の部分から見えないよう隠蔽するとよい。その観点だと、クラス内のどこからでもアクセスできるいわゆるメンバー変数などは、なるべく使う箇所を減らしたほうが、クラスの複雑性を減らすことができる。 -
隠蔽をやりすぎて明らかに必要な情報まで隠してしまうということのないようにする。
-
通常モジュールを作る時、今自分がやりたいと考えている機能をさせるためにモジュールを作ると思う。しかしその狭い目的のためだけでなく、幾分広く他の目的も果たせるような一般的なモジュールを作るよう意識していくと良い。そして、その目的を果たすのに最も単純なインターフェースとなるよう考えていく。
レイヤー化
意義
モジュール化の中でも、ディスクからのバイトの読み書きのような低レベル階層から、ソフトウェアの直接ユーザーに触れる部分などの高レベル階層のような階層を意識していくことをここではレイヤー化と呼ぶ。
注意点
-
低レベル階層の機能を呼び出すだけで実質的には何もしていない高レベル階層の関数(素通りさせるだけの関数)は、基本的に何も機能を果たしていないのにインターフェースだけが複雑になる(追わなければいけない部分が増える)事になるので、可能なら避けると良い。避けるとしたら、そこで複数の階層を作るのはやめて単一階層にしてしまう等になるだろう。ただコーディングの際のアーキテクチャとして、必ず複数階層を用いてやるなどとルールを決めてやっている場合は、それとの兼ね合いもあると思う。複数階層化する意義があるのか(=それほどに複雑な機能なのか)を、適宜考えて決める。
-
レイヤー化の一種として「デコレータ(ラッパ)」というものがある。元のクラスを核として、それよりもやや特殊な状況に対処させるようにするため元のクラスを少し拡張していくという事である。乱用すると、さほど機能を果たしていないのに覚えることだけが増えてしまうため、どのような状況で用いるかを考える必要がある。例えば以下のような状況だと意義があると言える。
- 第三者の提供したライブラリなど、自分では元のコードに直接手を加えることが不可能な場合。
- 既存のコードをリファクタリングしたいが、すぐには全部書き直せないので、とりあえずラップして一部だけ改善する場合。
- テストのため、既存のコードをラップして、一部の振る舞いを上書きしてテストができるようにする場合。
-
レイヤー間でのデータの受け渡し
- グローバルな領域に静的な変数などを作ってどこからでも参照できるようにする、シングルトンのオブジェクトを使ってどこからでも参照できるようにするといった手もある。手軽に書けるものの、読む時にデータの流れが追いづらくなったり、テストがしにくくなると言った問題点がある。
- 各モジュール間でデータを伝言ゲームのように次々と受け渡ししていくという方法がある。テストの時も、テスト用のデータを渡しやすくなる。参考 各モジュール間でデータを伝言ゲームのように次々と受け渡ししていかなければいけない時の対処
- グローバルな領域に静的な変数などを作ってどこからでも参照できるようにする、シングルトンのオブジェクトを使ってどこからでも参照できるようにするといった手もある。手軽に書けるものの、読む時にデータの流れが追いづらくなったり、テストがしにくくなると言った問題点がある。
メソッド
メソッドの分割
- メソッドに関して何十行以上になったら長すぎるため分割するという向きもあるが、どちらかといえばモジュール同様、そのメソッドを使うために覚えないといけないこと(インターフェース)に対して十分大きな機能を持っているという事、そのメソッドが一つの機能を完遂するということの方が重要。単に長いからというだけで分割するとあちこちに分散していく事になりかえって見辛い。
- メソッドの中に副次的な機能を遂行している部分があり、その部分が他の目的にも使えそうなら切り出してサブメソッドとするなどが考えられる。
- それ以外でメソッドを分割すると、一つの機能を果たすために元々は一つのメソッドを呼べばいいだけだったのが、二つ以上呼ばないといけない事になる。もし分割する場合は、分割する前よりもシンプルなインターフェースになったと言えるかを十分考える。また、分割してみて、一方のメソッドの実装内容を理解するためにもう一方のメソッドの実装内容を理解しないといけないようだと、それぞれのメソッドが単一の機能を完遂しているとは言えず、不適切な分割である事が多い。
デザイン
デザインのパターンの選択肢をたくさん持っておく
ある目的を達成するためにデザインは複数考えておいた方が良い結果になることが多いため、色々なデザインのパターンを知っておこう。デザインパターンの一例についてはこちらを参照(Swiftによる)
プロジェクトが変化してきたらデザインも見直す
コメント
何をコメントするか
設計者の頭の中にはあるが、他の人が読んだ時すぐには読み取れないというような情報があればコメントに記載しておく。これにより、読む人の理解が早くなり時間の節約になる。あまりにたくさんコメントを書かなければ伝わらないようであれば、クラスやメソッドの命名がおかしいのではないか、機能をきちんと分割できていないのではないかと疑ってみよう。
外部から見た役割の説明
クラスや構造体、関数や変数などの宣言をした際、それが外部からみてどういった役割を果たすかの説明のためコメントをつけることが一番多いと考えられる。そうすると、そのクラスなどが大雑把にいってどういった位置付けになって来るかが分かるため、プログラム全体が見えやすくなる。それらの役割が、命名から一見して明らかな場合はコメントをつけなくてもいいが、そういった事例は頻度としては少ない方と考えられる。基本は1行程度、コメントをつけることが多い。
内部の実装の詳細・他のどのモジュールに依存するか
内部の実装の詳細や、あるモジュールが他のどのモジュールにどう依存するかなどの情報は通常は書く必要がないし、そもそもあまりたくさん書かなければいけないようなら機能の分割などデザインがきちんとできているか疑った方がいいと思われる。ただ、逆にどうしても必要になってくる時は、書かないと必要な情報が得られないことになり致命的になることもある。その場合は書いた方がいい。
例えば、
- 変数の単位は何か
- 条件判断において端の値を含むか含まないか
- 返り値としてnullが返って来る場合はどのようなケースで返って来るのか
- 後始末の処理(オブジェクトの解放や、何かを購読している時の購読の解除)は誰がやる必要があるのか(内部で自動でやってくれるのか、呼び出し側が明示的に行わなければいけないのか)
- 「この配列は常に一つ以上の要素を含む」といった、常に成り立つが、書いておかないと分からないような性質
コードを繰り返さない
コードの中身を繰り返すだけのようなコメントは無駄なのでやめる。
同じ説明を繰り返さない。
他の箇所にもあるようなコメントを繰り返さない。また、外部のドキュメントなどに情報がまとまっている場合は、それへのリンクを貼るようにし、その内容を一から繰り返さないようにする。
コメントの形式は慣習・ルールに従う
使っている言語やツールにおいて、「こういった形式でコメントする」といった慣習やルールが定まっていることが多いと思われる。公式のコードを参照したり、解説を見たりして、コメントをつける形式を知っておき、それに沿ってコメントするようにしよう。
インデントや見出しコメントなども慣習に従いつけていこう。
コメントをつけるのを後回しにしない
命名
クラスや関数・変数などの命名は、それらが外部から見たときどんな役割を持つかを短い言葉で表すような名前にする。
また、同じ概念を表すような名前は、プログラム全体で一貫して同じ言葉を用いるようにしよう。(後で検索が容易になる)
一貫性
一貫性を保つべき対象
以下の要素はプロジェクト全体でなるべく一貫性を保ち、後から見た時に理解がしやすいようにする。
- クラスなどの命名
- コーディングの形式(カッコをつける位置とか)
- インターフェースの形式(あるモジュールを利用する時に覚えておかなければならない事・渡さなければいけない値・返って来る値の形式など)
- デザインパターン
- その他、複数の変数やクラスで共通するような性質など
一貫性の保ち方
- すでに書かれているコードがある場合は、そのすでにコードが沿っている形式に従うようにする。少し良い書き方の形式を知っているからと言っても、すでにプロジェクトにある一貫性を崩してまでも導入すべき場合は少ない。
- ドキュメント化する(例えば、コーディング規約など)
- ツールや、プログラムの仕組みにより、一貫性が保たれることを強制する
注意を要する技術や手法
複雑さのため読むときに時間がかかったり、読者の期待を裏切ってしまうような書き方はなるべく避け、必要やむを得ない場合はコメントなどで補うこと。
イベント駆動プログラミング
- イベント駆動とは「イベント」と呼ばれるアプリや端末上で起きた出来事に対して処理を行うプログラムの実行形式のこと。参考: イベント駆動プログラミング、iOS のイベント駆動をライフサイクルイベントとユーザアクションイベントにわけて理解する
- イベント駆動においては、関数がユーザーのボタンタップや、画面の出現などのイベント発生時に呼ばれることになる。それら関数が直接呼ばれることはなく、どのような状況で呼ばれるかの把握が難しくなる。そのため、関数がいつどのような状況で呼ばれるのか、関数のコメントに記載しておいた方がいい。
複数の異なる型のデータを格納できる入れ物の使用
JavaのPairであるとか、Swiftで言ったらtupleであるとか、異なる型のデータを一緒くたに入れられる仕組みは書くときは便利である。が、後から読むときに理解しづらい。なるべく使用は避け、異なる型のデータを変数としてもつクラスを定義して使うなどの方が、書く手間は多少かかるが、読みやすさは増す。
スーパークラスの型を持つ変数に、サブクラスのオブジェクトを代入すること
例えばCarクラスのサブクラスとしてPoliceCarクラスがある場合、Carクラスとして宣言した変数にPoliceCarクラスのオブジェクトを代入するということである。その変数の中に入っているのが実際は如何なるオブジェクトなのかの把握が一見してできなくなるため、どうしても必要になってしまう時以外は避けたほうがいいだろう。
オブジェクト指向
オブジェクト指向ではサブクラスがスーパークラスに依存することになるため、場合によってはスーパークラスのことを全て理解していないとサブクラスを理解できないということにもなりかねず、書き方に注意を要する。
-
あるクラスに使い回したい機能があるときに、安易にそのクラスを継承をさせるのではなく、その機能を別のヘルパークラスとして抜き出して色々な箇所で使い回すという形にできないかを考える。
-
どうしても継承を使わないといけない場合は、スーパークラスが管理している状態とサブクラスが管理している状態とが完全に分かれるようにすると良い。具体的には、スーパークラスが管理している変数については、サブクラスからは直接変更はせず、参照をするだけか、あるいはスーパークラスの関数を通じて間接的に変更するだけにする。こうすれば、状態の変化が複雑になることを避けることができる。
アジャイル開発
- アジャイル開発においては毎日ミーティングを行い、1週間など短い時間で実装や改修を行い、顧客に提供し、フィードバックを得ることを繰り返していく。プロジェクト全体のデザインをどうするかを最初に決めるのは困難であり徐々に見直していくべきであることを考えると、短い時間で適宜振り返りを入れていくのは良い。
- ただ、とにかく短い時間で機能の実装さえすれば良いという考えにつながりやすいため注意が必要である。機能の実装だけでは十分ではなく、機能が「深いモジュール」になっているか(果たす役割に比べて覚えなければならないインターフェースが少なくて済むか)や、全体のデザインの適正化も重要である。アジャイル開発の本自体にも書かれているように、短い時間で成果を出しフィードバックをもらうには、ただただ機能さえ実装すればいいと考えるのではなく、適宜スコープを調整し今やらなければいけないことと後でやることを整理する。
参考文献
"A PILOSOPHY OF SOFTWARE DESIGN", John Ousterhout, Yaknyam Press, 2019
「レガシーコード改善ガイド」, マイケル・C・フェザーズ, ウルシステムズ株式会社, 2009
「アジャイルサムライ」, Jonathan Rasmusson, オーム社, 2011
https://note.com/timakin/n/nf7e2a70905d4
https://qiita.com/erukiti/items/9cc7850250268582dde7
http://tlync.hateblo.jp/entry/2014/09/28/025652
https://martinfowler.com/bliki/TechnicalDebt.html
https://medium.com/mop-developers/how-to-manage-technical-debt-961f8f94a623