リファクタリング(クライアント編)- AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第11回】マニュアル

  • 6
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

:warning: 2015/12/2 大幅に追記、修正を行いました。
公開時(ワークショップ当日)はあっさり内容でしたが、ワークショップにて口頭で説明した内容やその他補足説明を追記しています。

:large_blue_circle: はじめに

本投稿は、2015/11/27に行われた、リファクタリング(クライアント編) - connpassの内容についてまとめた資料です。

:warning:今後の予定は以下に掲載されますのでよろしくお願いします!
AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass

今回も前回に続き、「リファクタリング」について学びます。今回はクライアント編です。

以下、前回と同じですが、おさらいしておきましょう。。

:large_blue_circle: リファクタリングとは?

プログラムの動作を変えずに内部構造を改善すること。ようするに、コードをキレイにするということ。

:large_blue_circle: リファクタリング手順

  1. リファクタリングすべきか判断する。
  2. テストがあるか?なければ書く。
  3. リファクタリングする
  4. テストする
  5. 1に戻る

:one: リファクタリングすべきか判断する - コードの匂い

コードの「不吉な匂い」。リファクタリングすべきコードは、匂いを発します。
以下の様なコードは、匂います!
引用:不吉な匂い

  • 重複したコード
  • 長すぎるメソッド
  • 巨大なクラス
  • 多すぎる引数
  • 変更の発散
  • 変更の分散
  • 属性、操作の横恋慕
  • データの群れ
  • 基本データ型への執着
  • スイッチ文
  • パラレル継承
  • 怠け者クラス
  • 疑わしき一般化
  • 一時的属性
  • メッセージの連鎖
  • 仲介人
  • 不適切な関係
  • クラスのインタフェース不一致
  • 未熟なクラスライブラリ
  • データクラス
  • 相続拒否
  • コメント

:point_up: 分類してみました

ムリヤリ感と基準がよくわからない感がありますが、ちょっとだけとっつきやすくなるよう、分類してみました:smirk:

  • 激臭系
  • 蓋を開けたら臭う系
  • メンドクサイ系
  • 加齢臭系
  • 生臭い系
  • 汗臭い系
  • ウソ臭い系

以下、解説します。
各臭い毎に、「消臭方法」や「〜原則に違反」などを書いてますが、ここで説明するには紙面が足りません:disappointed:
興味あればググってみてください。

激臭系

問答無用で臭い。
読まずとも「眺める」だけで匂うコード。

  • 2. 長すぎるメソッド
    • 分割しよう。
  • 3. 巨大なクラス
    • 分割しよう。
  • 4. 多すぎる引数
    • 分類してオブジェクトにしよう。
  • 10. スイッチ文
    • オブジェクト指向しましょう。

蓋を開けたら臭う系

少し読んでみたらすぐ臭さに気づくレベルの臭い。

  • 1. 重複したコード
    • DRY原則違反。 コピペやめて共通化しましょう。
  • 12. 怠け者クラス
    • やってることが少なくて存在意義の薄いクラス。他のクラスに役割を吸収させましょう。
  • 16. 仲介人
    • 自分では何もしていないクラス。削除もしくはもっと仕事させましょう。
  • 18. クラスのインタフェース不一致
    • 別の担当者が同じ物作った?意識合わせしましょう。
  • 20. データクラス
    • いわゆる「ドメインモデル貧血症」。データと振る舞いをもたせましょう。

メンドクサイ系

くさいの意味がちょっと違うのでカタカナ(笑)
修正しようとするとメンドクサイやつです。

  • 5. 変更の発散
    • SRP(単一責任原則)違反。いろいろやり過ぎている。仕事が多すぎるので分担しましょう。
  • 6. 変更の分散
    • 一つのことをいろんなクラスに分担させすぎている。責任もう少し集中させましょう。
  • 11. パラレル継承
    • 「変更の分散」の特殊ケース。OCP(開放/閉鎖原則)違反。親クラスとサブクラスで責任を適切に分担しましょう。

加齢臭系

