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

Strategic Choice

2014-10-24

[]無関係の下位問題の抽出:抽出先:プロジェクト内で独立

どういうこと?

抽出した「無関係の下位問題」は、プロジェクトから完全に独立させるのが望ましい状態です。それができない場合、プロジェクト内で独立させておくだけでも、そうする価値はあります。

どうして?

「無関係の下位問題」は、プロジェクトから完全に独立させるのが望ましい状態ですが、それができない場合や、適切でない場合があります。

たとえば、あるビジネスレビューサイトのコードを見てみます。以下の処理は、新しい「Business」オブジェクトを作って、「name」「url」「date_created」を設定しています。

business = Business()
business.name = request.POST["name"]
url_path_name = business.name.lower()
url_path_name = re.sub(r"['\.]", "", url_path_name)
url_path_name = re.sub(r"[^a-z0-9]+", "-", url_path_name)
url_path_name = url_path_name.strip("-")
business.url = "/biz/" + url_path_name
business.date_created = datetime.datetime.utcnow()
business.save_to_database()

「url」は「name」のクリーンバージョンです。例えば、name が「A.C. Joe's Tire & Smog, Inc.,」であれば、url は「/biz/ac-joes-tire-smog-inc」になります。

このコードの「無関係の下位問題」は、「名前を有効なURLに変換する」ことです。これは楽に抽出できます。ついでに、正規表現もプリコンパイルして、読みやすい名前もつけておきます。

CHARS_TO_REMOVE = re.compile(r"['\.]+")
CHARS_TO_DASH = re.compile(r"[^a-z0-9]+")
def make_url_friendly(text):
	text = text.lower()
	text = CHARS_TO_REMOVE.sub('', text)
	text = CHARS_TO_DASH.sub('-', text)
	return text.strip("-")

元のコードに「規則性」のあるパターンができました。

business = Business()
business.name = request.POST["name"]
business.url = "/biz/" + make_url_friendly(business.name)
business.date_created = datetime.datetime.utcnow()
business.save_to_database()

コードが読みやすくなりました。これで正規表現や文字列処理に心を奪われずに済みます。

ただ、「make_url_friendly()」の置き場所は問題です。汎用的な関数ですが、この正規表現は、アメリカのビジネス名だけを対象にしています。この場合、別のプロジェクトより、元のファイルと同じ場所に置いたほうがよさそうです。

どうすれば?

「無関係の下位問題」は、プロジェクトから完全に独立できなくても、躊躇なく抽出します。「コードを読みやすくする」という効果については変わらないからです。

抽出したものをどこに置くかは、大切なことではないので、あとで決めてもかまいません。大切なのは、「無関係の下位問題」 を抽出するということです。

2014-10-23

[]無関係の下位問題の抽出:抽出先:プロジェクトから独立

どういうこと?

「無関係の下位の問題」を抽出して、プロジェクトから独立させ、複数のプロジェクトで再利用します。

どうして?

プロジェクトから独立した「無関係の下位の問題」の解決は、便利な「外部ライブラリ」となります。

「SQLデータベース」「JavaScript のライブラリ」「HTML のテンプレートシステム」などは、いずれも内部のことを考えずに使えます。つまり、プロジェクトから完全に切り離されています。結果として、自分のプロジェクトのコードは小さく保たれます。

どうすれば?

自分のプロジェクトの「無関係の下位の問題」を、できるだけ独立したライブラリに分離します。そうすれば、残りのコードは小さくて考えやすいものになります。

2014-10-22

[]無関係の下位問題の抽出:対象:煩雑なインターフェイス

どういうこと?

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

既存の「無関係の下位問題」のインターフェイスが汚い場合は、自分で「ラップ」関数を作ります。

どうして?

提供されているインターフェイスを使用する場合、それがあまりうまくないと、プログラムが「無関係の下位問題」にまみれてしまうことがあります。

例えば、ブラウザのクッキーを扱うインターフェイスは、非常に残念なインターフェイスです。クッキーは名前と値のペアになっているはずなのに、ブラウザが提供するインタフェースには、以下のような構文の「document.cookie」という文字列しかありません。

name1=value1; name2=value2; ...

クッキーを探すには、この巨大な文字列を自分でパースしなければならないのです。以下は、max_results という名前のクッキーを読み込むコードです。

var max_results;
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
	var c = cookies[i];
	c = c.replace(/^[ ]+/, ''); // 先頭の空白を削除
	if (c.indexOf("max_results=") === 0)
		max_results = Number(c.substring(12, c.length));
}

とても汚いコードになってしまいます。

同様に、クッキーの値の作成や変更もおかしなことになります。「document.cookie」に、正しい構文で値を設定する必要があるのです。以下の文は、すべてのクッキーを書き換えるように見えますが、(不思議なことに)書き換えずに「追記する」のです。

document.cookie = "max_results=50; expires=Wed, 1 Jan 2020 20:53:47 UTC; path=/";

さらにさらに、クッキーの削除も直感的ではありません。「有効期限を過去にして設定する」ことが削除を意味するのです。

そのまま無理やり使用すると、コードがどんどん「無関係の下位問題」に浸食されていきます。

どうすれば?

「ラップ」関数を作り、インターフェイスを簡潔にします。

理想とは程遠いインタフェースに妥協することはありません。自分でラッパー関数を用意して、手も足も出ない汚いインタフェースを覆い隠すようにします。

上述cookieの例であれば、以下のようなインターフェイスの関数を作成すると、コードが簡潔になります。

// 取得
get_cookie(name);
// 設定
set_cookie(name, value, days_to_expire);
// 削除
delete_cookie(name);

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"
		}
	}
}