【YAPC::Asia Hachioji 2016】ES2015のclassでアプリケーションを書いてみた話
-
Upload
hiroyuki-kusu -
Category
Technology
-
view
426 -
download
0
Transcript of 【YAPC::Asia Hachioji 2016】ES2015のclassでアプリケーションを書いてみた話
ES2015 の class でアプリケーションを書いてみた話
Hiroyuki Kusu ( @hkusu_ )
YAPC::Asia Hachioji 2016 mid in Shinagawa7/3 LT
■ 〜 2015年 12月・ Android アプリの開発
■ 2016年 1月〜・ JavaScript アプリケーションの開発
Javaに慣れ親む
わりと Java っぽいクラスベースのオブジェクト志向でいけた!
(あくまで「ぽい」)
ES2015
■ ES2015 で書く・ Babel (トランスパイラ )・ Browserify (ブラウザ用の場合 )
■ エディタ・WebStorm (JetBrains 社製 IDE)・ like Android Studio
前提となる環境
class
class Person { constructor(name, age) { this.name = name this.age = age }
getName() { return this.name }
getAge() { return this.age }
hello() { return ` こんにちは! ${this.name} さん ` }}
export default Person
Person.js
class Person { constructor(name, age) { this.name = name this.age = age }
getName() { return this.name }
getAge() { return this.age }
hello() { return ` こんにちは! ${this.name} さん ` }}
export default Person
Person.js
コンストラクタ
class Person { constructor(name, age) { this.name = name this.age = age }
getName() { return this.name }
getAge() { return this.age }
hello() { return ` こんにちは! ${this.name} さん ` }}
export default Person
Person.js
インスタンス変数
class Person { constructor(name, age) { this.name = name this.age = age }
getName() { return this.name }
getAge() { return this.age }
hello() { return ` こんにちは! ${this.name} さん ` }}
export default Person
Person.js
インスタンスメソッド
class SomeUtil {
static isObject(arg) { return typeof arg === 'object' && arg !== null && !Array.isArray(arg); }
}
export default SomeUtil
SomeUtil.js
クラスメソッド
import Person from './Person'
class Men extends Person { hello() { return ` おす! ${this.name} さん ` }}
export default Men
Men.js
継承
1ファイル、1クラス(原則)
import Person from './Person'import SomeUtil from './SomeUtil'
// …
const person = new Person('山田 ', 45)
クラスをインポートして利用
列挙型(ぽいもの )
const Week = { SUN: Symbol(), MON: Symbol(), TUE: Symbol(), WED: Symbol(), THU: Symbol(), FRI: Symbol(), SAT: Symbol(),}
export default Week
import Week from './Week'
// …
const myWeek = Week.SUN
Week.js
利用例
const Action = { SEARCH: Symbol(), REGISTER: Symbol(), UPDATE: Symbol(), DELETE: Symbol(),}
export default Action
Action.js
キーとして利用(例えば Redux などで)
Singleton
import axios from 'axios'import { config } from './../config/Config'
class QiitaApiService { constructor(config) { this.baseUrl = config.QIITA_BASE_URL }
search(searchWord, perPage = 10) { return this.httpGet(`search?q=${searchWord}&per_page=${perPage}`) }
httpGet(query) { return axios.get(`${this.baseUrl}/${query}`) }}
export default QiitaApiService
export const qiitaApiService = new QiitaApiService(config)
QiitaApiService.js
インスタンス化したものを export
import { qiitaApiService } from './service/QiitaApiService'
// …
qiitaApiService.search('JavaScript', 10) .then(() => { // ... })
アプリケーション内でインスタンスが共有される
アクセス修飾子(private / protected)
with WebStorm& JSDoc
import axios from 'axios'import { config } from './../config/Config'
class QiitaApiService { constructor(config) { /** @private */ this.baseUrl = config.QIITA_BASE_URL }
search(searchWord, perPage = 10) { return this.httpGet(`search?q=${searchWord}&per_page=${perPage}`) }
/** * @private */ httpGet(query) { return axios.get(`${this.baseUrl}/${query}`) }}
// …
QiitaApiService.js
private変数
privateメソッド
型チェック
with WebStorm& JSDoc
// …class QiitaApiService { /** * @constructor * @param {Config|SpecConfig} config */ constructor(config) { /** @private */ this.baseUrl = config.QIITA_BASE_URL }
/** * @param {string} searchWord * @param {number} [perPage=10] * @returns {promise} */ search(searchWord, perPage = 10) { return this.httpGet(`search?q=${searchWord}&per_page=${perPage}`) }
/** * @param {string} query * @returns {promise} * @private */ httpGet(query) { return axios.get(`${this.baseUrl}/${query}`) }}// …
/** @type {QiitaApiService} */export const qiitaApiService = new QiitaApiService(config)
型が迷子になったら @type で指定
Test
Repository
層
Service
層
Dependency injection
Config利用 利用 利用
クラス内で出来るだけ別クラスを new() しない
static な状態の保持は可能な限り避ける
import { config } from './../config/Config'import ItemRepository from './../repository/ItemRepository'import QiitaApiService from './../service/QiitaApiService'
const itemRepository = new ItemRepository(new QiitaApiService(config))
Dependency injection
// ...
describe('ItemRepository', () => { let itemRepository let qiitaApiService
before(() => { qiitaApiService = new QiitaApiService(config) itemRepository = new ItemRepository(qiitaApiService) });
describe('#getItemByWord', () => { let qiitaApiServiceSearchStub
before(() => { qiitaApiServiceSearchStub = sinon.stub(qiitaApiService, 'search') qiitaApiServiceSearchStub.resolves({ result: 'success' }) });
after(() => { qiitaApiServiceSearchStub.restore() });
it('be fulfilled', (done) => { expect(itemRepository.getItemByWord('abc', 99)).to.be.fulfilled .then((result) => { expect(qiitaApiServiceSearchStub).to.have.been.calledWith('abc', 99) expect(result).to.eql({ result: 'success' }) }) .then(done, done) }) })})
※Mocha、 Chai、 Sinon.JS および Promise系のライブラリを利用
class 単位でテスト
// ...
describe('ItemRepository', () => { let itemRepository let qiitaApiService
before(() => { qiitaApiService = new QiitaApiService(config) itemRepository = new ItemRepository(qiitaApiService) });
describe('#getItemByWord', () => { let qiitaApiServiceSearchStub
before(() => { qiitaApiServiceSearchStub = sinon.stub(qiitaApiService, 'search') qiitaApiServiceSearchStub.resolves({ result: 'success' }) });
after(() => { qiitaApiServiceSearchStub.restore() });
it('be fulfilled', (done) => { expect(itemRepository.getItemByWord('abc', 99)).to.be.fulfilled .then((result) => { expect(qiitaApiServiceSearchStub).to.have.been.calledWith('abc', 99) expect(result).to.eql({ result: 'success' }) }) .then(done, done) }) })})
メソッドのテスト
// ...
describe('ItemRepository', () => { let itemRepository let qiitaApiService
before(() => { qiitaApiService = new QiitaApiService(config) itemRepository = new ItemRepository(qiitaApiService) });
describe('#getItemByWord', () => { let qiitaApiServiceSearchStub
before(() => { qiitaApiServiceSearchStub = sinon.stub(qiitaApiService, 'search') qiitaApiServiceSearchStub.resolves({ result: 'success' }) });
after(() => { qiitaApiServiceSearchStub.restore() });
it('be fulfilled', (done) => { expect(itemRepository.getItemByWord('abc', 99)).to.be.fulfilled .then((result) => { expect(qiitaApiServiceSearchStub).to.have.been.calledWith('abc', 99) expect(result).to.eql({ result: 'success' }) }) .then(done, done) }) })})
テスト対象のインスタンスの組み立て
(必要に応じてテスト用のものと差し替え )
// ...
describe('ItemRepository', () => { let itemRepository let qiitaApiService
before(() => { qiitaApiService = new QiitaApiService(config) itemRepository = new ItemRepository(qiitaApiService) });
describe('#getItemByWord', () => { let qiitaApiServiceSearchStub
before(() => { qiitaApiServiceSearchStub = sinon.stub(qiitaApiService, 'search') qiitaApiServiceSearchStub.resolves({ result: 'success' }) });
after(() => { qiitaApiServiceSearchStub.restore() });
it('be fulfilled', (done) => { expect(itemRepository.getItemByWord('abc', 99)).to.be.fulfilled .then((result) => { expect(qiitaApiServiceSearchStub).to.have.been.calledWith('abc', 99) expect(result).to.eql({ result: 'success' }) }) .then(done, done) }) })})
必要に応じてスタブを用意
ほか
■ 静的解析・ ESLint を利用
・ Airbnb の規約がおすすめ・WebStorm と連携しておく・ JSDoc 漏れを検査させるとよ
い
■ ドキュメント生成・ ESDoc を利用
・テストコードとも連動できる
■ HTTP通信ライブラリ・ axios .. Promise に対応
まとめ
■ ES2015で普通にクラスベースのオブジェ クト志向でアプリケーションが書ける ようになった
■ IDE(WebStorm)でクラス含む型のサポート もある程度うけられる ⇒機能はできるだけ class で表現する
.. ちゃんとやるなら TypeScript がいいと思う
Sample codehkusu/react-app-example
※React 周りのコードも含んじゃってます
END