若かった頃(システムが小規模だった頃 or 開発の途中までは)はたぶん臭くなかったはず
メンドクサイ系の親戚筋

  • 7. 属性、操作の横恋慕
    • クラスの役割分担が不明確になっている。役割分担を適切にしましょう
  • 15. メッセージの連鎖
    • あるオブジェクトから取得したこのオブジェクトの知っているそのオブジェクトの...。全体を整理してみましょう。
  • 17. 不適切な関係
    • クラス間が密結合してしまっている。自分のことは自分でしよう。
  • 21. 相続拒否
    • 継承したはいいがオーバライドして変えまくって、結果別モノに...。LSP(リスコフの置換原則違反)。継承をやめるか、親の機能を子に移動するとか検討しましょう。

生臭い系

ほっとくと臭くなる類のものを放置系。
データモデリング不足、カプセル化の検討不足。もうちょっと手間かけて調理しましょう。

  • 8. データの群れ
    • いつも同じセットで使用するデータ。オブジェクトに閉じましょう。
  • 9. 基本データ型への執着
    • ValueObjectを作ってみましょう
  • 14. 一時的属性
    • 変数(フィールド)はあるが使われたり使われなかったり。外部化またはNullObject化しましょう

汗臭い系

頑張った結果なのだが、汗臭くなっただけだった...。

  • 13. 疑わしき一般化
    • 将来を見越しすぎて必要以上に「イイ感じにしよう」とがんばったが無駄だった。YAGNI原則(それは必要にならない)違反。「本当に必要になってから」実装することにして、それまでは「シンプルさ」を追求しましょう。

ウソ臭い系

ウソを付くつもりはなかったのに、結果的には嘘つき。

  • 19. 未熟なクラスライブラリ
    • そのままでは使えないライブラリ。使えるように修正するか、ライブラリで全部やるのは諦めましょう。
  • 22. コメント
    • コメントが古い、もしくはウソ。極力「コメントなし」でも読めるようにしましょう。コードでは表せないこと(なぜこういうロジックにしたか、など)だけをコメントにすることを目標に、コメントに頼らずまずはコードを改善しましょう。

:two: テストがあるか?なければ書く - PhpUnit

リファクタリングは、「“プログラムの動作を変えずに”内部構造を改善すること』です。
プログラムの動作が変わっていないことを確認するには、テストするしかありません。
テストしましょう。

テストを書くと気づきますが、
「テストしやすいコードを書く」ことは非常に重要です。

:three: リファクタリングする

「コードの匂い」毎にテクニックはありますが、全部は説明しきれないので、今回のワークショップでは簡単なものを取り上げます。

前回作成したアップロードの関数が「長すぎるメソッド」の匂いがしますので、「メソッドの抽出」をおこなってリファクタリングしてみます。

:four: テストする

テストして結果を確認しましょう。パスすればリファクタリング完了です。
うまくテストコードを書くためには、
何をテストして何をテストしないか*をきちんと分けることが重要です。

テストしない部分は「モック」として実装し、テストを効率的に進めていきます。

:five: 1に戻る

リファクタリングは常に実施する必要があります。
技術的負債は計画的、継続的に減らしていきしましょう。

:large_blue_circle: ワークショップメニュー

  • 事前準備
    • ブランチ整備
  • Lesson1 TODO一覧表示の削除リンク、詳細リンク、完了チェックボックスの表示テスト
  • Lesson2 TODO詳細の表示データ取得テスト
  • Lesson3 TODO詳細の表示データ取得処理のちょっとリファクタリング

という感じですすめます。

:large_blue_circle: 事前準備

事前準備は毎回同じなので、別エントリにまとめています。
全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。
やることは、

  • gitのブランチを整えて今回用ブランチvol/10を作成する

です。まずこれをやりましょう。

:warning: それと、第5回と第6回に不参加の方は、テーブルの修正が必要です。

  • 第5回
    • ユーザ登録用のテーブル作成(ログイン機能実装のため)
  • 第6回
    • TODO一覧テーブルへの列追加(owner列とassignee列。担当者アサイン機能実装のため)

