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

Strategic Choice

2014-10-21

[]無関係の下位の問題の抽出:対象:グルーコード

どういうこと?

「無関係の下位問題」を積極的に見つけて、抽出します。

その他のコードを支援するためだけに存在する「グルー*1コード」は、別の関数に分離します。

どうして?

グルーコードは、プログラムの本質的なロジックとは関係ありません。

例えば、「{ "username": "...", "password": "..." }」のようにユーザの機密情報を含んだディクショナリがあります。これらの情報をURL に使いたいのですが、機密情報なので、Cipherクラスで暗号化したいとします。

ただし、Cipherクラスはディクショナリではなく文字列を受け取ります。欲しいのはURLセーフな文字列ですが、Cipherクラスは単なる文字列を返すようになっています。また、Cipherクラスは、他にも引数が必要なので、使いにくいクラスです。単純なタスクですが、多くのグルーコードが必要になります。

user_info = { "username": "...", "password": "..." }
user_str = json.dumps(user_info)
cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = cipher.update(user_str)
encrypted_bytes += cipher.final() # 現在の128ビットブロックをフラッシュする
url = "http://example.com/?user_info=" + base64.urlsafe_b64encode(encrypted_bytes)
...

ここで取り組んでいる問題は「ユーザの情報を暗号化してURLに含める」ことですが、コードの大部分が「オブジェクトをURLセーフな文字列にする」のためにあり、高レベルの目標が見えにくくなっています。

どうすれば?

グルーコードは「無関係の下位問題」なので、抽出します。

上述例であれば、以下のように関数を抽出します。

def url_safe_encrypt(obj):
	obj_str = json.dumps(obj)
	cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
	encrypted_bytes = cipher.update(obj_str)
	encrypted_bytes += cipher.final() # 現在の128 ビットブロックをフラッシュする
	return base64.urlsafe_b64encode(encrypted_bytes)

その結果、プログラムの本質的なロジックを扱うコードは簡潔になります。

user_info = { "username": "...", "password": "..." }
url = "http://example.com/?user_info=" + url_safe_encrypt(user_info)

*1:糊や接着剤のことです。

2014-10-20

[]無関係の下位問題の抽出:対象:デバッグコード

どういうこと?

「無関係の下位問題」を積極的に見つけて、抽出します。

「デバッグコード」は、「無関係の下位問題」です。

どうして?

「デバッグコード」は、高レベルの目標とは無関係です。

例として、Ajaxでデータを送受信する処理を見てみます。ここでは、サーバから返ってきたディクショナリをデバッグ表示しています。

ajax_post({
	url: 'http://example.com/submit',
	data: data,
	on_success: function (response_data) {
		var str = "{\n";
		for (var key in response_data) {
			str += " " + key + " = " + response_data[key] + "\n";
		}
		alert(str + "}");

		// 引き続き'response_data' の処理
	}
});

このコードの高レベルの目標は「サーバをAjaxで呼び出してレスポンスを処理する」ことです。しかし、このコードの大部分は「ディクショナリをキレイに印字する」という「無関係の下位問題」で占められています。これでは、高レベルの目標がとらえにくくなります。

どうすれば?

デバッグコードを抽出します。

上述例であれば、「format_pretty(obj)」関数を作成し、デバッグコードを抽出します。

var format_pretty = function (obj) {
	var str = "{\n";
	for (var key in obj) {
		str += " " + key + " = " + obj[key] + "\n";
	}
	return str + "}";
};

ちなみに、この抽出には、ほかにも色々な「良い副作用」があります。呼び出し側のコードは簡潔になりますし、「format_pretty()」は再利用できます。

中でも、最も大きな良い副作用の一つは、「コードが独立していれば、format_pretty() の改善が楽になる」点です。関数にすることで、小さくて独立したものになり、「機能追加」「読みやすさの向上」「エッジケースの処理」などが楽に行えるようになります。

例えば、抽出した「format_pretty(obj)」が抱えている問題は、以下の通りです。

  • obj にはオブジェクトを期待している。普通の文字列(やundefined)だと例外が発生する。
  • obj には単純な型を期待している。ネストしたオブジェクトだとobjectObject のように表示されるので、プリティとは言えない。

これらの問題は、関数化されているので、容易に改善できます。

var format_pretty = function (obj, indent) {
	// null・undefined・文字列・非オブジェクトを処理する。
	if (obj === null) return "null";
	if (obj === undefined) return "undefined";
	if (typeof obj === "string") return '"' + obj + '"';
	if (typeof obj !== "object") return String(obj);
	if (indent === undefined) indent = "";

	// (非null)オブジェクトを処理する。
	var str = "{\n";
	for (var key in obj) {
		str += indent + " " + key + " = ";
		str += format_pretty(obj[key], indent + " ") + "\n";
	}

	return str + indent + "}";
};

出力は以下のようになります。

{
	key1 = 1
	key2 = true
	key3 = undefined
	key4 = null
	key5 = {
		key5a = {
			key5a1 = "hello world"
		}
	}
}

2014-10-17

[]無関係の下位問題の抽出:対象:ユーティリティコード

どういうこと?

「無関係の下位問題」を積極的に見つけて、抽出します。

「ユーティリティコード」は、古くからある「無関係の下位問題」の代表例です。

どうして?

プログラムの核となる基本的なタスクというものがあります。例えば、「文字列の操作」「ハッシュテーブルの使用」「ファイルの読み書き」などです。

こうした基本的な「ユーティリティ」は、プログラミング言語の組み込みライブラリとして実装されています。ファイルの中身をすべて読み込みたければ、PHPなら「file_get_contents("filename")」、Pythonなら「open("filename").read()」が使用できます。

