ブログトップ 記事一覧 ログイン 無料ブログ開設

Strategic Choice

2014-10-01

[]式の分割:文も分割

どういうこと?

式の分割についての手法は、文の分割にも有効なので、これを文にも適用します。

どうして?

「巨大な式」同様、「巨大な文」も読みにくいものです。

例えば、以下のコードは、すぐには理解できません。

var update_highlight = function (message_num) {
	if ($("#vote_value" + message_num).html() === "Up") {
		$("#thumbs_up" + message_num).addClass("highlighted");
		$("#thumbs_down" + message_num).removeClass("highlighted");
	} else if ($("#vote_value" + message_num).html() === "Down") {
		$("#thumbs_up" + message_num).removeClass("highlighted");
		$("#thumbs_down" + message_num).addClass("highlighted");
	} else {
		$("#thumbs_up" + message_num).removeClass("highlighted");
		$("#thumbs_down" + message_num).removeClass("highlighted");
	}
};

それぞれの式はそれほど大きなものではありませんが、それらがすべて一箇所に集まると、「巨大な文」となって一斉に襲いかかってきます。

どうすれば?

文を分割します。

上述例であれば、幸いなことに、同じ式がいくつかあるので、それらを要約変数として関数の最上部に抽出します。

var update_highlight = function (message_num) {
	var thumbs_up = $("#thumbs_up" + message_num);
	var thumbs_down = $("#thumbs_down" + message_num);
	var vote_value = $("#vote_value" + message_num).html();
	var hi = "highlighted";

	if (vote_value === "Up") {
		thumbs_up.addClass(hi);
		thumbs_down.removeClass(hi);
	} else if (vote_value === "Down") {
		thumbs_up.removeClass(hi);
		thumbs_down.addClass(hi);
	} else {
		thumbs_up.removeClass(hi);
		thumbs_down.removeClass(hi);
	}
};

「var hi = "highlighted"」の部分は厳密には不要です。しかし、同じものが6回も登場しているし、こうしたほうが便利なこともあります。

  • タイプミスを減らすのに役立つ。
  • 横幅が縮まり、コードが読みやすくなる。
  • CSSクラス名を変更することになれば、一箇所を変更すればいい。

2014-09-30

[]式の分割:マクロの導入

どういうこと?

「マクロ」を使用して、式をシンプルにします。

どうして?

「マクロ」を使用すると、式が簡潔になり、読みやすくなることがあります。

例として、以下のコードを見てみます。

void AddStats(const Stats& add_from, Stats* add_to) {
	add_to->set_total_memory(add_from.total_memory() + add_to->total_memory());
	add_to->set_free_memory(add_from.free_memory() + add_to->free_memory());
	add_to->set_swap_memory(add_from.swap_memory() + add_to->swap_memory());
	add_to->set_status_string(add_from.status_string() + add_to->status_string());
	add_to->set_num_processes(add_from.num_processes() + add_to->num_processes());
	// ...
}

どの式も長くて、よく似ていますが、同じものではなさそうです。でも、よく見ると、フィールド名が違うだけで、どの式も同じことをしていることに気づきます。

add_to->set_XXX(add_from.XXX() + add_to->XXX());

マクロを定義すれば、式を簡潔にできます。

void AddStats(const Stats& add_from, Stats* add_to) {
	#define ADD_FIELD(field) add_to->set_##field(add_from.field() + add_to->field())
	ADD_FIELD(total_memory);
	ADD_FIELD(free_memory);
	ADD_FIELD(swap_memory);
	ADD_FIELD(status_string);
	ADD_FIELD(num_processes);
	...
	#undef ADD_FIELD
}

すべてのゴミをはぎ取ることができました。コードを見てすぐに本質が理解できます。それぞれの行が同じことをしているのも明確です。

どうすれば?

言語でサポートされていれば、「マクロ」を使用して、式を簡潔にします。

何も「マクロを頻繁に使え」と言ってるわけではありません。濫用すると、逆にコードがわかりにくくなったり、見つけにくいバグが潜り込んでしまうこともあります。マクロを禁止しているプロジェクトもあります。

しかし、上述例のように、「簡潔で読みやすくなる」という明確な利点がもたらされるなら、検討する価値はあります。

2014-09-29

[]式の分割:反対から考える

どういうこと?

式が複雑になりすぎてしまった場合、問題を「反対」から考えてみて、シンプルにならないかどうか検討します。

どうして?

物事を反対から考えると、うまくいく場合があります。

どうすれば?

「反対」から問題解決を試みます。

例えば、「配列を逆順にイテレートしてみる」「データ後ろから挿入してみる」など、とにかくいつもと反対のことをやってみるのです。

具体的に、Rangeクラスで考えてみます。

struct Range {
	int begin;
	int end;
	// 例えば、[0,5) は[3,8) と重なっている。
	bool OverlapsWith(Range other);
};

以下の図は、範囲の例です。(白丸は値を含まない、の意味。)

f:id:asakichy:20140428180746p:image

ここでは、「A」「B」「C」はお互いに重なっていませんが、Dはすべてと重なっています。