をやってますので、それぞれ下記リンク先を参照して実施して下さい。
ユーザ登録用のテーブル作成
TODO一覧テーブルへの列追加

準備ができたら、Lesson1です!

:large_blue_circle: Lesson1 TODO一覧表示の削除リンク、詳細リンク、完了チェックボックスの表示テスト

クライアント側でちょっとだけややこしい条件判断のあるロジックがあるので、そこをテストしてみます。下記画像のチェックボックスとリンクボタンを権限によって表示内容を調整している部分です。

todo.png

このような制御をしています。

  担当 担当でない
オーナ 削除=表示,詳細=表示,Check=可 削除=表示,詳細=表示,Check=可
オーナでない 削除=非表示,詳細=表示,Check=可 削除=非表示,詳細=非表示,Check=不可

テストツールたち

mocha

テストフレームワークです。テストの実行を行います。
アサーションツールは別途必要なので、次のchaiを使用します。

chai

テスト結果を確認するための、アサーションを実行するためのツールです。
BDD形式、TDD形式の記法を選ぶことが出来ます。今回はBDD方式でテストを書きます。

sinon

テストのためのモックやスタブを生成するツールです。

インストール

下記コマンドでOK!

インストールコマンド
#lib
cd /var/www/study/rest-study/app/webroot/js/lib
wget https://raw.githubusercontent.com/mochajs/mocha/master/mocha.js
wget https://github.com/chaijs/chai/raw/master/chai.js
wget http://sinonjs.org/releases/sinon-1.17.2.js
#css
cd /var/www/study/rest-study/app/webroot/css
wget https://github.com/mochajs/mocha/raw/master/mocha.css

これで準備OK!

編集するファイル一覧

  • app/webroot/clientTest.html
    • テスト実行用、実行結果表示用画面
  • app/webroot/js/test/test-require-config.js
    • テスト用require設定
  • app/webroot/js/test/setup.js
    • テスト読み込み、ツールの設定
  • app/webroot/js/test/tests.js
    • テスト実行用
  • app/webroot/js/test/test-todo-item-view.js
    • テストコード
  • app/webroot/js/test/test-login.js
    • テスト用にログインするためのユーティリティ

app/webroot/clientTest.html

<html>
  <head>
    <meta charset="utf-8"/>
    <title>TodoList Tests</title>
    <link rel="stylesheet" href="css/mocha.css"/>
  </head>
  <body>
    <div id="mocha"></div>
    <script type="text/javascript" src="js/require-config.js"></script>
    <script type="text/javascript" src="js/test/test-require-config.js"></script>
    <script type="text/javascript" src="js/lib/require.js" data-main="test/setup.js"></script>
  </body>
</html>
  • require-config.js
    • todoアプリケーションの中でも使用しているrequire.jsの設定ファイルです。これでjsの依存関係を解決します。
  • test-require-config.js
    • これもrequire.jsの設定です。テスト用の設定を追加しています。
  • data-main="test/setup.js"
    • 最初に動くjsを指定しています。

:warning: この辺りは、todoアプリのdefault.ctpとほぼ同じですね。

app/webroot/js/test/test-require-config.js

require.paths.mocha = 'lib/mocha';
require.paths.chai = 'lib/chai';
require.paths.sinon = 'lib/sinon-1.17.2';
require.shim.mocha = {
    init: function () {
        this.mocha.setup('bdd');
        return this.mocha;
    }
};
  • require.paths.mocha = 'lib/mocha';
    • mochaのパスを設定しています。app/webroot/js/require-config.jsで既にrequire.pathsの設定が行われているため、そこに追加するためにこのような書き方をしています。
    • chai, sinonに関しても同じくpathsを設定しています。

app/webroot/js/test/setup.js

require([
    'mocha',
    'chai',
    'marionette',
], function (mocha, chai) {
    require([
        'js/test/tests.js'
    ], function () {
        chai.should();
        mocha.run();
    });
});

mocha, chaiを読み込み(require)し、さらにネストしたrequireでjs/test/tests.js(テストコード)を読み込み、chaiの初期設定を行ったあとmocha.run()でテストコードを実行しています。

app/webroot/js/test/tests.js

