こんにちは。
ヒコーキ好きのみこやんです。
本テーマ「開発手法の名著をふまえ、チーム内ルールを決めて実践してみる」では、まず前編として、命名やコメント、コードの書き方の一部についてのプロジェクトチーム内ルールをご紹介しました。
後編となる今回は、さらにコードに関するルールの続きと、「やらなくていいならやらないシリーズ」について書いてみたいと思います。
コードに関すること
前編で紹介しきれなかった項目をいくつか書いてみます。
論理演算式をシンプルに記述する
- 要件定義や仕様にある条件記述をそのまま実装すると複雑で読みにくくなる場合は、シンプルな形に置き換えること。場合によっては一部の条件を説明変数に抽出し、ifの中ではシンプルになるような形に整えること。
- 論理演算はド・モルガンの法則を活用して可読性をよくする。
123456789// BEFOREif (!($x !== 100 || $y !== 200)) {// 何らかの処理...}// BETTERif ($x === 100 && $y === 200) {// 何らかの処理...}
要件定義や仕様書に記載される条件の中には「XもしくはY、ではない場合」といったものもあると思います。
要件を定義している間は、ユーザやクライアントからのヒアリングをもとに記述することが多いと思うので、聞いたままを記載することになるでしょう。ここでその通りに書かれていないと、場合によってはヒアリングの突き合わせなどで面倒になる可能性もあると思います。
しかし、実装がその通りになっている必要があるかというと、必ずしもそうではないでしょう。言葉で表現する冗長なロジックや仕組みを、実装に落とし込む段階で平易かつ理解しやすい形にできるのであれば、そうするのが妥当です。なぜなら、ロジックがそのまま実装されていても理解しづらいですが、平易になったものに、元のロジックや意図を含めた命名によって表現するほうが、遥かに効果的で可読性が高まるからです。
それは、たとえ小さなロジックであっても言えることです。
たとえばif文の条件文として記載される論理式などは、わかりやすい例かもしれません。
さきほどの「XもしくはY、ではない場合」のちょっとした例が上のコードだとします。
「$xが100以外、もしくは$yが200以外、ではない場合」を、愚直に実装したものがBEFOREになると思います。
論理演算でよくお目にかかる「ド・モルガンの法則」というものがありますが、これを使うことで「裏を返せば」として表現可能です。
つまり、「$xが100以外、もしくは$yが200以外、ではない場合」の裏を返すと、BETTERのように「$xが100か、$yが200の場合」となります。
このコードの可読性が高い理由は、前編の冒頭で紹介した「リーダブルコード」で定義された「何を持ってリーダブルコードたるのか」の「読み進めるために覚えておく内容が最小のコード」や「脳を疲れさせないコード」に該当するからだと考えます。
BEFOREの例では、最初に否定(!)の論理演算が来ており、続く式の結果の否定演算をすることになります。
したがって、続く式、つまりここでは$xと$yの条件の結果が出るまで、否定の演算は保留されることになります。
「保留(スタックと言っても良いかもしれません)」とはすなわち、コードの読み手に「記憶しておく」ことに他なりません。この例でいえば、「◯◯を否定する」の「◯◯」が解決するまで「否定する」を記憶しておくことになります。お分かりの通り「◯◯」の数が多ければ多いほど、長ければ長いほど、複雑なら複雑なほど、読みにくければ読みにくいほど、時間がかかります。時間がかかるということは、短期記憶に留めておかなければならない時間も長くなります。
また、得てしてこうしたパターンでは、「◯◯」の中にもさらに「保留」が要求される条件が入り込んでいる場合があります。ちょうどネストされたif文を読むような状況に近くなるかもしれません。短期記憶に保存される量が多ければ多いほど、よほど万能な記憶力の持ち主でない限り、シームレスに記憶を呼び戻すことは難しいでしょう。したがって、「◯◯」を読み解いたあとに、保留されていた条件を呼び戻して条件を完成させて理解することになります。
この読み解きに失敗すれば「もう一度初めから読み直す」ことになり、コードリーディングの時間コストが増大していきます。
BETTERの例では、個別の条件を判断し終えたら「忘却」してよい状態になります。場合によっては「その先を読む必要がない」こともあります。
読み解きは実にシンプルで、
- $xが100、$yが200なら、ifブロックを読み解く
- $xが100でないなら、ifブロックは無視
- $yが200でないなら、ifブロックは無視
のように理解できます。
上の例が極端なだけではないか、と考える方もおられると思います。
この例だけに着目すれば、あきらかな作為的な例題に見えます。しかし現実の仕様を読み解く場合、こうしたパターンが暗黙的に潜んでいることが往々にしてあります。それをロジックとして落とし込むときに、上の例のように「そのままロジックにしてしまう」ことも発生しうるものです。
if文の条件が複雑になったり、if文やswitch文がネストしたり、条件式が重なるような実装になっていたら、一旦気持ちを実装から引いて考えてみるのがよいでしょう。
コードの複雑度を高めない
- ifやswitch、forなどのネストは、メソッド分解をするなどして名前づけをし、読みやすい粒度に置き換える。それでも長くなるようであれば、根本的に設計を見直す。
たとえば、以下のようなコードがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public function connectToServer() { // ..... if ($statusCode > 0) { switch ($statusCode) { case CONNECTION_REFUSED: $message = "connection refused"; break; case CONNECTION_TIMED_OUT: $message = "connection timed out"; if ($seconds > 300) { $message += " (too long; over 300secs)"; } break; default: $message = "unknown error"; } } // ..... } |
パッと見ただけでもif文の中にswitch文があり、case文の1つにさらにif文がありますね。
ネストが深く、高いコードの複雑度を持っていることがわかります。
これらは明らかに可読性を下げています。前編でお話したように、一部のコードをメソッドに分割し、適切に名前付けすることで、コード可読性を高めるとともに、複雑度の高いコードを回避するように実装すべきでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public function connectToServer() { // ..... if ($statusCode > 0) { $message = $this->getStatusMessage($statusCode, $seconds); } // ..... } private function getStatusMessage(int $statusCode, int $seconds): string { switch ($statusCode) { case CONNECTION_REFUSED: return "connection refused"; case CONNECTION_TIMED_OUT: return $this->appendSecondsIfExceeded($seconds, "connection timed out"); } return "unknown error"; } private function appendSecondsIfExceeded(int $seconds, string $message): string { return $seconds > self::TIME_EXCEEDED) ? "$message (too long; over " . self::TIME_EXCEEDED . "secs)" : $message; } |
これにより、
- ネストが解消され、無駄に記憶を必要とせず、脳を疲れさせない。
- すなわち、一度にみるべきコードブロックの行数が減り、見通しやすい。
- やりたい処理が適切に名前付けされており、必要なときだけその処理を読めば済む。
- ユニットテストコードが書きやすくなる。
というメリットが生まれます。
また別案として、「リスコフの置換原則」を用いて解決する方法もあるかと思います。
詳細は割愛しますが、たとえば以下のように、interfaceを実装したエラー用のclassを個別に定義し、必要な箇所でnewしてオブジェクトを生成して返し、メッセージ表示はオブジェクトを使用するなどの実装もあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
<?php define("CONNECTION_FAILURE", 0); define("CONNECTION_REFUSED", 1); define("CONNECTION_TIMED_OUT", 2); interface ConnectionError { public function getMessage(): string; }; class ConnectionUnknown implements ConnectionError { public function getMessage(): string { return "Unknown error."; } } class ConnectionRefused implements ConnectionError { public function getMessage(): string { return "Connection refused."; } } class ConnectionTimedOut implements ConnectionError { private $secsTimedOut = 0; function __construct(int $secs) { $this->secsTimedOut = $secs; } private function buildTimedOutMessage(): string { return $this->secsTimedOut > 0 ? " (too long; over {$this->secsTimedOut} secs)" : ""; } public function getMessage(): string { $timedOutMessage = $this->buildTimedOutMessage(); return "Connection timed out{$timedOutMessage}."; } } function getStatusMessage(int $statusCode, int $secs = 300): ConnectionError { switch ($statusCode) { case CONNECTION_REFUSED: return new ConnectionRefused(); case CONNECTION_TIMED_OUT: return new ConnectionTimedOut($secs); } return new ConnectionUnknown(); } // show any messages echo getStatusMessage(CONNECTION_REFUSED)->getMessage() . "\n"; echo getStatusMessage(CONNECTION_TIMED_OUT)->getMessage() . "\n"; echo getStatusMessage(CONNECTION_TIMED_OUT, 200)->getMessage() . "\n"; echo getStatusMessage(CONNECTION_FAILURE)->getMessage() . "\n"; |
ifが不要なら書かない
- そもそもifを使わなくて良いなら、使わない。さらに、elseを使わなくて良いなら、使わない。
コード中において、読み解くキーワードが少なければ少ないほど、早く読み解くことができると言えます。
if文は読み解く先を分岐させ、思考の流れを止めてしまう要因になりがちです。したがって、if文を書かなくて済むなら、極力書かない方が良いと言えるでしょう。
以下はわかりやすく示した例になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// (1) function isSuccess($status) { if ($status === "SUCCESS") { return true; } else { return false; } } // (2) function isSuccess($status) { if ($status === "SUCCESS") { return true; } return false; } // (3) function isSuccess($status) { return $status === "SUCCESS"; } |
(1)の例では、if/elseを使ってture/falseを返しています。
しかし、そもそもelseで他の仕事がないのであれば、elseは不要です。それが(2)となります。
もっと言えば、特定の条件を判断してture/falseだけ返すのであれば、(3)のように条件それ自体のtrue/falseを直接返せばよく、そもそもif文を用意する必要すらないでしょう。
設計と実装
実装に求められるテクニックも多岐にわたり非常に重要ですが、そもそも設計がおざなりであれば、実装もむごいことになります。
私の所属するプロジェクトでは、私の経験からいえることとして、設計に時間を十分にかけるように伝えています。設計さえある程度しっかりしていれば、実装でむごい目に会うことも少なく、途中で仕様変更などが発生しても柔軟に対応することができると考えています。
設計に時間をかける
- 仕様を把握してからは、十部に設計に時間を使うこと。設計を雑・適当にしてしまうと、実装が複雑化したり、設計時に気付かない問題があっても実装上後戻りできず複雑なコードになってしまうことがある。
- 目安として、全体を100%とした場合、設計には50〜60%程度、実装に30〜40%程度、テストに10%程度のリソースを当てる。
パーセンテージは完全に目安であり、プロジェクトやタスク、あるいは組織やチームによっても異なるところだと思います。
そういう違いがあったとしても、やはり設計にウェイトを置くべきではないかと、考えています。
もちろん、やみくもに設計に時間をかければよいわではないですし、簡単に済む設計もあると思います。
最近では少なくなったと思いますが、一時期はアジャイル的な開発手法の場合、イテレーション・スプリント・タイムボックスなどといった開発サイクル単位で考えるため、その都度設計を検討すれば良いといった誤解も見受けられた時期があったと思います。
作りたいものが、プロジェクトの中で180度反転することはほぼないと思います。したがって、設計に十分時間を使ったからといって、途中変更したらその時間が無意味に帰するとか、都度都度考えるべきことであるわけではないと考えています。
コードの存在理由を明確にする
- どこに記述・配置しても問題ないコードがあったとしても、そこに書く以上は「そこに記述する合理的理由」を必ず持つこと。
- 必要最小限のコードを必要な場所に記述することで、コード品質を担保する。
- もし本当にそのコードが「どこに書いても良い」のであるとしたら、もしかしてそのコードは不要である可能性も視野に入れる。
前後編にわたり「可能な限り書かない」シリーズをいくつかご紹介していますが、必要最小限の実装ですませられるようなコードを心がければ、その実装がそこに存在すると言う合理的理由にのみ依拠する結果となるでしょう。
じっくりコードを見て、別の場所にあっても全く問題がないコードであれば、それは別の実装として抽出できるコードか、どこにも収まる場所がみつからないのであれば、不要なコードです。
抽象化する
- forやforeachなどのループ、ifが多重になっている、あるいはそれらの組み合わせ、あるひと塊の計算コードなどが出てきたら、それらを「命名」し、メソッドにして「抽象化」することを考える。
- さらにある種のメソッドが増えてきた場合、インタフェースとクラスを設計して抽象化できるかどうか十分に検討をする。
- 「複雑なものに名前をつける」ことは一種の抽象化であり、抽象化することで「コードを理解する際に具体的な内容まで理解する必要がない」という、とても重要なメリットが生まれる。このことを、常に頭に浮かべて設計と実装を行わなければならない。
条件やループが登場すると言うことは、そこに複雑さを持つ要素があると判断して構わないと思います。
クラスやメソッドや関数として抽出し、十分適切な名前を与えることで、抽象化していく必要があります。
単一のクラスやメソッドが巨大に膨れ上がってきた場合も、もちろん抽象化が足りていない証明たりえます。場合によってはインタフェースを定義した上で複数のクラスに分割するなどのリファクタリングが必要となるでしょう。
プロのソフトウェアエンジニアの心構え
最後は地味に昭和のオッサンのお説教になってしまいますが、老若男女問わず、ソフトウェアエンジニアたる者この心構えであれ、というものをチームには伝えています。
こちらは皆様の参考にはならないかもしれませんが、一応列挙しておきたいと思います。個別の解説も割愛します。読んでの通り、というところかと思いますので…。
また、昨今では「KISS原則」「YAGNI原則」「SOLID原則」といった各種原則は、多くのデベロッパーが知るところとなっていると思いますが、ともすると忘れがちでもあります。自戒を込めての記載としています。
技術に溺れない
- 技術は正確・着実に自分に積み重ね、プロダクトに貢献していくべきものである。技術を単に使うだけのモチベーションになってはいけない。
- 最新技術や新たな試みは常に刺激に満ち溢れている。しかしそれをプロダクトに採用する場合、既存の技術では不可能であること、もしくはメリットを大きく上回ることを、明確に説明できなければならない。
- 明確な理由や、選択肢が1つしかない場合を除き、想定する技術的選択肢は広く持たねばならない。広い選択肢により、多くの技術的検討がなされるべきである。チームでの議論を経て選択されなければならない。
KISS、YAGNI、SOLID原則
- 変更に対して柔軟に対応できるプロダクトを開発する場合に、最低でも以下の3原則は頭に入れ、実践し続ける必要がある。
- KISS:Keep It Simple, Stupid. (シンプルに保て)
- YAGNI:You Ain’t Gonna Need It. (いま必要ない実装はするな)
- SOLID原則:メンテナンス性のよいオブジェクト指向プログラミングにおける5つの設計原則。
終わりに
前後編を通じて、プロジェクトのデベロッパーに実践してもらいたいルール・規則、デベロッパーとしての心構えなどを書かせていただきました。
ご紹介した内容すべてが正しいわけではないと思います。また前編冒頭に書いたとおり、チームやプロジェクトにとって不要なセオリーというものもあります。
私たちは、常に先人の重要な知見を取り込みながら、過去のエンジニアたちの苦い経験を繰り返さずに目的を達成することができます。
そして、こうした知見が身近で手に入れられることは、非常に大きな強みではないでしょうか。
プログラミングもサーバもフロント(チョトだけ)もインフラ(オンプレ中心)もwebもゲームも組み込みも幅広く何でも雑多にやってきた古株ですが、フルスタックではありません。好きなものはプログラミングと計算機科学。我々と一緒に好きなプログラミングを楽しみませんか?