Upgrade to Pro — share decks privately, control downloads, hide ads and more …

実録レガシーコード改善 / Working with Legacy Code: the True Record

実録レガシーコード改善 / Working with Legacy Code: the True Record

2024/01/15(月) 12:00 〜 13:00

t-wadaさんが後世に残したい、実録レガシーコード改善 https://findy.connpass.com/event/304101/

テストコードが無いコードを引き継いだところからはじまる、実際に2018年に行った受託開発案件のエピソードとコードをプロダクトオーナー(引き継ぎ前のコードを書いた本人)の許可を得て使用しています。登場するコードは全て本物、登場するデータは講演用の架空のものです。

Takuto Wada

January 15, 2024
Tweet

More Decks by Takuto Wada

Other Decks in Programming

Transcript

  1. ࣮࿥ϨΨγʔίʔυվળ
    ٕज़తෛ࠴@
    fi
    OEZ
    5BLVUP8"%"
    +BO !'JOEZ
    !U@XBEB !UXBEB !UXBEB
    📷🙆 🙆
    rev.7

    View full-size slide

  2. ߨԋͷഎܠ


    w "84%FW%BZ5PLZPͰߨԋͨ͠಺༰Λେ෯ʹվగͯ͠ߨԋ͠·͢
    w ࣮ࡍʹ೥ʹߦͬͨडୗ։ൃҊ݅ͷΤϐιʔυͱίʔυΛϓϩμΫτΦʔφʔʢҾ͖ܧ͗લͷίʔυΛॻ͍ͨ
    ຊਓʣͷڐՄΛಘͯ࢖༻͍ͯ͠·͢
    w Ҿ͖ܧ͗લͷίʔυΛॻ͍ͨϓϩμΫτΦʔφʔ͸ݚڀऀͰ͋ͬͯɺϓϩͷ։ൃऀͰ͸͋Γ·ͤΜ
    w ొ৔͢Δίʔυ͸શͯຊ෺Ͱ͢
    w ٕज़͸೥౰࣌ͷ΋ͷͳͷͰɺݱࡏ͔ΒݟΔͱ΍΍ݹ͍Ͱ͢ɻੜ+BWB4DSJQU $PNNPO+4 .PDIB ౳ʑ
    w ొ৔͢Δσʔλ͸ߨԋ༻ͷՍۭͷ΋ͷͰ͢
    w +40/΍Ϩεϙϯεͷத਎͚ͩ͸ߨԋ༻ͷՍۭͷσʔλͰ͢

    View full-size slide

  3. Agenda
    ݱঢ়֬ೝ
    ಆ͏४උΛ੔͑Δ
    ػೳ௥Ճͱͦͷ݁Ռ
    ϞσϧΛ෼཭͢Δ
    ࣄ࣮ͱ৘ใΛ෼͚Δ
    ΞʔΩςΫνϟΛఆΊΔ
    👉
    3/118

    View full-size slide

  4. γφϦΦ౎ಓ෎ݝΫΠζΛߦ͏
    খ͍͞"MFYB4LJMM։ൃΛҾ͖ܧ͍ͩ

    View full-size slide

  5. ΞϨΫαɺ౎ಓ෎ݝΫΠζΛ։͍ͯ
    ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ
    ಢ໦ݝͷݝிॴࡏ஍͸ʁ

    View full-size slide

  6. Ӊ౎ٶʂ
    ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ
    ಢ໦ݝͷݝிॴࡏ஍͸ʁ
    ͦ͏Ͱ͢ɻͰ͸൪ɻ
    ԭೄݝͷݝͷՖ͸ʁ

    View full-size slide

  7. ͕͍ͪ·͢ɻਖ਼ղ͸σΠΰͰ͢ɻ
    Ͱ͸൪ɻἚ৓ݝͷϩʔϚࣈදه͸ʁ
    ͦ͏Ͱ͢ɻͰ͸൪ɻ
    ԭೄݝͷݝͷՖ͸ʁ
    ϋΠϏεΧεʁ

    View full-size slide

  8. ͦ͏Ͱ͢ɻͰ͸ऴΘΓͰ͢ɻ
    ͋ͳͨ͸఺Ͱͨ͠ɻ
    ਆށʂ
    ɾ
    ɾ
    ɾ
    ͦ͏Ͱ͢ɻͰ͸൪ɻ
    ฌݿݝͷݝிॴࡏ஍͸ʁ

    View full-size slide

  9. ΅Μ΍Γͱͨ͠ཧղ

    View full-size slide

  10. 'use strict';
    var Alexa = require('alexa-sdk');
    var questions = require('./questions.json');
    var handlers = {
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    'AMAZON.RepeatIntent': function () {
    var speechOutput = `${this.attributes['advance']}番。 ${questions[this.attributes['itemIndex']].q}`;
    this.emit(':ask', speechOutput, speechOutput);
    },
    'AMAZON.HelpIntent': function () {
    var speechOutput = 'クイズが出題されたらそれに答えてください。それでは始めましょう。「スタート」と言ってください。 ';
    this.emit(':ask', speechOutput, speechOutput);
    },
    'AMAZON.CancelIntent': function () {
    this.response.speak('終わります。');
    this.emit(':responseReady');
    },
    'AMAZON.StopIntent': function () {
    this.response.speak('終わります。');
    this.emit(':responseReady');
    },
    LaunchRequest: function () {
    this.emit('QuizIntent');
    },
    'AMAZON.StartOverIntent': function () {
    this.emit('QuizIntent');
    },
    Unhandled: function () {
    this.emit(':tell', 'すみません、わかりませんでした。終わります。');
    }
    };
    exports.handler = function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
    Ҿ͖ܧ͍ͩίʔυ
    w ߦͷ+BWB4DSJQU
    w ϩδοΫ͸"84-BNCEBͰ࣮૷
    w ࠓޙ͍ͭ͘΋ͷػೳ௥Ճ͕༧ఆ͞Ε͍ͯΔ
    w ओཁͳϩδοΫ͸ॳճىಈॲཧʢ2VJ[*OUFOUʣͱճ
    ౴डཧʢ"OTXFS*OUFOUʣͷΑ͏ͩ
    w ࣭໰σʔλ͸KTPOͰ؅ཧ͞Ε͍ͯΔ

    View full-size slide

  11. ·ͣ͸গ͠མͪண͍ͯίʔυΛಡΜͰΈΔ

    View full-size slide

  12. ॳճىಈॲཧͷίʔυ
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    どうやらthis.attributes に入れたものが
    リクエストを跨いで引き継がれるらしい

    View full-size slide

  13. ճ౴डཧͷίʔυ
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    this.event の深いところから
    ユーザの音声入力を受け取っているらしい
    文字列結合のスタイルが異なったり
    Attributes を使い回しているところに
    天然のレガシーコードらしさがある

    View full-size slide

  14. ཧղ͕গ͚ͩ͠ਐΉ

    View full-size slide

  15. 'use strict';
    var Alexa = require('alexa-sdk');
    var questions = require('./questions.json');
    var handlers = {
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    'AMAZON.RepeatIntent': function () {
    var speechOutput = `${this.attributes['advance']}番。 ${questions[this.attributes['itemIndex']].q}`;
    this.emit(':ask', speechOutput, speechOutput);
    },
    'AMAZON.HelpIntent': function () {
    var speechOutput = 'クイズが出題されたらそれに答えてください。それでは始めましょう。「スタート」と言ってください。 ';
    this.emit(':ask', speechOutput, speechOutput);
    },
    'AMAZON.CancelIntent': function () {
    this.response.speak('終わります。');
    this.emit(':responseReady');
    },
    'AMAZON.StopIntent': function () {
    this.response.speak('終わります。');
    this.emit(':responseReady');
    },
    LaunchRequest: function () {
    this.emit('QuizIntent');
    },
    'AMAZON.StartOverIntent': function () {
    this.emit('QuizIntent');
    },
    Unhandled: function () {
    this.emit(':tell', 'すみません、わかりませんでした。終わります。');
    }
    };
    exports.handler = function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
    w ߦͷ+BWB4DSJQU
    w ϩδοΫ͸"84-BNCEBͰ࣮૷
    w ࠓޙ͍ͭ͘΋ͷػೳ௥Ճ͕༧ఆ͞Ε͍ͯΔ
    w ओཁͳϩδοΫ͸ॳճىಈॲཧʢ2VJ[*OUFOUʣͱճ
    ౴डཧʢ"OTXFS*OUFOUʣͷΑ͏ͩ
    w ࣭໰σʔλ͸KTPOͰ؅ཧ͞Ε͍ͯΔ
    ཧղ͕গ͚ͩ͠ਐΉ

    View full-size slide

  16. ͳ͓ɺςετίʔυ͸ͳ͍

    View full-size slide

  17. IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM
    ςετͷͳ͍ίʔυ͸ѱ͍ίʔυͰ͋ΔɻͲΕ͚ͩ͏·͘ॻ͔Εͯ
    ͍Δ͔͸ؔ܎ͳ͍ɻͲΕ͚ͩඒ͍͔͠ɺΦϒδΣΫτࢦ޲͔ɺ͖ͪ
    ΜͱΧϓηϧԽ͞Ε͍ͯΔ͔͸ؔ܎ͳ͍ɻ
    ςετ͕͋Ε͹ɺݕূ͠ͳ͕Βίʔυͷಈ͖Λૉૣ͘มߋ͢Δ͜ͱ
    ͕Ͱ͖Δɻςετ͕ͳ͚Ε͹ɺίʔυ͕ྑ͘ͳ͍ͬͯΔͷ͔ѱ͘
    ͳ͍ͬͯΔͷ͔͕ຊ౰ʹ͸Θ͔Βͳ͍ɻ
    .JDIBFM'FBUIFSTᐌ͘
    ʰϨΨγʔίʔυվળΨΠυʱQWJ

    View full-size slide

  18. ͪͳΈʹɺόʔδϣϯ؅ཧ΋͞Ε͍ͯͳ͍

    View full-size slide

  19. ΋ͪΖΜࣗಈԽ΋͞Ε͍ͯͳ͍

    View full-size slide

  20. ιϑτ΢ΣΞ։ൃͷຊப͕Ұຊ΋ͳ͍ͱ͜Ζ͔Βͷελʔτͩͬͨ
    w 7FSTJPO$POUSPM
    w 5FTUJOH
    w "VUPNBUJPO
    🙅
    🙅
    🙅
    元のコードを書いたのがプロの開発者では
    ないので、これはしかたない

    View full-size slide

  21. Agenda
    ݱঢ়֬ೝ
    ಆ͏४උΛ੔͑Δ
    ػೳ௥Ճͱͦͷ݁Ռ
    ϞσϧΛ෼཭͢Δ
    ࣄ࣮ͱ৘ใΛ෼͚Δ
    ΞʔΩςΫνϟΛఆΊΔ
    👉
    21/118

    View full-size slide

  22. ຊபͷߏஙʹ༏ઌॱҐΛ͚ͭΔͳΒ
    Ґ7FSTJPO$POUSPM
    Ґ5FTUJOH
    Ґ"VUPNBUJPO

    View full-size slide

  23. ·ͣόʔδϣϯ؅ཧͱࣗಈԽ͕ٸ຿
    w όʔδϣϯ؅ཧ͸༏ઌ౓࠷େ
    w όʔδϣϯ؅ཧແ͠ͰਐΊΔͷ͸ةݥ͗͢Δɻແ͍ͱ࿩ʹͳΒͳ͍
    w ·ͣ͸ݱঢ়ͷίʔυͱઃఆϑΝΠϧΛશͯ(JUʹೖΕΔʢൿಗ৘ใ·ΘΓ͚ͩ஫ҙʣ
    w ࣗಈԽ͸ϨόϨοδ͕ޮ͖΍͍͢ͷͰૣظணख͢Δ
    w νʔϜ։ൃͷ৔߹ɺͩΕ͔Ұਓ͕ࣗಈԽ͢Ε͹શһ͕डӹऀʹͳΕΔ
    w ྫ͑͹Ϗϧυ΍σϓϩΠ͸Ұ౓ߏங͢Ε͹୯७࡞ۀͷ࣌ؒΛ҆ఆͯ͠࡟ݮͰ͖Δ
    w ؆୯ͳγΣϧεΫϦϓτ౳Ͱྑ͍ͷͰͱʹ͔͘ख࡞ۀΛݮΒ͢

    View full-size slide

  24. ࣍ʹࣗಈςετ

    View full-size slide

  25. खͱ໨ͱࣖͱޱͰͷಈ࡞֬ೝ͸ෆ҆ఆͰେมͭΒ͍
    ςετൣғ

    View full-size slide

  26. ࣗಈςετ͸લઢج஍
    w ࣗಈςετ͕ͳ͍ͱ҆શͳίʔυมߋͱࠓޙͷ։ൃܧଓ͕೉͍͠
    w खͱ໨ͱޱͱࣖͰͷ֬ೝ͸ίετ͕͔͔Γ͗͢ɺෆ҆ఆͰɺϑΟʔυόοΫ΋஗͍
    w ࠷ॳ͸໢ཏੑ͸ෆཁɻ೴ఱؾͳਖ਼ৗܥʢ)BQQZ1BUIʣͰ͍͍ͷͰɺಈࣗ͘ಈςετ͕ཉ͍͠
    w ϦϑΝΫλϦϯά΁ͷ଱ੑΛߴΊΔͨΊɺ࣮૷͔Βؒ߹͍ΛऔͬͨςετΛॻ͖͍ͨ
    w طଘͷςετ͕ͳ͍ͷ͸ઃܭ͕ѱ͍ஹީͰ͋Γɺઃܭ͸ࠓޙมߋ͞ΕΔՄೳੑ͕ඇৗʹߴ͍
    w मਖ਼ͨ͠Γػೳ௥Ճͨ͠Γ͢ΔͷͰɺطଘͷߏ଄ʹ݁߹ͨ͠ςετΛॻ͍ͯ΋ίεύ͕ѱ͍
    w ࣮૷ͷৄࡉ͔Β͋Δఔ౓ڑ཭ΛऔΓͭͭɺ
    ࣗಈςετΛ҆ఆ࣮ͯ͠ߦͰ͖ΔϙΠϯτΛ୳͍ͨ͠
    w ·ͣ͸ૈ͍ςετΛͻͱͭಈ͔͢ͱ͜Ζ·Ͱ͍͖͍࣋ͬͯͨ
    IUUQTCPPLNZOBWJKQFDQSPEVDUTEFUBJMJE

    View full-size slide

  27. ͜ͷཻ౓ͰࣗಈςετΛॻ͚ͳ͍͔ʁ
    経験則としてはリクエスト/レスポンスあたりを狙えると
    実装から距離を取りつつ安定したテストを書ける
    ςετൣғ
    ςετൣғ
    経験則としてはリクエスト/レスポンスあたりを狙えると
    実装から距離を取りつつ安定したテストを書ける

    View full-size slide

  28. "MFYBͷࣗಈςετʹ࢖͑Δ
    ϥΠϒϥϦ΍ϑϨʔϜϫʔΫΛௐࠪ

    View full-size slide

  29. "MFYB4LJMMͷࣗಈςετʹ࢖͏ಓ۩ͷީิΛͭݟ͚ͭͨʢ˞౰࣌ʣ
    w BMFYBTLJMMUFTUGSBNFXPSL
    w ߴػೳ͔ͭந৅Խ͞Ε͍ͯΔ൓໘ɺ͔Ώ͍ͱ͜Ζʹख͕ಧ͖ʹ͍͘
    w &BTZࢦ޲
    w BXTMBNCEBNPDLDPOUFYU
    w ػೳ͕গͳ͘ɺϨΠϠ͕ബ͍
    w 4JNQMFࢦ޲
    w ͜͜͸ϨΨγʔαόϯφɻ৘ใͷগͳ͍ઓ৔ʹ͸খ͘͞௚ަੑͷߴ͍4JNQMF
    ͳಓ۩͕ཉ͍͠ͷͰBXTMBNCEBNPDLDPOUFYUΛબ୒

    View full-size slide

  30. BXTMBNCEBNPDLDPOUFYUͰςετΛॻ͍ͯΈΔ
    const assert = require('assert').strict;
    const index = require('../index');
    const context = require('aws-lambda-mock-context');
    describe('LaunchRequest を起動して最初の問題を出題', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    index.handler(require('./fixtures/launch.json'), ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    it('handler の response', () => {
    assert(speechResponse !== undefined);
    });
    });
    「undefined でなければ良い」程度の雑さ
    で最初のテストを書く
    Lambda レベルのテストは若干重いので
    beforeEach ではなくあえて before で書いている

    View full-size slide

  31. ϦΫΤετΠϕϯτͷKTPO͸։ൃίϯιʔϧͷ௨৴ϩά͔ΒݟΑ͏ݟ·ͶͰࣗ࡞
    {
    "version": "1.0",
    "session": {
    "new": true,
    "sessionId": "amzn1.echo-api.session.XXXXXXXX",
    "application": {
    "applicationId": "amzn1.ask.skill.XXXXXXXX"
    },
    "attributes": {
    },
    "user": {
    "userId": "amzn1.ask.account.XXXXXXXX"
    }
    },
    "request": {
    "type": "IntentRequest",
    "requestId": "amzn1.echo-api.request.XXXXXXXX",
    "timestamp": "2018-10-26T11:38:40Z",
    "locale": "ja-JP",
    "intent": {
    "name": "LaunchRequest",
    "confirmationStatus": "NONE"
    }
    }
    }

    View full-size slide

  32. Α͠Α͠ಈͧ͘ɻ͜Ε͸େ͖ͳҰา (
    SFFO
    👍
    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    1 passing (19ms)

    View full-size slide

  33. VOEF
    fi
    OFEͱͷൺֱͰ͸βϧͳͷͰ׬શҰகͷςετʹॻ͖׵͑Δ
    });
    it('handler の response', () => {
    assert.deepEqual(speechResponse, {
    version: '1.0',
    response: {
    shouldEndSession: false,
    outputSpeech: {
    type: 'SSML',
    ssml: ' 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? '
    },
    reprompt: {
    outputSpeech: {
    type: 'SSML',
    ssml: ' 1番。 青森県の県庁所在地は? '
    }
    }
    },
    sessionAttributes: {
    advance: 1,
    score: 0,
    itemIndex: 4
    },
    userAgent: 'ask-nodejs/1.0.25 Node/v8.10.0'
    });
    });
    });

    View full-size slide

  34. ςετ͕ࣦഊ͢Δ 3
    FE

    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    1) handler の response
    0 passing (33ms)
    1 failing

    View full-size slide

  35. + expected - actual
    {
    "response": {
    "outputSpeech": {
    - "ssml": " 簡単なクイズをしましょう。1番。 福井県の都道府県コード番号は? "
    + "ssml": " 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? "
    "type": "SSML"
    }
    "reprompt": {
    "outputSpeech": {
    - "ssml": " 1番。 福井県の都道府県コード番号は? "
    + "ssml": " 1番。 青森県の県庁所在地は? "
    "type": "SSML"
    }
    }
    "shouldEndSession": false
    }
    "sessionAttributes": {
    "advance": 1
    - "itemIndex": 71
    + "itemIndex": 4
    "score": 0
    }
    "userAgent": "ask-nodejs/1.0.25 Node/v8.10.0"
    "version": "1.0"
    ग़ྗ͕ظ଴஋ͱ׬શҰக͠ͳ͍ 3
    FE
    🤔

    View full-size slide

  36. ϥϯμϜੑΛ൐͏ϩδοΫ͕ࣦഊͷݩڟͩͬͨ
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    var random = Math.floor(Math.random() * questions.length);
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    3
    FE

    View full-size slide

  37. {
    "q": "茨城県の県庁所在地は?",
    "a": "水戸",
    "g": "PrefecturalOfficeLocation"
    },
    {
    "q": "茨城県の県の花は?",
    "a": "バラ",
    "g": "PrefectureFlower"
    },
    {
    "q": "茨城県のローマ字表記は?",
    "a": "ibaraki",
    "g": "Romanization"
    },
    {
    "q": "茨城県の都道府県コード番号は?",
    "a": "8",
    "g": "PrefectureOrder"
    },
    {
    "q": "栃木県の県庁所在地は?",
    "a": "宇都宮",
    "g": "PrefecturalOfficeLocation"
    },
    ࣭໰σʔλʢKTPOʣ

    View full-size slide

  38. ϨΨγʔίʔυͷδϨϯϚ
    IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM
    lίʔυΛมߋ͢ΔͨΊʹ͸ςετΛ੔උ͢Δඞཁ
    ͕͋Δɻଟ͘ͷ৔߹ɺςετΛ੔උ͢ΔͨΊʹ͸ɺ
    ίʔυΛมߋ͢Δඞཁ͕͋Δz
    ʰϨΨγʔίʔυվળΨΠυʱQ

    View full-size slide

  39. ઀߹෦ʢ4FBNʣ
    l઀߹෦ʢ4FBNʣͱ͸ɺͦͷ৔ॴΛ௚઀ฤू͠ͳͯ͘΋ɺ
    ϓϩάϥϜͷৼΔ෣͍Λม͑Δ͜ͱͷͰ͖Δ৔ॴͰ͋Δz
    IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM
    ʰϨΨγʔίʔυվળΨΠυʱQ

    View full-size slide

  40. Ҿ਺Λ઀߹෦ͱ͠ɺϥϯμϜੑΛ൐͏ؔ਺Λ֎͔Βࠩ͠ࠐΊΔΑ͏ʹ͢Δ
    var createHandler = function (getNextItemIndex) {
    return function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(createHandlers(getNextItemIndex));
    alexa.execute();
    };
    };
    exports.createHandler = createHandler;
    exports.handler = function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
    };
    exports.handler = function () {
    var getNextItemIndex = () => Math.floor(Math.random() * questions.length);
    return createHandler(getNextItemIndex);
    }();
    変更前
    変更後
    次の質問のインデックスを返す関数を渡す
    変更後

    View full-size slide

  41. var questions = require('./questions.json');
    -var handlers = {
    +var createHandlers = function (getNextItemIndex) {
    +
    +return {
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    - var random = Math.floor(Math.random() * questions.length);
    + var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    @@ -29,7 +31,7 @@ var handlers = {
    if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    - var random = Math.floor(Math.random() * questions.length);
    + var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    @@ -65,9 +67,18 @@ var handlers = {
    ֎͔Β౉͞Εͨؔ਺Λ࢖͏Α͏ʹॱ࣍ॻ͖׵͑Δ

    View full-size slide

  42. ςετ͔Β͸ϥϯμϜͰ͸ͳ͘ݻఆ஋Λฦؔ͢਺Λࠩ͠ࠐΉ
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    handler(require('./fixtures/launch.json'), ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    自動テストの決定性を担保するため
    値を決め打ちする

    View full-size slide

  43. )VNCMF0CKFDU1BUUFSO
    IUUQTXXXBNB[PODPKQEQ
    ςετ༰қੑΛԼ͍͛ͯΔཁૉΛബ͘੾Γग़͠ɺ
    ςετՄೳൣғΛ޿͘औΔجຊύλʔϯɻ
    ࠓճͰݴ͑͹ϥϯμϜੑΛബ͘੾Γग़ͨ͠

    View full-size slide

  44. ɹɹɹग़ྗ͕ظ଴஋ͱҰக͢ΔΑ͏ʹͳͬͨɻ͜Ε͸͞Βʹେ͖ͳҰา
    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    1 passing (19ms)
    (
    SFFO
    👍

    View full-size slide

  45. ɹɹɹ·ͣ͸ॳճىಈ࣌ɺਖ਼ղɺෆਖ਼ղͷςετέʔεΛ੔උ (
    SFFO
    👍
    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    問題に不正解の場合
    ✓ handler の response
    3 passing (23ms)

    View full-size slide

  46. ϨΨγʔίʔυվળٕ๏ͷதͰ͜͜·Ͱʹ࢖ͬͨ΋ͷ
    w ઀߹෦ͷݕ౼ͱۛຯ
    w ߜΓࠐΈ఺ͷൃݟͱґଘͷ෼཭
    w )VNCMF0CKFDU1BUUFSO
    w ࠶ݱੑ͋Δςετϋʔωεͷ੔උ
    w εϓϥ΢τΫϥε

    View full-size slide

  47. ͔Ζ͏ͯ͡ಆ͏४උ͕੔ͬͨ

    View full-size slide

  48. Agenda
    ݱঢ়֬ೝ
    ಆ͏४උΛ੔͑Δ
    ػೳ௥Ճͱͦͷ݁Ռ
    ϞσϧΛ෼཭͢Δ
    ࣄ࣮ͱ৘ใΛ෼͚Δ
    ΞʔΩςΫνϟΛఆΊΔ
    👉
    48/118

    View full-size slide

  49. ͜͜Ͱ࢓༷มߋͰ͢
    ͕ͩɺ͍·΍ࢲͨͪʹ͸ςετ͕͋Δʂ
    ΍͍ͬͯͧ͘ʂʂ
    w ؒҧ͑ͯ΋ɺಉ͡໰୊Λճ·Ͱ܁Γฦ͍ͨ͠
    w ؒҧ͑ͨճ਺ʹԠͯ͡ϝοηʔδΛม͍͑ͨ
    w ਖ਼ղͨ͠Β࣍ͷ໰୊ʹਐΉ
    w ճؒҧ͑ͨΒෆਖ਼ղͱͯ࣍͠ͷ໰୊ʹਐΉ

    View full-size slide

  50. 5%%ͷαΠΫϧ
    ࣍ͷ໨ඪΛߟ͑ͯϦετΞοϓ͢Δ
    Ϧετ͔ΒͻͱͭϐοΫΞοϓͯͦ͠ͷ໨ඪΛࣔ͢ςετΛͻͱͭॻ͘
    ͦͷςετΛ࣮ߦࣦͯ͠ഊͤ͞Δ 3FE

    ໨తͷίʔυΛॻ͘
    Ͱॻ͍ͨςετΛ੒ޭͤ͞Δ (SFFO

    ςετ͕௨Δ··ͰϦϑΝΫλϦϯάΛߦ͏ 3FGBDUPS

    ̍ʙΛ܁Γฦ͢
    IUUQTXXXPINTIBDPKQCPPL

    View full-size slide

  51. });
    });
    -describe('問題に不正解の場合', () => {
    +describe('1回目の不正解の場合', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    - handler(require('./fixtures/req_incorrect.json'), ctx);
    + const req = require('./fixtures/req_incorrect.json');
    + Object.assign(req.session.attributes, { // 事前状態
    + advance: 2,
    + score: 1,
    + accumIncorrects: 0,
    + itemIndex: 8
    + });
    + handler(req, ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    + it('連続不正解数が増えていること', () => {
    + assert(speechResponse.sessionAttributes.accumIncorrects === 1);
    + });
    - it('handler の response', () => {
    + it.skip('handler の response', () => {
    const expected = {
    ·ͣ͸ࣦഊ͢ΔςετΛॻ͘ 3
    FE
    テストコード内で事前状態を作る
    完全一致のテストはしばらくの間スキップ

    View full-size slide

  52. ༧૝௨Γςετ͕ࣦഊ͢Δ͜ͱΛ֬ೝ͢Δ
    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    - handler の response
    1) 連続不正解数が増えていること
    2 passing (33ms)
    1 pending
    1 failing
    1) 1回目の不正解の場合
    連続不正解数が増えていること:
    AssertionError [ERR_ASSERTION]: # test/test.js:128
    assert(speechResponse.sessionAttributes.accumIncorrects === 1)
    | | | |
    | | 0 false
    | Object{advance:3,score:1,accumIncorrects:0,itemIndex:4}
    Object{version:"1.0",response:#Object#,sessionAttributes:#Object#,userAgent:"ask-nodejs/1.0.25 Node/v8.10.0"}
    [number] 1
    => 1
    [number] speechResponse.sessionAttributes.accumIncorrects
    => 0
    3
    FE
    完全一致のテストはしばらくの間スキップ

    View full-size slide

  53. --- a/index.js
    +++ b/index.js
    @@ -21,15 +21,21 @@ return {
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    + var shouldRepeatSameQuestion = false;
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    } else { // 不正解の場合
    + this.attributes['accumIncorrects']++;
    + shouldRepeatSameQuestion = true;
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    }
    - if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    + if (shouldRepeatSameQuestion) {
    + var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`;
    + this.emit(':ask', resultMessage, reprompt);
    + } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    ࣦഊ͍ͯ͠ΔςετΛ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘

    View full-size slide

  54. ςετͷ੒ޭΛ֬ೝ͢Δ
    $ npm test
    > mocha --require intelli-espower-loader test
    LaunchRequest を起動して最初の問題を出題
    ✓ handler の response
    問題に正解した場合
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    - handler の response
    3 passing (21ms)
    1 pending
    (
    SFFO

    View full-size slide

  55. --- a/test/test.js
    +++ b/test/test.js
    @@ -101,6 +101,9 @@ describe('1回目の不正解の場合', () => {
    it('連続不正解数が増えていること', () => {
    assert(speechResponse.sessionAttributes.accumIncorrects === 1);
    });
    + it('返答の音声内容が1回目の不正解に伴う内容であること', () => {
    + assert(speechResponse.response.outputSpeech.ssml === ' 久慈? もう
    一度言ってください。岩手県の県庁所在地は? ');
    + });
    it.skip('handler の response', () => {
    const expected = {
    version: '1.0',
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    1) 返答の音声内容が1回目の不正解に伴う内容であること
    - handler の response
    3 passing (37ms)
    1 pending
    1 failing
    ·ࣦͣഊ͢ΔςετΛॻ͘ 3
    FE

    View full-size slide

  56. 1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    - handler の response
    4 passing (21ms)
    1 pending
    --- a/index.js
    +++ b/index.js
    @@ -29,7 +29,7 @@ return {
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    shouldRepeatSameQuestion = true;
    - resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    }
    if (shouldRepeatSameQuestion) {
    ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘ (
    SFFO

    View full-size slide

  57. assert(speechResponse.response.outputSpeech.ssml === ' 久慈? もう一度言ってくださ
    い。岩手県の県庁所在地は? ');
    });
    + it('進行状況が進んでいないこと', () => {
    + assert(speechResponse.sessionAttributes.advance === 2);
    + });
    + it('得点が変わらないこと', () => {
    + assert(speechResponse.sessionAttributes.score === 1);
    + });
    + it('問題番号が変わらないこと', () => {
    + assert(speechResponse.sessionAttributes.itemIndex === 8);
    + });
    it.skip('handler の response', () => {
    const expected = {
    5%%ͷճసΛ܁Γฦ͢ (
    SFFO
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    - handler の response
    7 passing (22ms)
    1 pending

    View full-size slide

  58. it('問題番号が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.itemIndex === 8);
    });
    - it.skip('handler の response', () => {
    + it('handler の response', () => {
    const expected = {
    version: '1.0',
    response: {
    shouldEndSession: false,
    outputSpeech: {
    type: 'SSML',
    - ssml: ' ちがいます。正解は盛岡です。 では3番。 青森県の県庁所在地は? '
    + ssml: ' 久慈? もう一度言ってください。岩手県の県庁所在地は? '
    },
    reprompt: {
    outputSpeech: {
    type: 'SSML',
    - ssml: ' 3番。 青森県の県庁所在地は? '
    + ssml: ' 2番。 岩手県の県庁所在地は? '
    }
    }
    },
    sessionAttributes: {
    - advance: 3,
    + advance: 2,
    score: 1,
    - itemIndex: 4
    + accumIncorrects: 1,
    + itemIndex: 8
    },
    userAgent: 'ask-nodejs/1.0.25 Node/v8.10.0'
    ׬શҰகͷςετ΋εΩοϓΛ΍ΊͯάϦʔϯʹ෮ؼ (
    SFFO
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    8 passing (20ms)
    このタイミングで完全一致のテストを復帰

    View full-size slide

  59. describe('2回目の不正解の場合', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    const req = require('./fixtures/req_incorrect.json');
    Object.assign(req.session.attributes, {
    advance: 2,
    score: 1,
    accumIncorrects: 1,
    itemIndex: 8
    });
    handler(req, ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    it('連続不正解数が増えていること', () => {
    assert(speechResponse.sessionAttributes.accumIncorrects === 2);
    });
    it('返答の音声内容が2回目の不正解に伴う内容であること', () => {
    assert(speechResponse.response.outputSpeech.ssml === ' 私には「久慈」
    と聞こえましたが、それは正しくありません。もう一度言ってください。岩手県の県庁所在地は?
    speak>');
    });
    it('進行状況が進んでいないこと', () => {
    assert(speechResponse.sessionAttributes.advance === 2);
    });
    it('得点が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.score === 1);
    });
    it('問題番号が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.itemIndex === 8);
    });
    });
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    1) 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    12 passing (47ms)
    1 failing
    5%%ͷճసΛ܁Γฦ͢ 3
    FE

    View full-size slide

  60. --- a/index.js
    +++ b/index.js
    @@ -29,7 +29,11 @@ return {
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    shouldRepeatSameQuestion = true;
    - resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    + if (this.attributes['accumIncorrects'] == 1) {
    + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    + } else if (this.attributes['accumIncorrects'] == 2) {
    + resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。も
    う一度言ってください。${currentQuestion.q}`;
    + }
    }
    if (shouldRepeatSameQuestion) {
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    13 passing (22ms)
    5%%ͷճసΛ܁Γฦ͢ (
    SFFO

    View full-size slide

  61. --- a/index.js
    +++ b/index.js
    @@ -29,11 +29,14 @@ return {
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    shouldRepeatSameQuestion = true;
    - if (this.attributes['accumIncorrects'] == 1) {
    - resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    - } else if (this.attributes['accumIncorrects'] == 2) {
    - resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言っ
    てください。${currentQuestion.q}`;
    - }
    + switch (this.attributes['accumIncorrects']) {
    + case 1:
    + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    + break;
    + case 2:
    + resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言っ
    てください。${currentQuestion.q}`;
    + break;
    + }
    }
    if (shouldRepeatSameQuestion) {
    5%%ͷճసΛ܁Γฦ͢ 3
    FGBDUPS

    View full-size slide

  62. describe('3回目の不正解の場合', () => {
    let speechResponse;
    before((done) => {
    const ctx = context();
    const getNextItemIndex = () => 4;
    const handler = index.createHandler(getNextItemIndex);
    const req = require('./fixtures/req_incorrect.json');
    Object.assign(req.session.attributes, {
    advance: 2,
    score: 1,
    accumIncorrects: 2,
    itemIndex: 8
    });
    handler(req, ctx);
    ctx.Promise.then(resp => {
    speechResponse = resp;
    done();
    }).catch(err => done(err));
    });
    it('連続不正解数が0に戻っていること', () => {
    assert(speechResponse.sessionAttributes.accumIncorrects === 0);
    });
    it('返答の音声内容が3回目の不正解に伴う内容であること', () => {
    assert(speechResponse.response.outputSpeech.ssml === ' ちがいま
    す。正解は盛岡です。 では3番。 青森県の県庁所在地は? ');
    });
    it('次の問題に進んでいるので進行状況が進んでいること', () => {
    assert(speechResponse.sessionAttributes.advance === 3);
    });
    it('得点が変わらないこと', () => {
    assert(speechResponse.sessionAttributes.score === 1);
    });
    it('次の問題に進んでいるので問題番号が変わっていること', () => {
    assert(speechResponse.sessionAttributes.itemIndex === 4);
    });
    });
    า෯Λ޿͛ͯ։ൃͷϖʔεΛ͞Βʹ଎ΊΔ 3
    FE
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    3回目の不正解の場合
    1) 連続不正解数が0に戻っていること
    2) 返答の音声内容が3回目の不正解に伴う内容であること
    3) 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    4) 次の問題に進んでいるので問題番号が変わっていること
    14 passing (52ms)
    4 failing

    View full-size slide

  63. ҰؾʹάϦʔϯʹ͢Δ
    diff --git a/index.js b/index.js
    index 65f66d8..4498974 100644
    --- a/index.js
    +++ b/index.js
    @@ -28,13 +28,18 @@ return {
    this.attributes['score']++;
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    - shouldRepeatSameQuestion = true;
    switch (this.attributes['accumIncorrects']) {
    case 1:
    resultMessage = `${usersAnswer}?
    + shouldRepeatSameQuestion = true;
    break;
    case 2:
    resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。
    もう一度言ってください。${currentQuestion.q}`;
    + shouldRepeatSameQuestion = true;
    + break;
    + default:
    + resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    + this.attributes['accumIncorrects'] = 0;
    break;
    }
    }
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    3回目の不正解の場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が3回目の不正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    ✓ 次の問題に進んでいるので問題番号が変わっていること
    18 passing (19ms)
    (
    SFFO

    View full-size slide

  64. LaunchRequest を起動して最初の問題を出題
    ✓ 連続不正解数が0であること
    ✓ 返答の音声内容が初回の出題に伴う内容であること
    ✓ 進行状況は1であること
    ✓ 得点は0であること
    ✓ 出題された問題番号は 4
    ✓ handler の response
    問題に正解した場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が問題の正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が1増えていること
    ✓ 次の問題に進んでいるので問題番号が変わっていること
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題番号が変わらないこと
    3回目の不正解の場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が3回目の不正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    ✓ 次の問題に進んでいるので問題番号が変わっていること
    最終問題に正解した場合
    ✓ shouldEndSession が true になっていること
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容がクイズ終了を知らせる内容であること
    ✓ 進行状況が変わらないこと
    ✓ 得点が1増えていること
    ✓ 問題番号が変わらないこと
    ✓ handler の response
    35 passing (32ms)
    (
    SFFO
    ΍͔ͬͨʂʁ
    ˞ϑϥάɹɹɹɹ
    5%%ͷճసΛճ͠ଓ͚ɺ͖͞΄Ͳͷ
    ࢓༷มߋΛຬͨ͢ςετͱίʔυ͕
    ΦʔϧάϦʔϯ·Ͱ΍͖ͬͯͨ
    35 passing (32ms)

    View full-size slide

  65. ͕ͩɺͪΐͬͱ଴ͬͯ΄͍͠

    View full-size slide

  66. ઃܭ͸ྑ͘ͳ͍ͬͯΔͩΖ͏͔
    IUUQTUXPQBHJMFFTNDPKQXIBUEPXFOFFEGPSHSPXUIPGGVUVSFDCBGF
    رബԽͨ͠5%%ɺϓϩμΫτͷ੒௕ͷͨΊʹඞཁͳ΋ͷ͸ʁʙʰ݈શͳϏδωεͷܧଓత੒௕ͷͨΊʹ͸݈શͳίʔυ͕ඞཁͩʱରஊʢ̒ʣΑΓ

    View full-size slide

  67. ͜͜·Ͱॻ͍͖ͯͨίʔυΛݟͯΈΔͱɺ͜Ε͸ʜʜ
    ͜ɺ͜Ε͸ʜʜ😨
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var shouldRepeatSameQuestion = false;
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    this.attributes['accumIncorrects'] = 0;
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    switch (this.attributes['accumIncorrects']) {
    case 1:
    resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    case 2:
    resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    default:
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    this.attributes['accumIncorrects'] = 0;
    break;
    }
    }
    if (shouldRepeatSameQuestion) {
    var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`;
    this.emit(':ask', resultMessage, reprompt);
    } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },

    View full-size slide

  68. ϓϩμΫτίʔυͷ࣭͸
    Ή͠Ζ௿Լ͍ͯ͠ΔͷͰ͸ʜʜʁ

    View full-size slide

  69. ֏͞Μᐌ͘
    ςετͰ͸඼࣭͸্͕Βͳ͍Ͱ͢Αɻςετ͸͋͘·Ͱ΋
    ඼࣭Λ͋͛Δ͖͔͚ͬɻ඼࣭Λ͋͛Δͷ͸ϓϩάϥϛϯ
    άͰ͢ɻ͜Ε͸େੲ͔Βͦ͏ɻ

    View full-size slide

  70. ͜͜Ͱ௥͍ଧͪΛ͔͚ΔΑ͏ʹ࢓༷มߋ
    ͑ͬɺ͍·ʜʜʁ😨
    w ໰ΛϒϩοΫʢ ʣʹ۠੾ΓɺલͷϒϩοΫͰؒҧ͑ͨ
    δϟϯϧʢݝிॴࡏ஍౳ʣΛ༏ઌͯ͠ग़୊͍ͨ͠
    w ϒϩοΫ಺Ͱಉ͡δϟϯϧ͸ग़͞ͳ͍
    w ࠷ॳͷϒϩοΫͰ͸δϟϯϧ͕ॏͳΒͳ͍Α͏ʹϥϯμϜʹ໰
    ग़͍ͨ͠

    View full-size slide

  71. ͞Βʹ௥͍ଧͪΛ͔͚ΔΑ͏ʹج൫ϥΠϒϥϦͷαϙʔτऴྃ
    ͑ͬɺ͍·ʜʜʁ😭
    npm WARN deprecated [email protected]:
    This version of the Alexa Skills Kit SDK
    is no longer supported.
    Please use the v2 release found here:
    https://github.com/alexa/alexa-skills-
    kit-sdk-for-nodejs

    View full-size slide

  72. ͜Μͳ͸ͣͰ͸ͳ͔ͬͨ

    View full-size slide

  73. IUUQTXXXBNB[PODPKQEQ
    ʰ$MFBO"SDIJUFDUVSFʱQ
    ʮ͋ͱͰΫϦʔϯʹ͢Ε͹͍͍Αɻઌʹࢢ৔ʹग़͞ͳ͚Ε͹ʂʯ
    ։ൃऀ͸ͦ͏΍͍ͬͯͭ΋͝·͔͢ɻ͕ͩɺ͋ͱͰΫϦʔϯʹ͢Δ͜ͱ͸ͳ͍ɻ
    ࢢ৔͔ΒͷϓϨογϟʔ͸ࢭ·Βͳ͍͔Βͩɻʮઌʹࢢ৔ʹग़͞ͳ͚Ε͹ʯͱ͍
    ͏͜ͱ͸ɺޙΖʹڝ߹ଞ͕ࣾେ੎͍Δͱ͍͏͜ͱͰ͋Δɻڝ߹ଞࣾʹ௥͍ൈ͔Ε
    ͳ͍ͨΊʹ͸ɺ͜Ε͔Β΋૸Γଓ͚Δ͔͠ͳ͍ɻ
    ͦͷ݁Ռɺ։ൃऀ͸ϞʔυΛ੾Γସ͑Δ͜ͱ͕Ͱ͖ͳ͍ɻ࣍ͷػೳɺ·ͨ࣍ͷػ
    ೳɺ·ͨ·ͨ࣍ͷػೳΛ௥Ճ͢Δ͜ͱʹͳΓɺίʔυΛΫϦʔϯʹ͢Δ͜ͱ·Ͱ
    ख͕ճΒͳ͍ɻ
    ͦͯ͠ɺ่յ͕࢝·Δɻੜ࢈ੑ͕θϩʹ͍͍ۙͮͯ͘ɻ
    6ODMF#PCᐌ͘

    View full-size slide

  74. Agenda
    ݱঢ়֬ೝ
    ಆ͏४උΛ੔͑Δ
    ػೳ௥Ճͱͦͷ݁Ռ
    ϞσϧΛ෼཭͢Δ
    ࣄ࣮ͱ৘ใΛ෼͚Δ
    ΞʔΩςΫνϟΛఆΊΔ
    👉
    74/118

    View full-size slide

  75. ݱঢ়ͷͭΒ͞Λ෼ੳ͢Δ
    w ͭͷมߋཁҼʢج൫ͷࣄ৘ɺ࢓༷ͷมߋʣ͕୯ҰͷϓϩμΫτίʔυʹ͢΂
    ͯ߱Γ͔͔ͬͯ͘Δ
    w ୯Ұ੹೚ͷݪଇʢ431ʣʹ൓͍ͯ͠Δͱ͍͑Δ
    w ج൫ͷΞοϓσʔτͱ࢓༷มߋɺ΋ͪΖΜͲͪΒ΋΍Βͳ͚Ε͹ͳΒͳ͍
    w ݱঢ়͸ΠϕϯτϋϯυϥʹશͯͷυϝΠϯϩδοΫ͕ॻ͔Ε͍ͯΔঢ়ଶʹ౳͠
    ͍ɻ͜ͷ··Ͱ͸ະདྷ͕ͳ͍
    w υϝΠϯϞσϧΛ"MFYBͱ-BNCEB͔ΒҾ͖͸͕͍ͨ͠

    View full-size slide

  76. ઓज़&YUSBDUPS4QSPVU
    w &YUSBDU
    w طଘͷίʔυʹςετΛॻ͍ͯอޢ͠ͳ͕Βɺ৽͘͠ίʔυΛநग़͍ͯ͘͠
    w 4QSPVU
    w طଘͷίʔυʹςετΛॻ͘͜ͱ͸͖͋ΒΊΔͱͯ͠΋ɺͤΊͯ৽͘͠ॻ͘
    ίʔυ͚ͩ͸ςετΛॻ͖ͳ͕Β։ൃ͢Δ
    IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM

    View full-size slide

  77. &YUSBDUઓज़ͱ4QSPVUઓज़ͷςετൣғ
    4QSPVU
    &YUSBDU

    View full-size slide

  78. ࡞ઓυϝΠϯϞσϧΛநग़ʢ&YUSBDUʣ͢Δઓज़Λબ୒
    w ͜͜·Ͱॻ͍͖ͯͨૈཻ౓ͷ-BNCEBϨϕϧͷςετΛάϦʔϯʹอͪ
    ͳ͕ΒɺϩδοΫΛυϝΠϯϞσϧΫϥεʹҾ͖͸͕͍ͯ͘͠
    w υϝΠϯϞσϧ͸"MFYB΍-BNCEBʹґଘͤ͞ͳ͍
    w υϝΠϯϞσϧ͸ςετۦಈ։ൃͰ৽ن։ൃ͢Δ
    w ࢓༷͕ݻ·Βͳ͍͔Βςετ͕ॻ͚ͳ͍ͷͰ͸ͳ͘ɺ࢓༷͕ݻ·Βͳ͍͔
    Βͦ͜ςετΛॻ͍ͯมԽΛࢧ͍͑ͯ͘

    View full-size slide

  79. ςετϐϥϛουͷԼஈΛ෼ް͘͢Δઓज़
    Ϣχοτ
    ΠϯςάϨʔγϣϯ
    &&
    ίετ
    ஧࣮ੑ
    ଎౓
    ςετέʔε਺
    ߴ
    ௿
    ௿
    ߴ
    ܾఆੑ

    View full-size slide

  80. ɹ1MBJO0MEͳϞσϧΫϥεΛॻ͚͹"MFYB΍-BNCEBʹґଘͤͣςετ͕ॻ͚Δ
    const assert = require('assert').strict;
    const {Session} = require('../models');
    describe('Session#start(item)', () => {
    let session;
    beforeEach(() => {
    session = new Session();
    session.start({
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    });
    });
    it('#message', () => {
    assert(session.message() === '簡単なクイズをしましょう。1番。 広島県の県の花は?');
    });
    it('#reprompt', () => {
    assert(session.reprompt() === '1番。 広島県の県の花は?');
    });
    });
    3
    FE
    Plain Old なオブジェクトのテストは非常に高速に
    動作するので beforeEach を気軽に使える

    View full-size slide

  81. -BNCEB͔ΒϩδοΫΛίϐʔͯ͠ςετΛ௨͢
    class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    message () {
    return `簡単なクイズをしましょう。${this.advance}番。 ${this.item.q}`;
    }
    reprompt () {
    return `${this.advance}番。 ${this.item.q}`;
    }
    }
    module.exports = {
    Session
    };
    (
    SFFO

    View full-size slide

  82. 'use strict';
    var Alexa = require('alexa-sdk');
    var questions = require('./questions.json');
    +const {Session} = require('./models');
    var createHandlers = function (getNextItemIndex) {
    return {
    QuizIntent: function () { // 初期状態
    - this.attributes['advance'] = 1; // 進行状況を初期化
    - this.attributes['score'] = 0; // 得点を初期化
    - this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    - var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    - var reprompt = `1番。 ${questions[random].q}`;
    - this.emit(':ask', message, reprompt); // 相手の回答を待つ
    + const session = new Session();
    + session.start(questions[random]);
    + this.attributes['advance'] = session.advance;
    + this.attributes['score'] = session.score;
    + this.attributes['accumIncorrects'] = session.accumIncorrects;
    + this.emit(':ask', session.message(), session.reprompt()); // 相手の回答を待つ
    },
    -BNCEB͕ϞσϧΛ࢖͏Α͏ʹมߋ (
    SFFO

    View full-size slide

  83. ࣍͸ճ౴डཧॲཧΛςετϑΝʔετͰॻ͘
    describe('Session#receive(answer)', () => {
    let session;
    beforeEach(() => {
    session = new Session();
    session.start({
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    });
    });
    describe('正解した場合', () => {
    beforeEach(() => {
    session.receive('モミジ');
    });
    it('score が加点されていること', () => {
    assert(session.score === 1);
    });
    });
    });
    3
    FE

    View full-size slide

  84. γϯϓϧʹςετΛάϦʔϯʹ͢Δ
    class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    message () {
    return `簡単なクイズをしましょう。${this.advance}番。 ${this.item.q}`;
    }
    reprompt () {
    return `${this.advance}番。 ${this.item.q}`;
    }
    receive (answer) {
    if (this.item.a === answer) {
    this.score += 1;
    }
    }
    }
    module.exports = {
    Session
    };
    (
    SFFO

    View full-size slide

  85. $ npm run test:models
    初回の出題時
    ✓ 問題番号は 1
    ✓ 得点は 0
    ✓ 連続不正解数は 0
    ✓ 次の問題に進むこと
    ✓ メッセージ
    ✓ reprompt
    3問目に初挑戦する状態
    正解した場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    ✓ 次の問題を設定した後のメッセージ
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に1回不正解の状態
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に2回不正解の状態
    不正解の場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    ✓ 次の問題を設定した後のメッセージ
    最終問題に初挑戦する状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は false
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    最終問題に2回不正解している状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    50 passing (59ms)
    Ҏ߱5%%ͷճసΛଓ͚ɺϩδοΫΛ-BNCEB͔Β͋Δఔ౓Ҡಈͨ͠
    class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    restore (attrs) {
    Object.assign(this, attrs);
    }
    setNextItem (item) {
    this.item = item;
    }
    message () {
    if (this.shouldRepeatSameQuestion()) {
    return this.resultMessage;
    }
    if (this.advance === 7) {
    return `${this.resultMessage}終わりです。あなたは${this.score}点でした。`;
    }
    if (this.advance === 1) {
    return `簡単なクイズをしましょう。${this.reprompt()}`;
    }
    return `${this.resultMessage}${this.reprompt()}`;
    }
    reprompt () {
    return `${this.advance}番。 ${this.item.q}`;
    }
    receive (answer) {
    this.shouldRepeat = false;
    if (this.item.a === answer) {
    this.resultMessage = 'そうです。 では';
    this.score += 1;
    this.accumIncorrects = 0;
    if (this.advance < 7) {
    this.advance += 1;
    }
    } else {
    this.accumIncorrects += 1;
    switch (this.accumIncorrects) {
    case 1:
    this.resultMessage = `${answer}? もう一度言ってください。${this.item.q}`;
    this.shouldRepeat = true;
    break;
    case 2:
    this.resultMessage = `私には「${answer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${this.item.q}`;
    this.shouldRepeat = true;
    break;
    default:
    this.resultMessage = `ちがいます。正解は${this.item.a}です。 では`;
    this.accumIncorrects = 0;
    if (this.advance < 7) {
    this.advance += 1;
    }
    break;
    }
    }
    }
    isFinished () {
    return this.advance === 7 && !this.shouldRepeat;
    }
    shouldRepeatSameQuestion () {
    return this.shouldRepeat === true;
    }
    }

    View full-size slide

  86. ॳճىಈϋϯυϥͷݱঢ়
    QuizIntent: function () { // 初期状態
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    const session = new Session();
    session.start(questions[random]);
    this.attributes['advance'] = session.advance;
    this.attributes['score'] = session.score;
    this.attributes['accumIncorrects'] = session.accumIncorrects;
    this.emit(':ask', session.message(), session.reprompt());
    },
    まだロジックを切り出しただけ。
    Lambda レベルのテストのグリーンを保つため、
    リクエストをまたがるデータの持ち方は変えない

    View full-size slide

  87. ճ౴डཧϋϯυϥͷݱঢ়
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    const session = new Session();
    session.restore({
    advance: this.attributes['advance'],
    score: this.attributes['score'],
    accumIncorrects: this.attributes['accumIncorrects'],
    item: currentQuestion
    });
    session.receive(usersAnswer);
    this.attributes['advance'] = session.advance;
    this.attributes['score'] = session.score;
    this.attributes['accumIncorrects'] = session.accumIncorrects;
    if (session.shouldRepeatSameQuestion()) {
    this.emit(':ask', session.message(), session.reprompt());
    } else if (session.isFinished()) { // 全ての問題が終了した場合
    this.emit(':tell', session.message()); // 会話を終える
    } else { // 続きの問題がある場合
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    session.setNextItem(questions[random]);
    this.emit(':ask', session.message(), session.reprompt()); // 会話を続ける
    }
    },
    現在の状態や正答/誤答にもとづく
    メッセージがLambdaから消えた

    View full-size slide

  88. ·ͩ·ͩߦͧ͘

    View full-size slide

  89. ࡞ઓৄࡉ΁ͷґଘΛݮΒ͍ͯ͘͠
    w Ϟσϧͷ͍࣋ͬͯΔ৘ใΛϦΫΤετΛ·͍ͨͰμϯϓϦετ
    ΞͰ͖ΔΑ͏ʹͯ͠ɺ"MFYBݻ༗ͷػೳʢBUUSJCVUFTʣ΁ͷґଘ
    ΛݮΒ͍ͯ͘͠
    w -BNCEBϨϕϧͷςετ͸Ұ࣌తʹ੺͘ͳΔ͕ɺظ଴஋ʹμϯ
    ϓσʔλΛՃ͑ΔܗͰ྘ʹ໭͍ͯ͘͠
    w ϞσϧΫϥε͸Ҿ͖ଓ͖ςετۦಈ։ൃͰ։ൃ͢Δ

    View full-size slide

  90. ·ͣEVNQػೳͷςετΛॻ͍ͯ੺͘͢Δ 3
    FE
    describe('Session#dump()', () => {
    let session;
    beforeEach(() => {
    session = new Session();
    session.start({
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    });
    });
    it('dump', () => {
    assert.deepEqual(session.dump(), {
    advance: 1,
    score: 0,
    accumIncorrects: 0,
    item: {
    q: '広島県の県の花は?',
    a: 'モミジ',
    g: 'PrefectureFlower'
    }
    });
    });
    });

    View full-size slide

  91. γϯϓϧʹάϦʔϯʹ͢Δ
    class Session {
    start (item) {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = item;
    }
    dump () {
    return {
    advance: this.advance,
    score: this.score,
    accumIncorrects: this.accumIncorrects,
    item: this.item
    };
    }
    restore (attrs) {
    Object.assign(this, attrs);
    }
    }
    (
    SFFO

    View full-size slide

  92. Ҏ߱5%%ͷճసΛଓ͚ɺμϯϓ͚ͩͰϦΫΤετΛ·ͨ͛ΔΑ͏ʹͳͬͨ
    初回の出題時
    ✓ 問題番号は 1
    ✓ 得点は 0
    ✓ 連続不正解数は 0
    ✓ 次の問題に進むこと
    ✓ メッセージ
    ✓ reprompt
    ✓ dump
    3問目に初挑戦する状態
    正解した場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    正解後に次の問題を設定した状態
    ✓ dump
    ✓ メッセージ
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に1回不正解の状態
    不正解の場合
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    3問目に既に2回不正解の状態
    不正解の場合
    ✓ 問題番号がひとつ進むこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進むこと
    不正解後に次の問題を設定した状態
    ✓ dump
    ✓ メッセージ
    最終問題に初挑戦する状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は false
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数が増えていること
    ✓ 同じ問題を繰り返すこと
    ✓ メッセージ
    最終問題に2回不正解している状態
    正解した場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が加点されていること
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    不正解の場合
    ✓ isFinished は true
    ✓ 問題番号が変わらないこと
    ✓ 得点が変わらないこと
    ✓ 連続不正解数がリセットされていること
    ✓ 次の問題に進まないこと
    ✓ メッセージ
    LaunchRequest を起動して最初の問題を出題
    ✓ 連続不正解数が0であること
    ✓ 返答の音声内容が初回の出題に伴う内容であること
    ✓ 進行状況は1であること
    ✓ 得点は0であること
    ✓ 出題された問題
    ✓ handler の response
    問題に正解した場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が問題の正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が1増えていること
    ✓ 次の問題に進んでいるので問題が変わっていること
    ✓ handler の response
    1回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が1回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題が変わらないこと
    ✓ handler の response
    2回目の不正解の場合
    ✓ 連続不正解数が増えていること
    ✓ 返答の音声内容が2回目の不正解に伴う内容であること
    ✓ 進行状況が進んでいないこと
    ✓ 得点が変わらないこと
    ✓ 問題が変わらないこと
    3回目の不正解の場合
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容が3回目の不正解に伴う内容であること
    ✓ 次の問題に進んでいるので進行状況が進んでいること
    ✓ 得点が変わらないこと
    ✓ 次の問題に進んでいるので問題が変わっていること
    最終問題に正解した場合
    ✓ shouldEndSession が true になっていること
    ✓ 連続不正解数が0に戻っていること
    ✓ 返答の音声内容がクイズ終了を知らせる内容であること
    ✓ 進行状況が変わらないこと
    ✓ 得点が1増えていること
    ✓ 問題が変わらないこと
    ✓ handler の response
    88 passing (82ms)
    QuizIntent: function () { // 初期状態
    const session = new Session();
    session.start(questions[getNextItemIndex()]);
    this.attributes['dump'] = session.dump();
    this.emit(':ask', session.message(), session.reprompt()); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    const session = new Session();
    session.restore(this.attributes['dump']);
    session.receive(usersAnswer);
    if (session.shouldRepeatSameQuestion()) {
    this.attributes['dump'] = session.dump();
    this.emit(':ask', session.message(), session.reprompt());
    } else if (session.isFinished()) { // 全ての問題が終了した場合
    this.attributes['dump'] = session.dump();
    this.emit(':tell', session.message()); // 会話を終える
    } else { // 続きの問題がある場合
    session.setNextItem(questions[getNextItemIndex()]);
    this.attributes['dump'] = session.dump();
    this.emit(':ask', session.message(), session.reprompt()); // 会話を続ける
    }
    },

    View full-size slide

  93. ·ͩ·ͩߦͧ͘

    View full-size slide

  94. ࡞ઓঢ়ଶભҠ΋Ϟσϧࣗ਎͕൑அͰ͖ΔΑ͏ʹ͢Δ
    w ঢ়ଶભҠ΋Ϟσϧࣗ਎͕൑அͰ͖ΔΑ͏ʹͯ͠ɺ-BNCEB΍
    "MFYBͱͷ઀஍໘ʢ݁߹౓ʣΛߋʹݮΒ͢
    w -BNCEBϨϕϧͷςετ͸ΦʔϧάϦʔϯͷ··૸Γଓ͚Δ
    w ϞσϧΫϥε͸ςετۦಈ։ൃͰܧଓ։ൃ͢Δ

    View full-size slide

  95. ࣍ͷ໰୊ʹਐΉ͔Ͳ͏͔΋Ϟσϧࣗ਎͕൑அ͢Δ
    class Session {
    constructor ({env}) {
    this.env = env;
    }
    start () {
    this.advance = 1;
    this.score = 0;
    this.accumIncorrects = 0;
    this.item = this.env.nextItem();
    }
    command () {
    return this.isFinished() ? ':tell' : ':ask';
    }
    receive (answer) {
    // 省略
    if (!this.shouldRepeatSameQuestion() && !this.isFinished()) { // 続きの問題がある場合
    this.item = this.env.nextItem();
    }
    }
    isFinished () {
    return this.advance === 7 && !this.shouldRepeat;
    }
    }

    View full-size slide

  96. -BNCEBଆͷ੹຿͕ݮΓɺ͔ͳΓεοΩϦ͖ͯͨ͠
    QuizIntent: function () { // 初期状態
    const session = new Session({env});
    session.start();
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    const session = new Session({env});
    session.restore(this.attributes['dump']);
    session.receive(usersAnswer);
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    現在の状態に基づく
    条件分岐がなくなった

    View full-size slide

  97. ΋͏গ͠ߦͧ͘

    View full-size slide

  98. ࡞ઓ݁߹౓Λ͞ΒʹݮΒ͢
    w ΫϥεΛެ։ͤͣɺϑΝΫτϦʔؔ਺͚ͩΛެ։͢Δ͜ͱͰ಺
    ෦ߏ଄Λ͞ΒʹӅṭ͠ɺ-BNCEB΍"MFYBͱͷ઀஍໘ʢ݁߹
    ౓ʣΛ࠷খʹ͢Δ
    w -BNCEBϨϕϧͷςετ͸ΦʔϧάϦʔϯͷ··૸Γଓ͚Δ
    w ϞσϧΫϥε͸ςετۦಈ։ൃͰܧଓ։ൃ͢Δ

    View full-size slide

  99. ந৅౓ͷߴ͍ؔ਺ͷΈΛެ։͢Δ
    const startSession = ({env}) => {
    const session = new Session({env});
    session.start();
    return session;
    };
    const restoreSession = ({env, dump}) => {
    const session = new Session({env});
    session.restore(dump);
    return session;
    };
    module.exports = {
    startSession,
    restoreSession
    };
    Session クラスの export もやめる

    View full-size slide

  100. 'use strict';
    var Alexa = require('alexa-sdk');
    var questions = require('./questions.json');
    -const {Session} = require('./models');
    +const {startSession, restoreSession} = require('./models');
    var createHandlers = function (getNextItemIndex) {
    const env = {
    @@ -13,24 +13,21 @@ var createHandlers = function (getNextItemIndex) {
    return {
    QuizIntent: function () { // 初期状態
    - const session = new Session({env});
    - session.start();
    + const session = startSession({env});
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    - const session = new Session({env});
    - session.restore(this.attributes['dump']);
    + const session = restoreSession({env, dump: this.attributes['dump']});
    session.receive(usersAnswer);
    this.attributes['dump'] = session.dump();
    ৘ใӅṭ͕ਐΉ

    View full-size slide

  101. ࣮૷ৄࡉͱͯ͠ͷ"MFYB-BNCEBͱϏδωεϩδοΫΛ෼཭Ͱ͖ͨ
    QuizIntent: function () { // 初期状態
    const session = startSession({env});
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    const usersAnswer = this.event.request.intent.slots.Answer.value;
    const session = restoreSession({env, dump: this.attributes['dump']});
    session.receive(usersAnswer);
    this.attributes['dump'] = session.dump();
    this.emit(session.command(), session.message(), session.reprompt());
    },
    👍

    View full-size slide

  102. ͻͲ͔ͬͨͱ͖ͱൺ΂ͯΈΔ
    QuizIntent: function () { // 初期状態
    this.attributes['advance'] = 1; // 進行状況を初期化
    this.attributes['score'] = 0; // 得点を初期化
    this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存
    var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`;
    var reprompt = `1番。 ${questions[random].q}`;
    this.emit(':ask', message, reprompt); // 相手の回答を待つ
    },
    AnswerIntent: function () { // ユーザからの返答により起動される
    // スロットから回答を取得
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    var currentQuestion = questions[this.attributes['itemIndex']];
    var shouldRepeatSameQuestion = false;
    var resultMessage;
    if (currentQuestion.a === usersAnswer) { // 正解の場合
    resultMessage = 'そうです。 では';
    this.attributes['score']++;
    this.attributes['accumIncorrects'] = 0;
    } else { // 不正解の場合
    this.attributes['accumIncorrects']++;
    switch (this.attributes['accumIncorrects']) {
    case 1:
    resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    case 2:
    resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${currentQuestion.q}`;
    shouldRepeatSameQuestion = true;
    break;
    default:
    resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`;
    this.attributes['accumIncorrects'] = 0;
    break;
    }
    }
    if (shouldRepeatSameQuestion) {
    var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`;
    this.emit(':ask', resultMessage, reprompt);
    } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合
    this.attributes['advance']++;
    var random = getNextItemIndex();
    this.attributes['itemIndex'] = random;
    var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`;
    this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける
    } else { // 全ての問題が終了した場合
    var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`;
    this.emit(':tell', resultMessage + endMessage); // 会話を終える
    }
    },
    ͜ɺ͜Ε͸ʜʜ😨

    View full-size slide

  103. Agenda
    ݱঢ়֬ೝ
    ಆ͏४උΛ੔͑Δ
    ػೳ௥Ճͱͦͷ݁Ռ
    ϞσϧΛ෼཭͢Δ
    ࣄ࣮ͱ৘ใΛ෼͚Δ
    ΞʔΩςΫνϟΛఆΊΔ
    👉
    103/118

    View full-size slide

  104. ͜͜Ͱ࢓༷௥ՃͰ͢
    w ऴྃͨ͠ϒϩοΫͷޡ౴܏޲Λݟͯ࣍ͷϒϩοΫͷ࣭໰άϧʔϓΛܾΊ͍ͨ
    w ޡ౴܏޲ͰۤखάϧʔϓΛ൑ఆ͠ɺΫΠζऴྃ࣌ʹϝοηʔδʹ൓ө͍ͨ͠
    w ޡ౴਺ͰϨϕϧ൑ఆ͍ͨ͠
    w ฏۉճ౴ॴཁ࣌ؒͰϨϕϧ൑ఆ͍ͨ͠
    w ޡ౴਺ɺฏۉճ౴ॴཁ࣌ؒΛڞʹධՁ͠ɺ௿͍ํͷϨϕϧΛ࠾༻͍ͨ͠
    υυοͱདྷ·ͨ͠ͳ
    &&Ͱ͸ςετ͠ʹ͍͘ཁ݅ͳͷͰɺ͜͜·ͰͰ
    ϢχοτςετՄೳͳͭ͘Γʹ͓͍ͯͯ͠Α͔ͬͨ

    View full-size slide

  105. ৘ใͱσʔλͷҧ͍Λҙࣝ͢Δ
    IUUQTXXXPSFJMMZDPKQCPPLT
    ͯ͞ɺօ͞Μ͸ʮ৘ใʯͱʮσʔλʯͷҧ͍Λ͝ଘ஌Ͱ͠ΐ͏͔ɻ
    զʑ͕ཉ͍͠ͷ͸ɺҙຯͷ͋Δʢ໨తΛ࣋ͬͨʣਖ਼͍͠৘ใͳͷͰ͢ɻҰํɺσʔλ
    ͸୯ͳΔ֤छͷࣄ࣮ͷ஋ʢԿΒ͔ͷɺ໊শͱ͔೔෇ͱֹ͔ۚͱ͔ʣͰ͋ͬͯͦΕࣗମ
    ʹ໨త͸͋Γ·ͤΜɻ
    ໨తΛ࣋ͬͨ৘ใ͸ɺແ໨తͳࣄ࣮Λूੵͨ͠σʔλΛछʑՃ޻ͯ͠ಘΒΕ·͢ɻ
    σʔλ͸།Ұແೋͷࣄ࣮஋Ͱ͔͢ΒɺͦΕ͔Β࡞Γग़͞ΕΔ৘ใ͸ͲΕ΋͕ਖ਼͘͠ɺ
    ޓ͍ʹ੔߹ੑ͕ͱΕ͍ͯ·͢ɻ
    ᴷ࿨ాলೋ
    ʰ42-Ξϯνύλʔϯʱ؂༁ऀલॻ͖

    View full-size slide

  106. ͜Ε͸ࣄ࣮ͩΖ͏͔ɺ৘ใͩΖ͏͔ɺͦΕΒ͕͍ࠞͬͯ͟ΔͩΖ͏͔
    dump: {
    advance: 7,
    score: 5,
    accumIncorrects: 0,
    item: {
    a: '盛岡',
    g: 'PrefecturalOfficeLocation',
    q: '岩手県の県庁所在地は?'
    }
    }

    View full-size slide

  107. ࣄ࣮ΛϞσϦϯά͠ɺߏ଄ʹ൓ө͢Δ
    Block
    Session
    Question
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Attempt
    Question
    Question
    Block
    Block

    View full-size slide

  108. "dump": {
    "startedAt": 1540553739000,
    "finishedAt": null,
    "blocks": [
    {
    "startedAt": 1540553739000,
    "finishedAt": 1540553825000,
    "genres": [
    "PrefectureFlower",
    "PrefecturalOfficeLocation",
    "Romanization"
    ],
    "questions": [
    {
    "item": {
    "q": "山形県の県の花は?",
    "a": "べにばな",
    "g": "PrefectureFlower"
    },
    "startedAt": 1540553752000,
    "finishedAt": 1540553778000,
    "result": "correct",
    "attempts": [
    {
    "startedAt": 1540553752000,
    "finishedAt": 1540553767000,
    "userAnswer": "サクラ",
    "result": "incorrect"
    },
    {
    "startedAt": 1540553767000,
    "finishedAt": 1540553778000,
    "userAnswer": "べにばな",
    "result": "correct"
    }
    ࣄ࣮Λ֨ೲ͠ɺ৘ใ͸͔ͦ͜Βܭࢉ͢Δํ͕҆ఆ͢Δ
    w ޡ౴܏޲ͰۤखάϧʔϓΛ൑ఆ͍ͨ͠
    w ޡ౴਺ͰϨϕϧ൑ఆ͍ͨ͠
    w ฏۉճ౴ॴཁ࣌ؒͰϨϕϧ൑ఆ͍ͨ͠
    ޡ౴܏޲ɺޡ౴਺ɺฏۉճ౴ॴཁ࣌ؒ͸ɺ
    ͢΂ͯʮࣄ࣮ʢσʔλʣʯ͔Βࢉग़Ͱ͖Δʮ৘ใʯ

    View full-size slide

  109. Agenda
    ݱঢ়֬ೝ
    ಆ͏४උΛ੔͑Δ
    ػೳ௥Ճͱͦͷ݁Ռ
    ϞσϧΛ෼཭͢Δ
    ࣄ࣮ͱ৘ใΛ෼͚Δ
    ΞʔΩςΫνϟΛఆΊΔ
    👉
    109/118

    View full-size slide

  110. ͜͜Ͱ͞ΒͳΔཁ๬Ͱ͢
    ਖ਼௚དྷΔͱࢥ͍ͬͯͨ͜Ε͸
    w "ࣾͷεϚʔτεϐʔΧʔ͚ͩͰͳ͘ɺ(ࣾͷεϚʔτεϐʔ
    ΧʔͰ΋ݕূΛߦ͍͍ͨ

    View full-size slide

  111. ઃܭͷݪଇ
    IUUQTXXXBNB[PODPKQEQ
    w ୯Ұ੹೚ͷݪଇʢΫϥεϨϕϧʣɺด࠯ੑڞ௨ͷݪଇʢίϯϙʔωϯτϨϕϧʣ
    w ಉ͡λΠϛϯάɺಉ͡ཧ༝Ͱมߋ͢Δ΋ͷ͸ͻͱ·ͱΊʹ͢Δ͜ͱɻมߋͷ
    λΠϛϯά΍ཧ༝͕ҟͳΔ΋ͷ͸ผʑʹ෼͚Δ͜ͱ
    w ҆ఆґଘͷݪଇ
    w ҆ఆ౓ͷߴ͍ํ޲ʹґଘ͢Δ͜ͱ

    View full-size slide

  112. ॻ੶ʰ$MFBO"SDIJUFDUVSFʱʹग़ͯ͘Δ
    ʢޡղ͞Ε͕ͪͳʣಉ৺ԁΛྫʹͯ͠ߟ͑ͯΈΔ

    View full-size slide

  113. ֤ࣾεϚʔτεϐʔΧʔ޲͚ͷ࣮૷͸
    ʮৄࡉʯͩͱ͍͑Δ

    View full-size slide

  114. ΫΠζͷ
    ίΞυϝΠϯ
    ֤εϚʔτεϐʔΧʔ
    ͱͷΞμϓλ૚
    ΫΠζͷίΞυϝΠϯ͸
    εϚʔτεϐʔΧʔͷৄࡉʹඇґଘ
    Ξμϓλ૚͸֤ࣾεϚʔτεϐʔΧʔͱ
    ίΞυϝΠϯͱͷ઀ଓʹͱͲΊΔ

    View full-size slide

  115. 事実を扱う
    情報を扱う
    基盤を扱う
    永続化を扱う
    ࣄ࣮͔ΒܭࢉͰ͖Δಘ఺౳΍
    76*ͱͯ͠ͷϝοηʔδͳͲΛѻ͏
    ࠷ऴతͳΞʔΩςΫνϟ

    View full-size slide

  116. IUUQTXXXPINTIBDPKQCPPL
    5%%͸ɺઃܭͷͻΒΊ͖͕ਖ਼͍͠ॠؒʹ๚ΕΔ͜ͱΛอূ͢Δ΋ͷͰ͸ͳ͍ɻ
    ͔͠͠ɺࣗ৴Λ༩͑ͯ͘ΕΔςετͱ͖ͪΜͱखೖΕ͞Εͨίʔυ͸ɺͻΒΊ
    ͖΁ͷඋ͑Ͱ͋Γɺ͍͟ͻΒΊ͍ͨͱ͖ʹɺͦΕΛ۩ݱԽ͢ΔͨΊͷඋ͑Ͱ΋
    ͋Δɻ
    ᴷ,FOU#FDL
    ʰςετۦಈ։ൃʱୈষ
    ,FOU#FDLᐌ͘

    View full-size slide

  117. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠
    w ٕज़ͷຊபʢόʔδϣϯ؅ཧɺςεςΟϯάɺࣗಈԽʣʹ͸༏ઌ౓͕͋Δ
    w ࣗಈςετΛॻ͍͚ͨͩͰ͸࣭͸্͕Βͳ͍ɻ࣭Λ্͛Δͷ͸ϓϩάϥϛϯά
    w ʢՄೳͰ͋Ε͹ʣϦϑΝΫλϦϯά΁ͷ଱ੑΛ࣋ͪͭͭ҆ఆͯ͠ςετͰ͖ΔϙΠϯτΛ୳͠ɺͦΕΛલઢج஍ͱ͢Δ
    w طଘίʔυΛςετͰे෼อޢͰ͖Δ͔Ͳ͏͔Ͱ&YUSBDUઓज़͔4QSPVUઓज़͔ΛܾΊΔ
    w ࢓༷͕ݻ·Βͳ͍͔Βςετ͕ॻ͚ͳ͍ͷͰ͸ͳ͘ɺ࢓༷͕ݻ·Βͳ͍͔Βͦ͜ςετΛॻ͍ͯมԽΛࢧ͍͑ͯ͘
    w ࣗಈςετΛॻ͖ͳ͕ΒυϝΠϯϞσϧΛநग़͢Δ
    w ίΞυϝΠϯΛಠཱͯࣗ͠ಈςετͰ͖ΔΑ͏ʹɺٕज़తͳৄࡉͱͷ݁߹౓Λஈ֊తʹݮΒ͍ͯ͘͠
    w ࣄ࣮ΛϞσϦϯά͠ɺ৘ใΛ͔ͦ͜ΒऔΓग़͢
    w ίΞυϝΠϯͱٕज़ৄࡉΛ෼཭͠ɺίΞυϝΠϯΛҭ͍͚ͯͯΔΞʔΩςΫνϟΛఆΊΔ
    w ʮΫϦʔϯΞʔΩςΫνϟΈ͍ͨͳ΍ͭʯ͸࠷ॳ͔Β໨ࢦ͢ͷͰ͸ͳͯ͘ɺݪཧݪଇϕʔεͰϦϑΝΫλϦϯά͍ͯ͘͠ͱ࣍
    ୈʹ͍͍ۙͮͯ͘΋ͷ

    View full-size slide