app/webroot/js/test/tests.js
define(function (require) {
    //読み込み
    var testTodoItemView = require('js/test/test-todo-item-view.js');
    //実行
    testTodoItemView();
});

テストを実行します。実際のテストはjs/test/test-todo-item-view.jsに記載されているので、それをrequireして実行する形となっています。
テスト用モジュールが増えたら、同様にテストコードを書いてこのファイルに実行用のrequreとテスト関数の実行行を追加します。

app/webroot/js/test/test-todo-item-view.js

app/webroot/js/test/test-todo-item-view.js
define(function(require){
    var TodoItemView = require('views/todo-item-view');
    var TodoModel = require('models/todo-model');
    var loginTest = require('test/test-login');
    return function(){
        describe("TODO一覧表示の削除リンク、詳細リンク、完了チェックボックスの表示テスト", function () {
            describe("ログインチェック", function () {
                it('ログインチェック', function (done) {
                    loginTest(done);
                });
            });

            describe("オーナかつ担当者の場合", function () {
                it("削除リンク O / 詳細リンク O / チェック O", function () {
                    itemView = _createAndRenderTodoItemView(true, true);
                    //削除リンクが表示されている
                    itemView.ui.removeLink.css('display').should.be.equals('');
                    //詳細リンク表示されている
                    itemView.ui.detailLink.css('display').should.be.equals('');
                    //完了チェックボックスが押せる
                    itemView.ui.checkBox.prop('disabled').should.be.equals(false);
                });
            });

            describe("オーナだが担当者ではない場合", function () {
                it("削除リンク O / 詳細リンク O / チェック O", function () {
                    var itemView = _createAndRenderTodoItemView(true, false);
                    //削除リンクが表示されている
                    itemView.ui.removeLink.css('display').should.be.equals('');
                    //詳細リンク表示されている
                    itemView.ui.detailLink.css('display').should.be.equals('');
                    //完了チェックボックスが押せる
                    itemView.ui.checkBox.prop('disabled').should.be.equals(false);
                });
            });

            describe("オーナではないが担当者の場合", function () {
                it("削除リンク X / 詳細リンク O / チェック O", function () {
                    var itemView = _createAndRenderTodoItemView(false, true);
                    //削除リンクが非表示(display=none)になっている
                    itemView.ui.removeLink.css('display').should.be.equals('none');
                    //詳細リンク表示されている
                    itemView.ui.detailLink.css('display').should.be.equals('');
                    //完了チェックボックスが押せる
                    itemView.ui.checkBox.prop('disabled').should.be.equals(false);
                });
            });
            describe("オーナでもなく担当者でもない場合", function () {
                it("削除リンク X / 詳細リンク X / チェック X", function () {
                    var itemView = _createAndRenderTodoItemView(false, false);
                    //削除リンクが非表示(display=none)になっている
                    itemView.ui.removeLink.css('display').should.be.equals('none');
                    //詳細リンクが非表示(display=none)になっている
                    itemView.ui.detailLink.css('display').should.be.equals('none');
                    //完了チェックボックスが押せない(disabled=true)
                    itemView.ui.checkBox.prop('disabled').should.be.equals(true);
                });
            });
            //utility
            function _createAndRenderTodoItemView(owned, assigned){
                //サーバから受信するデータ
                data = {
                    TodoList: {
                        id: "1",
                        todo: "do somothing",
                        status: "0",
                        owned: owned,
                        assigned: assigned
                    },
                    Owner: {
                        id: "1",
                        name: "anonymous"
                    },
                    Assignee: {
                        id: "1",
                        name: "anonymous"
                    }
                };
                //サーバからのデータをparseしてmodel生成
                var model = new TodoModel(data, {parse: true});
                //テンプレート。テスト対象のみ
                var template =
                    '<div>' +
                    '  <a class="remove-link">削除</a>' +
                    '  <a class="detail-link">詳細</a>' +
                    '  <input type="checkbox" class="toggle"></input>' +
                    '</div>';
                //modelとtemplateを渡してviewを生成
                var view = new TodoItemView({
                    model: model,
                    //template : $(template)
                });
                //表示までしてからreturn
                view.template = $(template);
                view.render();

                return view;
            }
        });
    }
});

