应用层
import api from 'todo/adts/mock'import store from 'todo/adts/store'import { create, canDeleteTodo, Todo } from 'todo/models/todo'import authStore from 'auth/adts/store'import { message } from 'lib/utils'export async function 查看待办() {const todos = await api.getAll()todos.forEach(t => store.add(t))}export async function 新增待办(title: string) {const { currentUser } = authStoreconst todo = create(title, currentUser)await api.create(todo)store.add(todo)}export async function 删除待办(todo: Todo) {const { currentUser } = authStoreif (canDeleteTodo(currentUser, todo)) {await api.del(todo.id)store.del(todo.id)} else {message('你无权限删除此待办')}}export async function 切换状态(todo: Todo) {const { id, complete } = todoawait api.toggle(id, !complete)store.set({ ...todo, complete: !complete })}
适配器
这一层我用用到两个适配器,store用来存todo数据,api用来拉取数据。因为是示例项目,store只是存了下基本的列表数据,常规应用中它应该还有loading,filter等等view需要的数据。store这一层的设计后面又详细论述。
import { Todo } from 'todo/models/todo'import { Store } from 'todo/app/port'import { reRender } from 'lib/utils'export default <Store>{todos: new Map(),add(todo: Todo) {this.todos.set(todo.id, todo)reRender()},del(id: number) {this.todos.delete(id)reRender()},set(todo: Todo) {this.todos.set(todo.id, todo)reRender()}}
import { Api } from 'todo/app/port'import { Todo } from 'todo/models/todo'export default <Api>{getAll: async () => [{ id: 1, owner: 'one', title: 'read book about DDD', complete: true }],create(_todo: Todo) { },del(_id: number) { },toggle(_id: number, _complete: boolean) { }}
import * as app from 'todo/app'window.addEventListener('hashchange', () => {location.hash === '#/' && app.查看待办()})window.addEventListener('todo', (e: CustomEvent) => {const { type, payload } = e.detailswitch (type) {case 'new':const event = payload as KeyboardEventconst target = event.target as HTMLInputElementconst title = target.value.trim()if (title && event.key === 'Enter') {app.新增待办(title)target.value = ''}breakcase 'toggle':app.切换状态(payload)breakcase 'del':app.删除待办(payload)breakdefault:break;}})
领域层
import { User } from 'auth/models/user'export interface Todo {id: numberowner: stringtitle: stringcomplete: boolean}export function create(title: string, owner: User): Todo {return {id: Math.random(),owner: owner.username,title,complete: false}}export function isBelongTo(todo: Todo, user: User) {return todo.owner === user.username}export function canDeleteTodo(user: User, todo: Todo) {return isBelongTo(todo, user) || user.type === 'admin'}
class还是纯函数?
传统领域驱动通常用class来表达实体,架构其实不关心实现的方式,不过适配器层已经有store来存储状态,所以这里保持简单,一律采用采用纯函数,并且不依赖上文。不过两种范式都是允许的,我比较推荐纯函数。
领域层代码是稳固的
领域层代码表达的产品的业务逻辑,这部分代码通常是稳固,不随ui,系统,技术栈的变更而变更。
同样遵循“代码即产品”原则
把上面的代码翻译一下,同样可作为产品的说明文档用。
ui
ui这一层就简单了。它应该只是简单的消费store里的数据和用例层的方法,本示例项目是发事件,让事件再去消费用例函数—架构并不在意这些细微差别。
import auth from 'auth/adts/store'export default () => {const { username } = auth.currentUserreturn `hello ${username}, <input placeholder="what need to be done ?" onkeydown="dispatch('todo/new', event)" >`}
import { Todo } from 'todo/models/todo'export default (todo: Todo) => {const { id, title, owner, complete } = todoconst _todo = `{id:${id}, owner: '${owner}', title: '${title}', complete: ${complete}}`return `<li><input type=checkbox ${complete ? 'checked' : ''} onclick="dispatch('todo/toggle', ${_todo})">${title}<button onclick="dispatch('todo/del', ${_todo})">x</button></li>`}
