コードの見た目がそろったら、
せ、
難しく考える必要はないさ。たしかにデザインパターンみたいな有名な設計技法はいろいろあるけど、
粒度?
大きな実装は適切な粒度に分割しよう
放っておくと実装は肥大化する
プログラムを開発していると、
リスト1は、
var ForceAuthAtStartup = {
// Thunderbird が起動したあとにこのメソッドが実行されると仮定する
onMailStartupDone: function() {
// ① UI を非表示にする
document.documentElement.style.visibility = "hidden";
// ②認証が必要な受信サーバを収集する
var allServers = MailServices.accounts.allServers;
var servers = [];
for (let i = 0, maxi = allServers.length, server; i < maxi; ++i) {
let server = allServers.queryElementAt(i, Ci.nsIMsgIncomingServer);
if (server.type != "none")
servers.push(server);
}
// ③各受信サーバに認証を試行する
var successCount = 0;
var failureCount = 0;
servers.forEach(function(server) {
server.verifyLogon({
OnStartRunningUrl: function() {},
OnStopRunningUrl: function(url, exitCode) {
// ④個々の受信サーバで認証に成功したかどうかを判別する
if (Components.isSuccessCode(exitCode))
successCount++;
else
failureCount++;
// ⑤すべてのサーバの処理が終わったら、全体の成否を判定する
if (successCount + failureCount == servers.length) {
// ⑥全体の成否の判定結果に応じて処理を行う
// ⑦すべて成功ならUI を再表示する
if (successCount == servers.length)
document.documentElement.style.visibility = "";
else // ⑧そうでないならThunderbird を終了する
Cc["@mozilla.org/toolkit/app-startup;1"]
.getService(Ci.nsIAppStartup)
.quit(Ci.nsIAppStartup.eAttemptQuit);
}
}
}, MailServices.mailSession.topmostMsgWindow);
}
}
};
リスト1の実装は、
しかし、
思いつくまま作業を進めた場合や、
このような肥大化した実装をより良い設計に改める方法としては、
解決しようとしている問題を切り分けよう
プログラムの中での実装の単位の大きさは、
しかし、
初級者と中・

問題を切り分ける基準としてわかりやすいのは、
- Thunderbirdの起動時に全体の処理を開始する
- UIの状態を変える
(表示・ 非表示の切り替え、 終了など) - 受信サーバで認証する
(認証を試行して、 成功したか失敗したか判断する)
クラスやモジュールを分けよう
問題を適切に切り分けられれば、
// 起動時の処理
var StartupHandler = {
onMailStartupDone: function() {
UIController.deactivate();
Authenticator.tryAuth(function(succeeded) {
// ⑥全体の成否の判定結果に応じて処理を行う
if (succeeded)
UIController.activate();
else
UIController.exit();
});
}
};
// UI の状態を変更する処理
var UIController = {
deactivate: function() {
// ① UI を非表示にする
},
activate: function() {
// ⑦ UI を再表示する
},
exit: function() {
// ⑧ Thunderbird を終了する
}
};
// 認証に関係する処理
var Authenticator = {
tryAuth: function(aCallback) {
// ②認証が必要な受信サーバを収集する
// ③各受信サーバに認証を試行する
...
// ④個々の受信サーバで認証に成功したかどうかを判別する
...
// ⑤すべてのサーバの処理が終わったら、全体の成否を判定する
if (successCount + failureCount == servers.length)
aCallback(successCount == servers.length);
...
}
};
モジュール数は増えましたが、UIController
モジュールの実装だけを見ればよいですし、Authenticator
モジュールの実装だけを見れば済みます。このように、
実装の切り分け基準を問題の切り分け基準と合わせると、
なお、Authenticator
モジュールに切り分けるにあたって、
メソッドを分けよう
クラスやモジュールだけでなく、Authenticator
モジュールのtryAuth
メソッドに突出して多くの処理が集中していますので、
このメソッドは
- 認証対象のサーバを収集する
- 各サーバで認証する
- すべてのサーバの認証結果が集まった段階でコールバック関数を実行する
もとのtryAuth
メソッドを、server
のverifyLogon
メソッドは第1引数として受け取ったリスナオブジェクトOnStopRunningUrl
メソッドに認証の結果を渡していますが、Authenticator
モジュール自身をリスナとして使うようにしており、OnStopRunningUrl
メソッドが④の役割を果たすtryAuth
メソッドに渡されたコールバック関数や成功/Authenticator
モジュールのプロパティ
var Authenticator = {
collectAuthServers: function() {
// ②認証が必要な受信サーバを収集する
var allServers = MailServices.accounts.allServers;
var servers = [];
...
return servers;
},
tryAuth: function(aCallback) {
this.callback = aCallback;
// ③各受信サーバに認証を試行する
this.successCount = 0;
this.failureCount = 0;
this.servers = this.collectAuthServers();
this.servers.forEach(function(server) {
server.verifyLogon(this, MailServices.mailSession.topmostMsgWindow);
});
},
OnStartRunningUrl: function() {},
OnStopRunningUrl: function(url, exitCode) {
// ④個々の受信サーバで認証に成功したかどうかを判別する
if (Components.isSuccessCode(exitCode))
this.successCount++;
else
this.failureCount++;
// ⑤すべてのサーバの処理が終わったら、全体の成否を判定する
if (this.successCount + this.failureCount == this.servers.length)
this.callback(this.successCount == this.servers.length);
}
};
パラメータの数を減らそう
関数やメソッドの引数、
引数の順番は間違えやすい
たとえば、
//「文字列」「変換元」「変換先」という順になっている
function convertEncoding(sourceString, fromEncoding, toEncoding) {
...
}
では、UTF-8
からShift_
へ変換する必要がある処理を実装することになりました。そういえばそんな処理を前に実装したんだった、var name = convertEncoding(...
と書き始めたところで、
関数やメソッドを定義したときには覚えていても、
- 「文字列の」
変換なんだから、 文字列が最初に来るはず - 文字列の
「エンコーディング」 の変換なんだから、 エンコーディングが最初に来るはず - 変換元、
変換先、 という順番で並ぶのが自然だ - 得られる結果の文字エンコーディングが重要なんだから、
変換先エンコーディングが最初に来るはず
どれもそれなりに妥当そうです。実際に、convertEncoding
に似たAPIを採用していますが、
var encoding = require("encoding");
//「文字列」「変換先」「変換元」という順になっている
var sjisBuffer = encoding.convert(utf8Buffer, "Shift_JIS", "UTF-8");
また、 このように、 実装の粒度が小さくなると、 JavaScriptの文字列は内部的にはUTF-16でエンコードされているものとして扱われます。このことを前提として、 実際のところ、 また、 ここまで、 実際に、 このように、var Iconv = require("iconv").Iconv;
var converter = new Iconv("UTF-8", "Shift_JIS");
var sjisBuffer = converter.convert(utf8Buffer);
引数の数を減らそう
var iconvLite = requrie("iconv-lite");
var unicodeString = iconvLite.decode(utf8Buffer, "UTF-8");
var sjisBuffer = iconvLite.encode(unicodeString, "Shift_JIS");
var decoder = new TextDecoder("UTF-8"),
var unicodeString = decoder.decode(utf8BytesArray);
var encoder = new TextEncoder("Shift_JIS");
var sjisBytesArray = encoder.encode(unicodeString);
クラスの機能にしよう
utf8_string = " 日本語の文字列"
sjis_string = utf8_string.encode("Shift_JIS")
eucjp_string = sjis_string.encode("EUC-JP")
1つのプログラムを1人だけで専任で開発していると、
チームのみんなで開発しようと思っても、
ああ。そうならないように、