いずれかの端が、他の範囲と重なっているかを確認する「OverlapsWith()」を実装します。

bool Range::OverlapsWith(Range other) {
  // 'begin' または 'end' が 'other' のなかにあるかを確認する。
  return (begin >= other.begin && begin <= other.end) 
    || (end >= other.begin && end <= other.end);
}

まだ2行しか書いていませんが、これからもっと増えていきます。以下の図は、すべてのロジックを示したものです。

f:id:asakichy:20140428180745p:image

場合分けや条件が多すぎて、バグを見逃しやすそうです。というか、もうバグがあります。先のコードでは、本当は重なっていないRange [0,2) と [2,4) が重なってしまいます。

begin/end の値を比較するときには、「<=」と「<」の使い分けに注意しなければなりません。この問題を修正したものが以下になります。

  return (begin >= other.begin && begin < other.end)
    || (end > other.begin && end <= other.end);

これで正しくなったと思いきや、まだ他にもバグがあります。begin/end が他の範囲を取り囲んでいるケースが抜けています。この処理を加えたコードが以下になります。

  return (begin >= other.begin && begin < other.end) 
    || (end > other.begin && end <= other.end) 
    || (begin <= other.begin && end >= other.end);

やはり、コードが複雑になってしまいました。これでは誰もコードを読んでくれませんし、これが絶対に正しいと自信を持って言うこともできません。

そこで、「OverlapsWith()」 の「反対」を考えてみます。この場合、「重ならない」にななります。2つの範囲が重ならないのは簡単です。2つの場合しかありません。

  1. 一方の範囲の終点が、ある範囲の始点よりも前にある場合。
  2. 一方の範囲の始点が、ある範囲の終点よりも後にある場合。

これをコードに置き換えるのは簡単です。

bool Range::OverlapsWith(Range other) {
	if (other.end <= begin) return false; // 一方の終点が、この始点よりも前にある
	if (other.begin >= end) return false; // 一方の始点が、この終点よりも後にある
	return true; // 残ったものは重なっている
}

一度に1 つずつしか比較していないので、コードがずっと単純になりました。これで「<=」が正しいかどうかを集中して見れるようになります。

2014-09-26

[]式の分割:短絡評価を悪用しない

どういうこと?

「短絡評価」の濫用・悪用は避けるようにします。

ブール演算子は短絡評価を行うものが多くあります。例えば、if (a || b) のaがtrue なら、bは評価されることがありません。

どうして?

「短絡評価」の動作は便利ですが、悪用すると複雑なロジックになります。

以下のようなコードがあります。

assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());

これは「このキーのバケツを取得する。もしバケツがnull じゃなかったら、中身が入っていないかを確認する」という意味です。たった1 行のコードですが、しばらく立ち止まって考えなければ理解できません。

以下のコードと比較してみます。

bucket = FindBucket(key);
if (bucket != NULL) assert(!bucket->IsOccupied());

全く同じものですが、こちらは2行になっています。コードは長くなりましたが、ずっと理解しやすくなりました。

1行で書いてしまったのは、そのとき「オレは頭がいい」と思ってしまったからです。ロジックを簡潔なコードに落とし込むことに一種の喜びを感じていたのです。この気持ちは、少なからず理解できます。パズルを解いているような感覚です。

問題は、これがコードのスピードバンプ*1になっていたことです。

どうすれば?

「短絡評価」を濫用するなど、「頭がいい」コードにならないように気を付けます。「頭がいい」コードは、あとで他の人がコードを読むときにわかりにくくなります。

ただし、「短絡評価を絶対に避けろ」という意味ではありません。以下のように、短絡評価が簡潔に使えることも多くあります。

if (object && object->method()) ...

また、「Python」「JavaScript」「Ruby」などの言語では、複数の引数のなかから1 つを返す「OR」演算子が使えます*2。例えば、以下のコードは、「a」「b」「c」の中から、最初に「真」と評価できるものを選びます。

x = a || b || c

*1:車を減速させる路面の隆起のことです。ここでは、コードを読む速度を遅くさせるものという意味になります。

*2:値がブール値になるわけではありません

2014-09-25

[]式の分割:ド・モルガンの法則による論理式変換

どういうこと?

「ド・モルガンの法則」を使用して、論理式を読みやすく変換します。

  1. not (a or b or c) ⇔ (not a) and (not b) and (not c)
  2. not (a and b and c) ⇔ (not a) or (not b) or (not c)

どうして?

「ド・モルガンの法則」を使うと、論理式を読みやすくできます。

例えば、以下のようなコードは、

if (!(file_exists && !is_protected)) Error("Sorry, could not read file.");

以下のように書き直せます。

if (!file_exists || is_protected) Error("Sorry, could not read file.");

どうすれば?

複雑な論理式があった場合、「ド・モルガンの法則」を使用することで、論理式が読みやすくなるかどうか検討します。

「ド・モルガンの法則」は、「notを分配してand/orを反転する」(逆方向は「notをくくりだす」)とすると覚えやすくなります。