しかし、まれに自分でこの溝を埋めなければならないことがあります。ファイルの例でいうと、C++では、ファイルの中身をすべて読み込む方法が用意されていません。よって、以下のようなコードを自分で書くことになります。

ifstream file(file_name);

// ファイルサイズを計算して、バッファにそのサイズを割り当てる。
file.seekg(0, ios::end);
const int file_size = file.tellg();
char* file_buf = new char [file_size];

// ファイルをバッファに読み込む。
file.seekg(0, ios::beg);
file.read(file_buf, file_size);
file.close();
...

しかし、このコードは、高レベルの目標と直接関係ありません。

どうすれば?

ユーティリティコードは、関数化して置き換えるようにします。

上述例であれば、「ReadFileToString()」という新しい関数に置き換えます。すると、言語ライブラリが「ReadFileToString()」という関数をあらかじめ用意しているかのようなコードになります。

「このライブラリにXYZ() 関数があればなあ」と思ったら、その関数を自分で書くようにします。そうしたコードは、複数のプロジェクトで使えるユーティリティコードに成長していきます。

2014-10-16

[]無関係の下位問題の抽出

どういうこと?

「無関係の下位問題」を積極的に見つけて、抽出します。

「無関係の下位問題」とは、高レベルの目標に直接的に効果のない、低レベルの問題のことです。

どうして?

コードを読む際、「高レベルの目標」に集中できると、コードが理解しやすくなります。

たとえば、以下のコードの高レベルの目標は「与えられた地点から最も近い場所を見つける」ことです。

// 与えられた緯度経度に最も近い'array' の要素を返す。
// 地球が完全な球体であることを前提としている。
var findClosestLocation = function (lat, lng, array) {
	var closest;
	var closest_dist = Number.MAX_VALUE;
	for (var i = 0; i < array.length; i += 1) {
		// 2 つの地点をラジアンに変換する。
		var lat_rad = radians(lat);
		var lng_rad = radians(lng);
		var lat2_rad = radians(array[i].latitude);
		var lng2_rad = radians(array[i].longitude);

		// 「球面三角法の第二余弦定理」の公式を使う。
		var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
		                     Math.cos(lat_rad) * Math.cos(lat2_rad) *
		                     Math.cos(lng2_rad - lng_rad));
		if (dist < closest_dist) {
			closest = array[i];
			closest_dist = dist;
		}
	}
	return closest;
};

ループ内のコードは「無関係の下位問題」を扱っていいます。それは「2つの地点(緯度経度)の球面距離を算出する」ことです。コード量が多いので、新しい関数「spherical_distance()」に抽出します。

var spherical_distance = function (lat1, lng1, lat2, lng2) {
	var lat1_rad = radians(lat1);
	var lng1_rad = radians(lng1);
	var lat2_rad = radians(lat2);
	var lng2_rad = radians(lng2);
	// 「球面三角法の第二余弦定理」の公式を使う。
	return Math.acos(Math.sin(lat1_rad) * Math.sin(lat2_rad) +
	                 Math.cos(lat1_rad) * Math.cos(lat2_rad) *
	                 Math.cos(lng2_rad - lng1_rad));
};

残ったコードは以下のようになります。

var findClosestLocation = function (lat, lng, array) {
	var closest;
	var closest_dist = Number.MAX_VALUE;
	for (var i = 0; i < array.length; i += 1) {
		var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
		if (dist < closest_dist) {
			closest = array[i];
			closest_dist = dist;
		}
	}
	return closest;
};

コードがずっと読みやすくなります。難しそうな幾何学の計算に心を奪われることなく、高レベルの目標に集中できるようになりました。

「spherical_distance()」は個別にテストができる関数です。また、「spherical_distance()」は将来的に再利用可能な関数です。これが「無関係の」下位問題と呼ばれる理由です。完全に自己完結しているので、自分がアプリケーションにどのように使われるかを知らない、ということです。

どうすれば?

「無関係の下位問題」を抽出します。

簡単にいうと「プロジェクト固有のコードから汎用コードを分離する」ということです。ほとんどのコードは汎用化できます。一般的な問題を解決するライブラリやヘルパー関数を作っていけば、プログラムに固有の小さな核だけが残ります。

この技法が役に立つのは、プロジェクトの他の部分から分離された、境界線の明確な小さな問題に集中できるからです。こうした下位問題に対する解決策は、より緻密で正確なものになります。それに、あとでコードを再利用できるかもしれません。

以下、「無関係の下位問題」を抽出する手順です。

  1. 関数やコードブロックを見て「このコードの高レベルの目標は何か?」と自問する。
  2. コードの各行に対して「高レベルの目標に直接的に効果があるのか?」あるいは「無関係の下位問題を解決しているのか?」と自問する。
  3. 「無関係の下位問題」を解決しているコードが相当量あれば、それらを抽出して別の関数にする。

2014-10-15

[]変数:一度だけ書き込む

どういうこと?

変数の変更箇所はできるだけ少なくします。理想は「一度だけ」です。

どうして?

変数を操作する場所が増えると、現在値の判断が難しくなります。

どうすれば?

まず、定数を使います。

「永続的に変更されない」変数は扱いやすくなります。例えば、以下のような定数は、多くのことを考える必要がありません。

static const int NUM_THREADS = 10;

また、オブジェクトをイミュータブルに設計するのもいいアイデアです。実際、多くの言語では、Stringなどの組み込み型はイミュータブルになっています。