Vue.js
×
 CleanArchitecture

国定凌太@92thunder

テックタッチ株式会社

フロントエンドエンジニア/テックリード

体が固すぎて25才にして腰痛持ち
最近は顎がよく外れる
 

なぜクリーンアーキテクチャをやるか

  • 5年先も通用するソフトウェアを作りたい
     

  • より少ない人員でスピードを維持しながらの
    機能追加・仕様変更に耐える
     

  • チーム全員で理解しやすいレイヤー構造
     

  • 特定フレームワークに縛られたくない

作ったもの

超シンプルなTODOアプリ
 

  • TypeScript
  • Vue.js (vue-cli3)
  • Vuex (vuex-smart-module)

Interface
Adapters

Frameworks
& Drivers

Usecases

Entities

UI (Vue)

Store(Vuex)

Controller

Domain

Interactor

Domain

Storage

API

api

repository

Interface
Adapters

Frameworks
& Drivers

Usecases

Entities

UI (Vue)

Store(Vuex)

Controller

Domain

Interactor

Domain

Storage

API

api

repository

UI(vue) → Controller
           Store

class getters extends Getters<state> {
  get tasks() {
    return this.state.tasks
  }
}
<script lang="ts">
import Vue from 'vue'
import Task from '../entities/Task'
import { controllersMapper } from '@/store/modules/controllers'
import { taskMapper } from '@/store/modules/domain/task'

export default Vue.extend({
  name: 'Todo',
  data: (): { text: string } => ({
    text: ''
  }),

  computed: taskMapper.mapGetters(['tasks']),

  methods: {
    ...controllersMapper.mapActions(['addTask', 'complete']),

    add() {
      this.addTask(this.text)
      this.text = ''
    },
    close(index: number) {
      this.complete(index)
    },
    isCompleted(task: Task) {
      return task.state !== 'DONE'
    }
  }
})
</script>
class actions extends Actions<state, getters, mutations> {
  taskInteractor!: TaskInteractor

  $init(store: Store<any>) {
    this.taskInteractor = new TaskInteractor(task.context(store))
  }

  addTask(title: string) {
    if (!title) {
      return
    }

    const task: Task = {
      title,
      description: '',
      state: 'TODO'
    }
    this.taskInteractor.addTask(task)
  }

  complete(index: number) {
    this.taskInteractor.complete(index)
  }
}

Controller → Usecase

export class TaskInteractor {
  store: TaskStoreModule

  constructor(store: TaskStoreModule) {
    this.store = store
  }

  addTask(task: Task) {
    // store
    this.store.actions.add(task)
  }

  complete(index: number) {
    const task = this.store.state.tasks[index]
    this.store.actions.update(
      {
        index,
        task: setState(task, 'DONE')
      }
    )
  }
}
class actions extends Actions<state, getters, mutations> {
  taskInteractor!: TaskInteractor

  $init(store: Store<any>) {
    // ControllerにTaskInteractorをInject
    this.taskInteractor = new TaskInteractor(task.context(store))
  }

  addTask(title: string) {
    if (!title) {
      return
    }

    const task: Task = {
      title,
      description: '',
      state: 'TODO'
    }
    this.taskInteractor.addTask(task)
  }

  complete(index: number) {
    this.taskInteractor.complete(index)
  }
}

export const controllers = new Module({
  actions
})

export const controllersMapper = createMapper(controllers

Store → Usecase

export interface TaskStoreState {
  tasks: Task[]
}
export interface TaskStoreActions {
  add: (task: Task) => void
  update: ({ index, task }: { index: number; task: Task }) => void
}

interface TaskStoreModule {
  state: TaskStoreState
  actions: TaskStoreActions
}

export class TaskInteractor {
  store: TaskStoreModule // storeの抽象に依存

  constructor(store: TaskStoreModule) {
    this.store = store
  }

  addTask(task: Task) {
    // store
    this.store.actions.add(task)

    // repository
    // this.repository.save(tasks)
  }

  complete(index: number) {}
}
// vuex-smart-moduleを使用
class state implements TaskStoreState {
  tasks: Task[] = []
}

class getters extends Getters<state> {
  get tasks() {
    return this.state.tasks
  }
}

class mutations extends Mutations<state> {
  updateTasks(tasks: Task[]) {
    this.state.tasks = tasks
  }

  updateTask({ index, task }: { index: number; task: Task }) {
    this.state.tasks.splice(index, 1, task)
  }
}

class actions extends Actions<state, getters, mutations>
  implements TaskStoreActions {
  add(task: Task) {
    this.commit('updateTasks', this.state.tasks.concat(task))
  }
  update({ index, task }: { index: number; task: Task }) {
    this.commit('updateTask', {
      index,
      task
    })
  }
}

export const task = new Module({
  state,
  mutations,
  actions,
  getters
})

export const taskMapper = createMapper(task)

Usecase → Domain

export default interface Task {
  title: string
  description: string
  state: TaskState
}

export function setState(task: Task, taskState: TaskState): Task {
  return {
    ...task,
    ...{ state: taskState }
  }
}




// titleやdescriptionの文字数制限などのvalidation
export class TaskInteractor {
  store: TaskStoreModule

  constructor(store: TaskStoreModule) {
    this.store = store
  }

  addTask(task: Task) {
    // store
    this.store.actions.add(task)

    // repository
    // this.repository.save(tasks)

    // api
    // this.api.save(tasks)
  }

  complete(index: number) {
    const task = this.store.state.tasks[index]
    this.store.actions.update(
      {
        index,
        task: setState(task, 'DONE')
      }
    )
  }
}

Interface
Adapters

Frameworks
& Drivers

Usecases

Entities

UI (Vue)

Store(Vuex)

Controller

Domain

Interactor

Domain

Storage

API

api

repository

今後の課題

  • UI, Storeを入れ替えてReactバージョンを作りたい
     
  • ControllerがVuex storeである必要が無い
     
  • Usecase→Storeの依存を逆転させるためにStoreのinterfaceを書いておく必要があるなどコード量は増える
     
  • 実際の開発に導入したい

Vue.js × CleanArchitecture

By ryota kunisada

Vue.js × CleanArchitecture

  • 2,006