事の発端
始まりはこちらのツイートから。
Usecasesレイヤーを充実させていったらVuex Actionsほとんど使わなくなるな笑
— Andy (@andoshin11) 2018年6月15日
それはどういうことだよ・・・
フロントでどう使うんだ・・・?
と疑問に思い、自分なりに検証・実装してみたいと思ったのが事の発端です。
Clean Architectureとは?
まず根本の理解がほぼなかったので調べることにしました。
こことか
https://qiita.com/koutalou/items/07a4f9cf51a2d13e4cdc
こことか
https://blog.tai2.net/the_clean_architecture.html
こことか
https://qiita.com/Tueno@github/items/705360b357c2a00c9532
こことか
https://github.com/apavamontri/nodejs-clean
こことか
http://techblog.reraku.co.jp/entry/2017/08/08/184313
https://www.youtube.com/watch?v=fWVE1fH0MVE
https://github.com/Shinpeim/NekogataDrumSequencer
こことか
http://azu.github.io/slide/2016/child_process_sushi/almin-javascript-architecture.html
https://github.com/almin/almin
https://github.com/azu/large-scale-javascript
いろいろ漁り弄りました。
がしかし、なんだかふわっとした、雲を掴んでいるような感覚で、
理解には及びませんでした。。
(記載させていただいたページはどれもわかりやすく書かれていると思います!)
これは実装・検証が必要だ。。。
全体の構造
全体の構造は
https://github.com/almin/almin
のexamplesをめちゃくちゃ参考にさせていただきました。
プログラム全体を以下のリストに分割して考えることにします。
※ かなり個人的な見解を含むと思います。
- Domain
- プレーンなClassで、扱いたい ”もの” をわかりやすいプロパティとメソッドで書く
- 可能な限りライブラリ等は使わない
- Infrastructure
- 環境依存なものを書く
- APIなど外部との連携を書く
- Repositoryから呼び出される
- Repository
- インスタンス化されたDomainを永続化させる(保存先はInfrastructureを使ってサーバだったりメモリだったり)
- シングルトン
- Infrastructureの存在を知っている
- UseCase
- Presenterから呼び出される
- ちょっとControllerっぽい動き(極力処理の流れのみを書く感じ)
- RepositoryからDomainを受け取ってDomainのメソッドを使ったり
- Presenter
- いわゆるUI実装(スタイル等も込み)
- 本記事だとVue・React
本記事の目標
本記事では、Clean Architectureを用いて、Presenterの入れ替え(つまりVueとReactを入れ替え)を行い、Presenterがビジネスロジックに依存しないで実装できかの検証とClean Architectureがフロントエンドでどんな実装になるのかを考えます。
個人的にはClean Architectureを理解することも目標としています。
何を作ったか
できる限り簡素な実装を行いたいので、ボタンをクリックしたら値が増減するだけのカウンターをサンプルに実装します。
増減した値はローカルストレージに保存して永続化します。
書いたコードは全てCodePenで公開します。
↓できたもの
- Domain / Infrastructure / Repository / UseCase
https://codepen.io/RikutoYamaguchi/pen/pKmZaY - Presenter (Vue.js)
https://codepen.io/RikutoYamaguchi/pen/MXdQOV - Presenter (React)
https://codepen.io/RikutoYamaguchi/pen/zaQaPJ
Presenterでは「Domain / Infrastructure / Repository / UseCase」をExternal Penとして追加し、
Vue.jsとReactで同じものを利用しています。
↓使用ライブラリ等
- https://cdnjs.cloudflare.com/ajax/libs/EventEmitter/5.2.5/EventEmitter.js
- https://cdnjs.cloudflare.com/ajax/libs/localforage/1.7.2/localforage.js
- https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.29/browser-polyfill.min.js
- https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js
- https://unpkg.com/react/umd/react.development.js
- https://unpkg.com/react-dom/umd/react-dom.development.js
Domainの実装
まずは、アプリケーションの基盤となるDomainを実装します。
今回はカウンターをプレーンはClassで表してみました。
- Domain
- プレーンなClassで、扱いたい ”もの” をわかりやすいプロパティとメソッドで書く
- 可能な限りライブラリ等は使わない
class Counter {
/**
* @param {Number} count
*/
constructor(count = 0) {
this.count = count;
}
countUp() {
this.count++;
return this._update(this);
}
countDown() {
this.count--;
return this._update(this);
}
/**
* 更新した新しいインスタンスを返す
* @param {Counter} updated
*/
_update(updated) {
return new Counter(updated.count);
}
}
-
count
というプロパティを持つ -
countUp
というcount
を1足すメソッドを持つ -
countDown
というcount
を1引くメソッドを持つ -
_update
という新しいインスタンスを返すメソッドを持つ
こう見ると非常にシンプルなClassが出来上がりました。
またライブラリ等は使用していないプレーンな状態となりました。
ついでにですが、このCounter
Domainをインスタンス化するにあたり、
Factoryも作りました。
class CounterFactory {
static createFromLocalStorage(counter) {
let count = 0;
if (!counter) {
return new Counter(count);
}
if (typeof counter.count !== 'number') {
count = 0;
} else {
count = counter.count;
}
return new Counter(count);
}
}
例えばですが、DomainをJSONから作る必要がある場合など、
Factoryからインスタンスを生成するようにすれば、
Domain内でデータ変換等をする必要がなくなります。
(→ つまりDomainではJSONの仕様を知る必要がなくなる。 = 依存しない)
今回はローカルストレージにデータを保存しようと思うので、
ローカルストレージのデータからCounter
Domainを作るメソッドを作成しました。
Infrastructureの実装
次に環境依存なInfrastructureを実装しました。
今回はローカルストレージへの書き込み、読み出しの実装となります。
- Infrastructure
- 環境依存なものを書く
- APIなど外部との連携を書く
- Repositoryから呼び出される
class LocalStorage {
/**
* ローカストレージから値を取得する
* @param {String} name
* @return {Promise}
*/
get (name) {
return localforage.getItem(name);
}
/**
* ローカストレージへ値を入れる
* @param {String} name
* @param {*} value
* @return {Promise}
*/
set (name, value) {
return localforage.setItem(name, value);
}
}
ここではlocalforage
に依存して実装を行っている事がわかります。
これは、Web API
だったりDOM API
だったりしても同じようにInfrastructureに実装します。
Repositoryの実装
続いてはRepositoryの実装です。
- Repository
- インスタンス化されたDomainを永続化させる(保存先はInfrastructureを使ってサーバだったりメモリだったり)
- シングルトン
- Infrastructureの存在を知っている
const REPOSITORY_CHANGE = 'REPOSITORY_CHANGE'; // Repository内部のemit名
class CounterRepository extends EventEmitter {
/**
* @param {Map} memory
* @param {LocalStorage} localStorage
*/
constructor(memory = new Map(), localStorage = new LocalStorage()) {
super();
this._memory = memory;
this._localStorage = localStorage;
}
/**
* ローカルストレージに格納されている
* カウンターを取り出す
*/
async getLocalStorageData () {
return await this._localStorage.get('counter');
}
/**
* メモリ上に永続化されたカウンタードメインを返す
*/
getCounter () {
return this._memory.get('counter')
}
/**
* カウンタードメイン永続化
* @param {Counter} counter
*/
save (counter) {
this._memory.set('counter', counter);
this._localStorage.set('counter', counter);
this.emit(REPOSITORY_CHANGE, counter);
}
/**
* emitterへハンドラ設定
* @param {Function} handler
*/
onChange(handler) {
this.on(REPOSITORY_CHANGE, handler);
}
}
// リポシトリーのシングルトン
const counterRepository = new CounterRepository();
CounterRepository
ではメモリ上に永続化させるためのMap
と、
先ほどInfrastructureで作成したLocalStorage
を引数に、
EventEmitterを親クラスとしたシングルトンとします。
今回の実装はとても簡単なので問題にならないですが、
複雑になってくると、UseCaseとRepositoryどっちに実装しよう。。。と迷うことがありそうです。
RepositoryはInfrastructureへの参照を利用する、保持するという役割以外のことは極力さけるほうが良さそうです。
また、RepositoryはDomainを永続化させる際(save
メソッドが実行されたら)、
自身のemitを実行し、その引数には永続化されたDomainを代入します。
onChange
メソッドはPresenterから更新時のハンドラを登録するために実装してあります。
UseCaseの実装
Presenter以外の実装はこれで最後です。
- UseCase
- Presenterから呼び出される
- ちょっとControllerっぽい動き(極力処理の流れのみを書く感じ)
- RepositoryからDomainを受け取ってDomainのメソッドを使ったり
// UseCase
class CounterUseCase {
/**
* @param {CounterRepository} counterRepository
*/
constructor(counterRepository) {
this.counterRepository = counterRepository;
}
/**
* CounterUseCaseを作って返す
*/
static create() {
return new CounterUseCase(counterRepository);
}
/**
* ローカルストレージから値を取得して、初期化する
* 1.ローカルストレージから値を取得する
* 2.取得した値を使ってドメインを作成する
* 3.作成したドメインを永続化する
*/
async initialize() {
const value = await this.counterRepository.getLocalStorageData();
const counter = CounterFactory.createFromLocalStorage(value);
this.counterRepository.save(counter);
}
/**
* 値を一つ足す
* 1.Repositoryから永続化されたドメインを取得する
* 2.ドメインのcountUpメソッドを呼び出し、更新したドメインを取得する
* 3.更新したドメインを永続化する
*/
countUp() {
let counter = this.counterRepository.getCounter()
counter = counter.countUp();
this.counterRepository.save(counter);
}
/**
* 値を一つ引く
* 1.Repositoryから永続化されたドメインを取得する
* 2.ドメインのcountDownメソッドを呼び出し、更新したドメインを取得する
* 3.更新したドメインを永続化する
*/
countDown() {
let counter = this.counterRepository.getCounter()
counter = counter.countDown();
this.counterRepository.save(counter);
}
}
UseCaseではRepositoryから永続化されたDomainのインスタンスを受け取り、
そのインスタンスやRepositoryを使った処理の流れ自体を記述していきます。
UseCaseは実際に必要な時のみ存在するよう、
static create
メソッドで自身のインスタンスを返す実装にしました。
Domain / Infrastructure / Repository / UseCaseまでの実装
See the Pen FrontEnd Clean Architecture Base by Rikuto Yamaguchi (@RikutoYamaguchi) on CodePen.
Presenter (Vue.js)
さてここからがUI部分の実装に入ります。
まずはVue.jsから。
const VuePresenter = Vue.component('VuePresenter', {
template: `
<div>
<p>カウント:{{ count }}</p>
<button @click="onClickCountUp">countUp</button>
<button @click="onClickCountDown">countDown</button>
</div>
`,
data () {
return {
count: null
}
},
beforeCreate() {
// Domainを初期化
CounterUseCase.create().initialize();
},
created() {
// Repositoryを購読
counterRepository.onChange(counter => {
this.count = counter.count;
})
},
methods: {
onClickCountUp() {
CounterUseCase.create().countUp();
},
onClickCountDown() {
CounterUseCase.create().countDown();
}
}
})
// render
new Vue({
el: '#vue-app',
template: '<VuePresenter />'
})
beforeCreated
でまずDomainを初期化します。
ここで重要なのはPresenter側では処理の流れ自体は何も知らないということです。
created
ではRepositoryの購読を行います。
onChangeのコールバックでは、引数で渡されるDomainを使って、
ローカルデータの更新を行います。
※ PresenterがDomainの実装を知っている状態になるので、本当は一つ経由するもの(Presenterが使用するデータの形に変換するもの)を用意したほうがよいかもしれません。
methods
では各ボタンが押された際の、UseCaseの呼び出しを行います。
See the Pen FrontEnd Clean Architecture (Vue.js) by Rikuto Yamaguchi (@RikutoYamaguchi) on CodePen.
Presenter (React)
次にReactで実装してみました。
class ReactPresenter extends React.Component {
constructor() {
super();
this.state = {
count: null
};
}
componentWillMount() {
// Domainを初期化
CounterUseCase.create().initialize();
// Repositoryを購読
counterRepository.onChange(counter => {
this.setState({
count: counter.count
})
})
}
onClickCountUp() {
CounterUseCase.create().countUp();
}
onClickCountDown() {
CounterUseCase.create().countDown();
}
render () {
return (
<div>
<p>カウント:{ this.state.count }</p>
<button onClick={() => this.onClickCountUp()}>countUp</button>
<button onClick={() => this.onClickCountDown()}>countDown</button>
</div>
)
}
}
// render
ReactDOM.render(
<ReactPresenter />,
document.getElementById('react-app')
);
やっていることはVue.jsのときと一緒です。
componentWillMount
でDomainの初期化とRepositoryの購読を行っています。
See the Pen FrontEnd Clean Architecture (React) by Rikuto Yamaguchi (@RikutoYamaguchi) on CodePen.
まとめ・思ったこと
ひとまずは、Presenterの入れ替え(Vue.jsとReact)を行うことはできました。
これの意味するところはPresenterの修正はビジネスロジック部分へ全く影響を及ぼさないということになるかと思います。
ただ、まだこの実装がClean Architectureといえるのか・・・?ふわっとした感覚で、実感はない状態です。
(依存関係は外側に向かっている用に見えるけど・・・うーーん。。。)
もっと複雑な実装を行った時どうなるんだろう・・・とまだまだわからないことだらけです。
みなさんのご意見・ご指摘お待ちしております!!!