【YAPC::Asia Hachioji 2016】ES2015のclassでアプリケーションを書いてみた話

Preview:

Citation preview

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