1年目から身につけたい! チーム開発 6つの心得

第3章 細かな粒度で実装しよう―単純なパーツを組み合わせた見通しの良い設計

この記事を読むのに必要な時間:およそ 7 分

コードの見た目がそろったら,次は設計だね


せ,設計……! 僕,そういうのってちゃんと勉強したことないんですよね……デザインパターンとか

難しく考える必要はないさ。たしかにデザインパターンみたいな有名な設計技法はいろいろあるけど,もっと基本的な方針から身につけることが大事だよ。たとえば,実装の粒度とか

粒度?


「クラスが大きすぎるから分けなさい」とか「パラメータが多すぎるから減らしなさい」とか指摘されたことはないかい? それのことだよ

大きな実装は適切な粒度に分割しよう

放っておくと実装は肥大化する

プログラムを開発していると,関数やクラスなどの実装がどんどん肥大化していくことがあります。

リスト1は,⁠メールクライアントのMozilla Thunderbirdの起動時に,UIUser Interface注1を非表示にしたあと,すべての受信サーバへの認証を試行して,認証に成功した場合だけUIを再表示する(1つでも認証に失敗したサーバがあればThunderbirdを終了する)⁠というアドオンtb-force-auth-at-startupの実装例です。ここでは実装の詳細は気にせずに,それぞれの個所が何をしているのかを示したコメントだけを追ってください。

リスト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の実装は,1モジュール・1メソッドだけで完結しています。単純に行数や文字数だけを見て,これを「コンパクトな実装」と思う人もいるかもしれません。

しかし,⁠UIの操作」⁠受信サーバの認証処理」といった複数の領域に渡る処理がほぼそのまま1ヵ所に詰め込まれていて,機能追加や修正のための変更が非常にしにくい状態になっており,実際は,肥大化した複雑な実装と言えます。複雑な実装ではプログラム全体が密に絡み合っていて,要件が少し変わっただけで全体を丸ごと作りなおさないといけないこともあります。また,バグの温床になりやすく,障害が発生したときの原因究明も難しいです。複雑な実装は,中・長期的に見るとメリットよりもデメリットのほうが大きいのです。

思いつくまま作業を進めた場合や,手探りで実装していった場合には,このようなことになってしまいがちです。細部にわたって事前に完璧な設計を行っておくのはまず不可能ですから,このような実装が突発的に生まれてしまうことは珍しくありません。

このような肥大化した実装をより良い設計に改める方法としては,問題の切り分けと,クラスやモジュール,メソッドといった各レベルでの実装の切り分けが有効です。段階を踏んで見ていきましょう。

注1)
ここでは,ツールバーやボタン,メール一覧など,ユーザの目に見える部分を指します。

解決しようとしている問題を切り分けよう

プログラムの中での実装の単位の大きさは,粒度という言葉で表されます。リスト1のような複雑な実装は「粒度が大きい」と言われ,1メソッドが数行程度であるような単純な実装は「粒度が小さい」と言われます注2)⁠プログラム全体としては大規模でも,個々のモジュールなどの粒度が適切な実装は,機能追加や変更がしやすく,障害があったときにも修正しやすいです。

しかし,単純にコードを一定の行数でバラバラに分けたりまとめたりすればよいというものでもありません。実装の大きさは,解決しようとしている問題の大きさと強く結びついています。実装を適切な粒度にするためには,解決したい問題をよく分析して,小さな問題の集合としてとらえなおす必要があります。このような作業を問題の切り分けと言います。

初級者と中・上級者の最大の違いは,ここにあります。初級者は目の前にある問題をその形のまま解決しようとしがちですが,1人の人間が解決できる問題の大きさには限りがあります。そのため個人の許容量を超える大きさの問題が降ってくるとお手上げになってしまいます。しかし問題の切り分けができる中・上級者は,そのような大きく複雑な問題を咀嚼(そしゃく)して,自分の理解できる単位にまで小さく切り分けます。そうすることで,複雑な問題でも解決の道筋を見つけることができるのです図1)⁠

図1 一見すると複雑な問題も,切り分けると単純な問題になる

図1 一見すると複雑な問題も,切り分けると単純な問題になる

問題を切り分ける基準としてわかりやすいのは,特に目立つキーワードです。たとえば先の例で挙げたThunderbirdアドオンが解決しようとしている問題であれば,⁠起動時」⁠UI」⁠認証」などがキーワードでしょう。これらを軸にすると次のように問題を切り分けられます。

  1. Thunderbirdの起動時に全体の処理を開始する
  2. UIの状態を変える(表示・非表示の切り替え,終了など)
  3. 受信サーバで認証する(認証を試行して,成功したか失敗したか判断する)
注2)
絶対的な指標ではなく,相対的な大小を言い表す言葉です。

著者プロフィール

結城洋志(ゆうきひろし)

株式会社クリアコード所属。Firefox黎明期からアドオン開発を手がけ,業務上もMozilla製品の技術サポートを担当。代表作は「ツリー型タブ」「テキストリンクなど。また,日経Linux誌にて「シス管系女子」を連載中。

コメント

コメントの記入