应用层

  1. import api from 'todo/adts/mock'
  2. import store from 'todo/adts/store'
  3. import { create, canDeleteTodo, Todo } from 'todo/models/todo'
  4. import authStore from 'auth/adts/store'
  5. import { message } from 'lib/utils'
  6. export async function 查看待办() {
  7. const todos = await api.getAll()
  8. todos.forEach(t => store.add(t))
  9. }
  10. export async function 新增待办(title: string) {
  11. const { currentUser } = authStore
  12. const todo = create(title, currentUser)
  13. await api.create(todo)
  14. store.add(todo)
  15. }
  16. export async function 删除待办(todo: Todo) {
  17. const { currentUser } = authStore
  18. if (canDeleteTodo(currentUser, todo)) {
  19. await api.del(todo.id)
  20. store.del(todo.id)
  21. } else {
  22. message('你无权限删除此待办')
  23. }
  24. }
  25. export async function 切换状态(todo: Todo) {
  26. const { id, complete } = todo
  27. await api.toggle(id, !complete)
  28. store.set({ ...todo, complete: !complete })
  29. }

同样用例完整。

适配器

这一层我用用到两个适配器,store用来存todo数据,api用来拉取数据。因为是示例项目,store只是存了下基本的列表数据,常规应用中它应该还有loading,filter等等view需要的数据。store这一层的设计后面又详细论述。

  1. import { Todo } from 'todo/models/todo'
  2. import { Store } from 'todo/app/port'
  3. import { reRender } from 'lib/utils'
  4. export default <Store>{
  5. todos: new Map(),
  6. add(todo: Todo) {
  7. this.todos.set(todo.id, todo)
  8. reRender()
  9. },
  10. del(id: number) {
  11. this.todos.delete(id)
  12. reRender()
  13. },
  14. set(todo: Todo) {
  15. this.todos.set(todo.id, todo)
  16. reRender()
  17. }
  18. }
  1. import { Api } from 'todo/app/port'
  2. import { Todo } from 'todo/models/todo'
  3. export default <Api>{
  4. getAll: async () => [{ id: 1, owner: 'one', title: 'read book about DDD', complete: true }],
  5. create(_todo: Todo) { },
  6. del(_id: number) { },
  7. toggle(_id: number, _complete: boolean) { }
  8. }
  1. import * as app from 'todo/app'
  2. window.addEventListener('hashchange', () => {
  3. location.hash === '#/' && app.查看待办()
  4. })
  5. window.addEventListener('todo', (e: CustomEvent) => {
  6. const { type, payload } = e.detail
  7. switch (type) {
  8. case 'new':
  9. const event = payload as KeyboardEvent
  10. const target = event.target as HTMLInputElement
  11. const title = target.value.trim()
  12. if (title && event.key === 'Enter') {
  13. app.新增待办(title)
  14. target.value = ''
  15. }
  16. break
  17. case 'toggle':
  18. app.切换状态(payload)
  19. break
  20. case 'del':
  21. app.删除待办(payload)
  22. break
  23. default:
  24. break;
  25. }
  26. })

领域层

  1. import { User } from 'auth/models/user'
  2. export interface Todo {
  3. id: number
  4. owner: string
  5. title: string
  6. complete: boolean
  7. }
  8. export function create(title: string, owner: User): Todo {
  9. return {
  10. id: Math.random(),
  11. owner: owner.username,
  12. title,
  13. complete: false
  14. }
  15. }
  16. export function isBelongTo(todo: Todo, user: User) {
  17. return todo.owner === user.username
  18. }
  19. export function canDeleteTodo(user: User, todo: Todo) {
  20. return isBelongTo(todo, user) || user.type === 'admin'
  21. }

领域层存放类型声明和业务逻辑,这里有几点需要注意:

class还是纯函数?

传统领域驱动通常用class来表达实体,架构其实不关心实现的方式,不过适配器层已经有store来存储状态,所以这里保持简单,一律采用采用纯函数,并且不依赖上文。不过两种范式都是允许的,我比较推荐纯函数。

领域层代码是稳固的

领域层代码表达的产品的业务逻辑,这部分代码通常是稳固,不随ui,系统,技术栈的变更而变更。

同样遵循“代码即产品”原则

把上面的代码翻译一下,同样可作为产品的说明文档用。

ui

ui这一层就简单了。它应该只是简单的消费store里的数据和用例层的方法,本示例项目是发事件,让事件再去消费用例函数—架构并不在意这些细微差别。

  1. import auth from 'auth/adts/store'
  2. export default () => {
  3. const { username } = auth.currentUser
  4. return `
  5. hello ${username}, <input placeholder="what need to be done ?" onkeydown="dispatch('todo/new', event)" >
  6. `
  7. }
  1. import { Todo } from 'todo/models/todo'
  2. export default (todo: Todo) => {
  3. const { id, title, owner, complete } = todo
  4. const _todo = `{id:${id}, owner: '${owner}', title: '${title}', complete: ${complete}}`
  5. return `
  6. <li>
  7. <input type=checkbox ${complete ? 'checked' : ''} onclick="dispatch('todo/toggle', ${_todo})">
  8. ${title}
  9. <button onclick="dispatch('todo/del', ${_todo})">x</button>
  10. </li>
  11. `
  12. }