「秩序のない分岐なんてほろんじまえ!!!!」
レガシーなシステムを触った時に、こう思ったことはあるのではないでしょうか。
私にもよくそう思うときがあります。
そんな絶望を生み出さないように分岐に関するアンチパターンと対策を考えてみました。
まず、なぜ生み出されるのかを書き出してみましょう。
秩序のない分岐
-
たくさんの分岐
→迷子になる -
ネストが多い
→インデントが見づらい。 -
分岐ごとに条件が複雑
→覚えるのがつらい。 -
気づいたらスパゲッティコード
→処理を追っている間にお腹が空き、スパゲッティを食べちゃうので太る。 -
テストがしづらい。テストケースが作りづらい。
→テストをするためのテストが必要とか言い出してしまう。 -
ifだけに畏怖
→書きたかった。
分岐処理に出会った時、これだけの問題があることに気づきました。
私はもう絶望したくないし、私のように絶望する人を作りたくない。(そして太りたくない。)
そのためにも実際に遭遇するアンチパターンをもとに対策をしていきましょう。
読む前の注意
- 初心者から中級者向けの内容です。
- コードは全てPHPで書いていますが、どの言語でも参考にはなると思います。
- 2015年に書いた記事なので、PHP5系ベースになります。
- PHP7の機能を使わずに書いているので、どの言語でも置き換えやすいと思います。
- 記載されているソースコードは、実際に動作確認までは行っていません。また特定箇所の抜き出しのような記述になっているのでコピペでは動かないです。
- サンプルのソースコード中のタイポなどには寛容な目でお願いします。
アンチパターン
分岐を扱う変数名のアンチパターン
なんと変数名だけで私を悩ませるときが存在する・・・。
「そんな馬鹿な」と思うかもしれないが、まずはアンチパターンを見てみよう。
$delete_off = false;
「削除のオフ」を宣言している変数がある。
offという単語があるので、boolean型を想定しやすくはなっているが、offが曲者である。
初期値がfalseにするとわかるのだが、否定の否定になっていることに気づくのではないだろうか。
日本語で表すなら、「やばくなくない?」と同じ状態である。
このパターンを「やばくなくない?パターン」と名づけたい。
これを実装している人がいたら、「この実装、やばくなくない?」と言ってあげよう。
対策するなら下記のように実装しよう。
$delete_on = true;
否定の否定を回避するだけでずっと読みやすくなる。
変数名に「no」が存在している場合にも注意しよう。
基本的に変数名は肯定で記述しよう。
※「exclude」という単語も可能であればやめて、「include」を使用しよう。
また、この場合のベストは下記になる。
$is_deleted = true;
「is」、「can」、「exists」等、boolean型を連想しやすい言葉を選択することにより、さらに読みやすいコードになるので覚えておこう。
ネストでの分岐アンチパターン
記事のオブジェクトである「$article
」が持つ情報が正常なら処理をするという実装で考えてみよう。
まずは、アンチパターンを見てみよう。
// 空判定
if (!empty($article)) {
// タイプ判定
if ($article->type === 'news') {
// 日付判定
if (strtotime($article->publish_date) < strtotime('now')) {
return // 正常処理
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
インデントが深い・・・。if/elseの連続でものすごく見づらい・・・。
インデントの深さが心に突き刺さる・・・。
この例だとまだ理解はしやすいが、すぐにスパゲッティコードになるので気をつけよう。
対策するなら下記のように実装しよう。
// 空判定
if (empty($article)) {
return null;
}
// タイプを判定
if ($article->type !== 'news') {
return null;
}
// 日付判定
if (strtotime($article->publish_date) >= strtotime('now')) {
return null;
}
return // 正常処理
ネストを排除したことにより、ものすごく見やすくなった。
また、例外処理を行っていくことにより、最後の処理が必然的に正常系の処理でまとまるようになったので、第三者が見ても理解しやすくなったはずである。
心がえぐられない!
「例外処理はとっととreturn」と覚えておこう。
分岐された値を取得する場合のアンチパターン
記事のタイプにそって「id」を取得する処理で考えてみよう。
まずは、アンチパターン。
// 記事タイプでの比較
if ($article->type === 'news') {
return 1;
} else if ($article->type === 'entertainment') {
return 2;
} else if ($article->type === 'recipe') {
return 3;
}
return null;
並列で3回もif文が存在している。
イフイフ・・・。
ネストしていないだけまだ良いが、実はif文を1つだけでこのパターンを処理することが可能である。
対策するなら下記のように実装しよう。
$article_types = [
'news' => 1,
'entertainment' => 2,
'recipe' => 3
];
if (!array_key_exists($article->type, $article_types)){
return null;
}
return $article_types[$article->type];
連想配列(map, hash)を使用することによって、ずっと見やすくなった。
さらに定義自体も同じところに集約できたので、設定を増やしたい場合は配列内の拡張のみで対応できるようになった。
ソースの大きな修正も必要なくなったのである。
すばらしい!
「連想配列を使用すれば、分岐もすっきり」と覚えておこう。
さらにヘルパーを利用して、視界からif文を消してしまおう。
$article_types = [
'news' => 1,
'entertainment' => 2,
'recipe' => 3
];
// array_get(検索対象, 検索文字、存在しない場合のデフォルト値)
return array_get($article_types, $article->type, null);
array_get()
というヘルパーを利用して、if文を消すことができてしまった。
すばらしいーーー!!
ただ、ここで注意して欲しいのは、array_get()
は、PHPで用意されている関数ではないということ。
Laravelのフレームワーク上で実装されているヘルパーである。
「Laravelを使用しないとダメじゃねーか」って思ったそこのあなた、安心してください。ヘルパーですよ。簡単に導入できます。
githubで、Laravelのヘルパーだけ切り出している方がいたので、これを使用してみましょう!
※ヘルパーなのでPHP以外の他言語でも実装は簡単に出来ると思います。
※ドット記法(dot-notation)で値が取得できるので、ドットの添え字には気をつけてください。
-
Laravelのヘルパーだけのライブラリ(github)
https://github.com/rappasoft/laravel-helpers/blob/master/src/helpers.php -
laravelで用意されている便利ヘルパー一覧(公式リファレンス)
http://laravel.com/docs/master/helpers#method-array-get
このようなヘルパーを知ることにより、実装が楽になるので、是非試してみよう。
分岐された関数を実行する時のアンチパターン
記事のタイプ毎に情報を取得する関数を実装する場合で考えてみよう。
まずは、アンチパターン。
if ($article->type === 'news') {
return $this->getNewsInfo();
} else if ($article->type === 'entertainment') {
return $this->getEntertainmentInfo();
} else if ($article->type === 'recipe') {
return $this->getRecipeInfo();
}
return null;
こんな実装もよく見るのではないかと思う。
先ほどと同様にやっぱりイフイフ、、、やっぱり読みづらい。
ここも対策してみよう。
$type_classes = [
'news' => NewsType::class,
'entertainment' => EntertainmentType::class,
'recipe' => RecipeType::class
];
if (!array_key_exists($article->type, $type_classes))
{
return null;
}
$type_instance = new $type_classes[$article->type];
return $type_instance->getInfo();
コード量は少し増えたが、if文はひとつになった!
また、生成されたインスタンスからの取得方法が全て統一されている!
難しいことをやっていそうだが、個別で定義していた関数「getNewsInfo()
」などを削除し、それぞれのクラスでgetInfo()
関数を実装しただけである。
このパターンは、Stateパターンと呼ばれるデザインパターンである。
NewsTypeクラスが何をしているか見てみよう。
// todo: Interface(or abstract)を省略しているが、実装すべき。
class NewsType {
public function getInfo() {
// 情報を取得する処理は省略
return $info; // 情報
}
}
上記の実装を見てもらえればわかるが、$info
を返しているだけである。
これを、EntertainmentType、RecipeTypeとそれぞれクラスを実装すれば良い。
各処理の依存は全てそのクラス内で実装すればよい。これがカプセル化である。
この実装で、Stateパターン + カプセル化が出来たのである。
分岐対策って素晴らしい!
ちなみにだが、このケースだとarray_get()
の恩恵は受けない。
$type_classes = [
'news' => 'NewsType',
'entertainment' => 'EntertainmentType',
'recipe' => 'RecipeType'
];
$type_class = array_get($type_classes, $article->type, null);
if(empty($type_class)) {
return null;
}
$type_instance = new $type_class;
return $type_instance->getInfo();
どうしてもクラスを持っていない場合を考慮しなければいけないので、この場合であれば、array_get()
は使わない方が良い。
ちなみにではあるが、「分岐された値を取得する場合のアンチパターン」で書いたidを取得する処理をgetId()
として、このクラスに持たせても良い。
class NewsType {
public function getId() {
return 1;
}
public function getInfo() {
// 情報を取得する処理は省略
return $info; // 情報
}
}
getId()
を作成してidを持たせることにより、NewsTypeクラスには、NewsTypeに依存する仕様が集まったことになる。
このクラスを見るだけで、Newstypeの仕様がわかるようになったと言えよう。
オブジェクトの拡張が続くようなら、下記のようにインスタンスを返してあげると良い。
$type_instance = new $type_class;
return $type_instance;
Stateパターンを使えば、仕様を一箇所にまとめることができるのである。
すばらしい!!
「分岐で「状態」や「タイプ」を参照し始めたら、Stateパターンを使うチャンス」と覚えておこう。
オブジェクト内に存在する分岐のアンチパターン
オブジェクトを扱う時にも、分岐は発生している。
画像処理用のパッケージであるImagickを使用して、リサイズ+圧縮をするという実装で考えてみよう。
ここからは少し難しくなってくる。
$file = 'test.jpg';
$width = 100; // 幅
$height = 100; // 高さ
$compression = 90; // 圧縮
echo resize($file, $width, $height, $compression); // 変換後の画像を出力
function resize($file, $width, $height, $compression=null) {
if (empty($file)) {
return null;
}
$image = new Imagick($file)
if (!emtpy($width) && !empty($height)) {
// $imageをリサイズ
}
if (!empty($compression)) {
// $imageを圧縮
}
return $image;
}
一見よさげな例であるが、この実装にはたくさんの問題がある。
- リサイズと圧縮の順番を入れ替えることができないので、圧縮してからリサイズするという関数を作らなくてはいけない。
- どちらが先に実行されるかわかりづらい
- 関数内のif文が多いのでテストケースをたくさん書かなければいけない
この処理の中にはコード以上にとても多くの分岐が存在しているのである。
ただ、これも対策方法がある。
$file = 'test.jpg';
$width = 100; // 幅
$height = 100; // 高さ
$compression = 90; // 圧縮
echo new Image($file)->resize($width, $height)->compression($compression)->get(); // 変換後の画像を出力
class Image() {
// イメージオブジェクトを管理する変数
private $image = null;
// コンストラクタ
public function __construct($file) {
$this->set($file);
}
// イメージオブジェクトをセット
public function set($file) {
$this->image = new Imagick($file);
return this;
}
// リサイズ
public function resize($width, $height) {
// リサイズ処理省略
$this->image = $resize_image;
return $this;
}
// 圧縮
public function compression($compression) {
// 圧縮処理省略
$this->image = $compression_image;
return this;
}
// イメージオブジェクト取得
public function get() {
return $this->image;
}
}
ソースコードは多くなってしまったが、再利用性が高い実装に変えることができた。
大きく変わった点としては下記がある。
- チェーンメソッドを利用して状態を変えることができるようになり、
resize()
、compression()
が柔軟に行える。 - 「
resize()
はリサイズだけ」、「compression()
は圧縮だけ」と処理が集中できるようになったことにより、テストケースが書きやすくなった。
この実装によって多くの潜在的な分岐が減ったのである。
この実装は、CQS(コマンドとクエリ分離原則)という考えで実装している。
CQSはオブジェクト内に定義した関数は、コマンドとクエリに分けることができるというものである。
-
コマンド = 状態を変更するための関数(副作用がある)
→resize()
、compression()
、set()
が該当する。 -
クエリ = 状態を取得するための関数(副作用がない)
→get()
が該当する。
CQSってすばらしい!!
アンチパターンの例では、コマンドとクエリが混在していたため、処理が複雑になっていたのである。
コマンドとクエリを明確にすることにより、複雑なコードが最適なコードとして定義することができるようになったと言えよう。
「複雑な関数ができてしまったら、その時はCQSのチャンス」と覚えておこう。
分岐を抑えるその他の方法
ここまで書いた分岐のアンチパターンは一例であるし、その対策も一例である。
さらにいえば、設計や思想まで「秩序のない分岐」を考慮していく方法が存在する。
分岐を抑えるその他の対策(一例)
-
MVC(Model View Controller)
→Routing、Controller、Model、Viewに分け、それぞれの役割を明確にし、秩序のない分岐を減らす。 -
DDD(ドメイン駆動設計)
→複雑なドメインの設計はモデルベースに置くことにより、秩序のない分岐を減らす。 -
CQRS(コマンドとクエリの責務分離)
→CQSの上位概念としてアーキテクチャレイヤーでコマンドとクエリを分割し、一方通行のデータフローを提供し、秩序のない分岐を減らす。 -
react + flux
→CQRSと似たアーキテクチャ。フレームワークレベルでコマンドとクエリを分け、一方通行のデータフローを提供し、秩序のない分岐を減らす。 -
人や思想のレイヤーで考えたコンウェイの法則
→「システムを設計する組織は、その構造をそっくりまねた構造の設計を生み出してしまう」という考えが存在し、組織を改善することにより、秩序のない分岐を減らす。
今日の分岐アンチパターンの復習
- その変数やばくなくない?
- 正常か例外で判定した分岐は、例外処理ならとっととreturn。
- 分岐で「状態」や「タイプ」を参照し始めたら、Stateパターンを使うチャンス。
- 複雑な関数ができてしまったら、CQSのチャンス。
- デザインパターンやアーキテクチャを知ることにより、分岐を整理することができる。
最後に
プログラマやエンジニアの世界では、デザインパターンや設計手法が身近に存在しているので、「秩序のない分岐」を簡単に減らせるはずだと思っている。
また、システムやアーキテクチャだけではなく、社会や組織単位でも言える事であり、もし対策できていない「秩序のない分岐」を消せた時、それは世界そのものを変えた時なんじゃないかなと思っている。
プログラマやエンジニアは世界を変えるだけの力があるし、それが身近であるはずである。
長くなりましたが、これをきっかけにクリエイティブな開発の手助けになれば幸いです。
それではよいプログラミングライフをー!