これがメインのテストコードです。やっていることは、

  • ログインする
  • views/todo-item-viewを読み込み、画面表示のコードを実行する
  • describeで始まる4つのコードブロック(それぞれ下記条件のテストケース)を実行します。
    • オーナかつ担当者の場合
    • オーナだが担当者ではない場合
    • オーナではないが担当者の場合
    • オーナでもなく担当者でもない場合

各テストコードの詳細

  • describeから始まる一連の構文がテストコードです。
    • describeにテスト対象の説明
    • itに、テスト対象コードを実行するコード、その結果を確認するコードを書きます。ここがテストコード本体です。
      • it (テスト対象) should be 〜と言った感じのDSL風の構文で記述できます。
      • ここでは、TODO表示用のModelの、ownedassignedの値を変えてviewを表示し、削除ボタン、詳細ボタン、チェックボックスが意図した通りに描画されるかテストしています。
    • viewの生成は_createAndRenderTodoItemView関数で行います。
      • 引数でownedassignedに設定する値が指定できるようになっています。
      • view生成時にはtemplateはダミーのものを設定し、そのダミーtemplate内の削除ボタン、詳細ボタン、チェックボックスが設定されるようになっています。

app/webroot/js/test/test-login.js

```js:app/webroot/js/test/test-login.js
define(function (require) {
var UserModel = require('models/user-model');

var userModel = new UserModel();
return function login(done){
    if (!userModel.isLoggedIn()) {
        userModel.login(
            'yamada',   //DB登録データに合わせる
            'test',     //DB登録データに合わせる
            function success(message) {
                console.info("logged in!")
                done();
            },
            function error(message) {
                done('login error! ' + '\n' + message);
            }
        )
    } else {
        //ログイン済みなら終了
        console.info('ログイン済み');
        done();
    }
}

});
```

  • 実際にログイン用APIを実行してサーバにログインします。
    • ユーザ名、パスワードを指定していますので、自分の環境に合わせて設定して下さい。
    • ログイン後、done()関数を実行し、テストの終了を宣言します。

:warning: done関数について

it関数内でdoneを実行することにより、テストの実行を終了することが出来ます。 
it関数自体はuserModel.loginを実行してそのまま抜けてしまいますが、done関数はuserModel.loginに指定したcallback関数のsuccess関数実行後に実行されます。
テスト自体の終了はdone関数実行まで保留されますので、この仕組みにより非同期callback関数を呼び出すまでテストは継続することが出来ます。

先ほどのapp/webroot/js/test/test-todo-item-view.js内の処理で、

app/webroot/js/test/test-todo-item-view.js

〜省略〜
        describe("TODO一覧表示の削除リンク、詳細リンク、完了チェックボックスの表示テスト", function () {
            describe("ログインチェック", function () {
                it('ログインチェック', function (done) {
                    loginTest(done);
                });
            });
〜省略〜

としてlogintTest関数を実行しています。この時のit関数の引数doneをそのままloginTest関数に渡し、内部でdone関数を実行する流れになっています。

テスト実行

http://(PublicIp)/rest-study/clientTest.html

で、実行できます!こんな感じです。
:warning: 次のレッスンの結果も反映されています。

test.png

うまくいったら、Gitにコミットしておきましょう。

:warning: GitHubでのdiff表示へのリンク

第11回 Lesson1 TODO一覧表示の削除リンク、詳細リンク、完了チェックボックスの表示テスト · suzukishouten-study/rest-study@728ff09
Lesson2へ!

:large_blue_circle: Lesson2 TODO詳細の表示データ取得テスト

javascriptで嫌な匂いを発するコードといえば、「コールバック地獄」がよく話題に上がります。コールバックを使用する部分をリファクタリングするため、まずはテストを書くのがLesson2です。
今回やっていることは、

  • ListenToによるイベント駆動 -> successコールバック関数による駆動に変更
  • ユーザ一覧の取得とTODO一覧の取得が両方完了した際に画面表示処理を動かすようにする

の2つです。
TODO詳細画面の表示ロジックは現在下記の用になっています。

app/webroot/js/views/todo-detail-layout-view.js
//詳細画面用レイアウトビュー
define(function(require) {
    var TodoDetailItemView = require('views/todo-detail-item-view');
    var TodoModel = require('models/todo-model');
    var UserCollection = require('collections/user-collection');

    var TodoDetailLayoutView = Marionette.LayoutView.extend({
        //テンプレート
        template : '#todo-detail-layout-template',

        regions : {
            itemRegion : '#todo-item',
        },

        onRender : function() {
            this.userCollection = new UserCollection();
            this.listenToOnce(this.userCollection, 'reset', this.onLoadUsers, this);
            this.userCollection.fetch({
                reset : true
            });
        },

        onLoadUsers : function(userCollection){
            var todoModel = new TodoModel({
                id : this.options.modelId
            });
            //モデルのサーバからのデータ取得完了時、描画を行う
            this.listenToOnce(todoModel, 'sync', this.showItem, this);
            //サーバからデータ取得
            todoModel.fetch({
                wait : true
            });
        },

        showItem : function(todoModel) {
            this.itemRegion.show(new TodoDetailItemView({
                model : todoModel,
                userList : this.userCollection.models
            }));
        },

    });
    return TodoDetailLayoutView;
});
  1. onRender関数でユーザ一覧を取得、サーバからの正常レスポンス受信後、onLoadUsers関数を実行。
  2. onLoadUsers関数では、TOTO一覧を取得し、サーバからの正常レスポンス受信後、showItem関数を実行。
  3. shoItem関数で、実際に表示を行うItemViewを生成して表示。

となっているのですが、1.,2.で行われているイベントのハンドリング部分をLesson3でリファクタリングします。
その前段階のLesson2では、「最終的に3のshowItem関数が実行される」ということが保証されるよう、テストで担保しておきます。

編集するファイル一覧

  • app/webroot/js/test/test-todo-detail-layout-view.js
    • テストコード
  • app/webroot/js/test/tests.js
    • テスト実行用

app/webroot/js/test/test-todo-detail-layout-view.js

app/webroot/js/test/test-todo-detail-layout-view.js
define(function(require){
    var sinon = require('sinon');
    var TodoDetailLayoutView = require('views/todo-detail-layout-view');
    var TodoModel = require('models/todo-model');
    var loginTest = require('test/test-login');
    return function () {
        describe("TODO詳細の表示データ取得テスト", function () {
            it('ログインチェック', function (done) {
                loginTest(done);
            });

            it("Todoとユーザ一覧取得", function (done) {
                var layoutView = _createTodoDetailLayoutView();
                sinon.stub(layoutView, 'showItem', function (todoModel) {
                    todoModel.should.be.ok;
                    layoutView.showItem.restore();
                    done();
                });
                layoutView.render();
            });

            //utility
            function _createTodoDetailLayoutView(data) {
                //テンプレート
                var template = '<div><div></div></div>';
                //modelとtemplateを渡してviewを生成
                var view = new TodoDetailLayoutView({
                    template: $(template),
                    modelId: 1
                });
                return view;
            }
        });
    }
});

sinon.stubを使用し、showItem関数のスタブを作っています。
スタブでは、単にスタブが実行されたことだけをチェックすればいいので、引数がnullで無いことだけチェックしてOKとしています。

app/webroot/js/test/tests.js

app/webroot/js/test/tests.js
 define(function (require) {
    //読み込み
    var testTodoItemView = require('js/test/test-todo-item-view.js');
+   var testTodoDetailLayoutView = require('js/test/test-todo-detail-layout-view.js');
    //実行
    testTodoItemView();
+   testTodoDetailLayoutView();
 });

作成したtest-todo-detail-layout-view.jsテストを追加して実行されるようにしています。

テストが追加されましたので、実行してみましょう。

うまくいったら、Gitにコミットしておきましょう。

:warning: GitHubでのdiff表示へのリンク

第11回 Lesson2 TODO詳細の表示データ取得テスト · suzukishouten-study/rest-study@5c7b38a
Lesson3へ!

:large_blue_circle: Lesson3 TODO詳細の表示データ取得処理のちょっとリファクタリング

テストコードが動いたので、Lesson3ではリファクタリングします。
修正後、再度テストを実行してみましょう。

編集するファイル一覧

  • app/webroot/js/views/todo-detail-layout-view.js
    • リファクタリング対象コード

app/webroot/js/views/todo-detail-layout-view.js

//詳細画面用レイアウトビュー
define(function(require) {
    var TodoDetailItemView = require('views/todo-detail-item-view');
    var TodoModel = require('models/todo-model');
    var UserCollection = require('collections/user-collection');

    var TodoDetailLayoutView = Marionette.LayoutView.extend({
        //テンプレート
        template : '#todo-detail-layout-template',

        regions : {
            itemRegion : '#todo-item',
        },

        onRender: function () {
            //Todoを取得
            this.todoModel = new TodoModel({
                id : this.options.modelId
            });
            var todoFetching = this.todoModel.fetch();
            //ユーザ一覧取得
            var userCollection = new UserCollection();
            var userFetching = userCollection.fetch();
            $.when(
                todoFetching,
                userFetching
            ).done(function(){
                this.showItem(this.todoModel, userCollection);
            }.bind(this));
        },

        showItem: function (todoModel, userCollection) {
            this.itemRegion.show(new TodoDetailItemView({
                model : todoModel,
                userList : userCollection.models
            }));
        },

    });
    return TodoDetailLayoutView;
});

Lesson2で説明したとおり、元は下記の流れです。

  1. onRender関数でユーザ一覧を取得、サーバからの正常レスポンス受信後、onLoadUsers関数を実行。
  2. onLoadUsers関数では、TOTO一覧を取得し、サーバからの正常レスポンス受信後、showItem関数を実行。
  3. shoItem関数で、実際に表示を行うItemViewを生成して表示。

これを、

  1. onRender関数で、ユーザ一覧を取得。サーバからのレスポンスが返る前にそのままTOTO一覧を取得。
  2. jQueryのwhendoneの仕組みを使用し、1で行った「ユーザ一覧の取得とTODO一覧の取得」が両方完了した時点でshowItemを実行する。

という流れに変えています。

:warning: 「jqueryのwhendoneのしくみ」と書きましたが、正確にはjQuery.Deferredというものを使用します。
良い説明がありましたのでリンクしておきます。(著者様、有難うございます)
爆速でわかるjQuery.Deferred超入門 - Yahoo! JAPAN Tech Blog

:warning: userCollectionは元々this.userCollectionと書いていましたが、ローカル変数で事足りるように成ったため、ローカル変数に変更しています。
そのため、showItem関数にデータを取得した状態のuserCollectionを渡すようにしています。
このため、テストコードのスタブのshowItem関数を修正する必要があります。その修正が下記です。

app/webroot/js/views/todo-detail-layout-view.js

app/webroot/js/views/todo-detail-layout-view.js

            it("Todoとユーザ一覧取得", function (done) {
                var layoutView = _createTodoDetailLayoutView();
-               sinon.stub(layoutView, 'showItem', function (todoModel) {
+               sinon.stub(layoutView, 'showItem', function (todoModel, userCollection) {
                    todoModel.should.be.ok;
+                   userCollection.should.be.ok;
                    layoutView.showItem.restore();
                    done();
                });

引数userCollectionを追加し、nullでないことをチェックしています。

修正は以上です。

では、テストを実行してみましょう。

うまくいったら、Gitにコミットしておきましょう。

:warning: GitHubでのdiff表示へのリンク

第11回 Lesson3 TODO詳細の表示データ取得処理のちょっとリファクタリング · suzukishouten-study/rest-study@a7dd0af

次回予告

次回でいよいよ最終回です。
これまでのまとめや、今後の展望などについて発表する予定です!
グループワークもあるかも!

コメント/フィードバックお待ちしております。

参加者の方も、そうでない方もお気づきの点があればお願い致します。