一、领域层
1. 概念
描述应用程序主题区域的实体和数据,以及转换该数据的代码。领域是区分不同程序的核心。
如在商店这个应用中,领域就是产品、订单、用户、购物车以及更新这些数据的方法。
2. 特点
数据结构和他们之间的转化与外部世界是相互隔离的。外部的事件调用会触发领域的转换,但是并不会决定他们如何运行。
二、应用层
1. 概念
围在领域外面的是应用层,这一层描述用例。
例如,“添加到购物车”这个场景就是一个用例。它描述了单击按钮后应执行的具体操作。
- 向服务器发送一个请求;
- 执行领域转换;
- 使用响应的数据更新 UI。
此外,在应用层中还有端口 — 它描述了应用层如何和外部通信。通常一个端口就是一个接口(interface),一个行为契约。端口也可以被认为是一个现实世界和应用程序之间的“缓冲区”。输入端口会告诉我们应用要如何接受外部的输入,同样输出端口会说明如何与外部通信做好准备。
三、适配器层
1. 概念
最外层包含了外部服务的适配器,我们通过适配器来转换外部服务的不兼容 API。
适配器可以降低我们的代码和外部第三方服务的耦合,适配器一般分为:
- 驱动型 - 向我们的应用发消息;
- 被动型 - 接受我们的应用所发送的消息。
一般用户最常和驱动型适配器进行交互,例如,处理UI框架发送的点击事件就是一个驱动型适配器。它与浏览器 API 一起将事件转换为我们的应用程序可以理解的信号。
2. 特点
注意,离中心越远,代码的功能就越 “面向服务”,离应用的领域就越远,这在后面我们要决定一个模块是哪一层的时候是非常重要的。
四、整洁架构的设计
1. 设计领域
程序设计中最重要的就是领域设计,它们表示了实体到数据的转换。
商店的领域可能包括:
- 每个实体的数据类型:用户、饼干、购物车和订单;
- 如果你是用OOP(面向对象思想)实现的,那么也要设计生成实体的工厂和类;
- 数据转换的函数。
领域中的转换方法应该只依赖于领域的规则,而不依赖于其他任何东西。比如方法应该是这样的:
- 计算总价的方法
- 检测用户口味的方法
- 检测商品是否在购物车的方法
如果应用是音乐版权管理平台,那么其领域可能包括:
A、用户实体及其数据转换
- 普通用户:consumer
- 音乐人
- 词曲创作人
- 厂牌/公司
- 管理员用户:admin
- 超级管理员用户:superadmin
- 财务人员
- 审核人员
- 运营人员
- 内容提供商…
对应领域的方法可能包括:
- 普通用户
- 发布/修改作品的方法
- 合约签署的方法
- 分发结算的方法
- 咨询、活动/公告/任务查看、数据总览的方法…
- 管理员用户
- 用户/作品/合约/结算(报表、预付、提现、投产)等模块审核的方法
- 用户/作品/合约/结算等模块管理的方法
- 配置管理的方法
- 权限管理的方法
- 咨询管理、活动/公告/任务管理、数据总览的方法…
B、作品实体及其数据转换
C、合约实体及其数据转换
D、结算记录实体及其数据转换
2. 设计应用
应用层包含用例,一个完整用例包含一个参与者、一个动作和一个结果。
在商店应用里,我们可以这样区分:
- 一个产品购买场景;
- 支付,调用第三方支付系统;
- 与产品和订单的交互:更新、查询;
- 根据角色访问不同页面。
我们一般都是用主题领域来描述用例,比如“购买”包括下面的步骤:
- 从购物车中查询商品并创建新订单;
- 创建支付订单;
- 支付失败时通知用户;
- 支付成功,清空购物车,显示订单。
用例方法就是描述这个场景的代码。
此外,在应用层中还有端口—用于与外界通信的接口
3. 设计适配器
在适配器层,我们为外部服务声明适配器。适配器可以为我们的系统兼容各种不兼容的外部服务。
在前端,适配器一般是UI框架和对后端的API请求模块。比如在我们的商店程序中会用到:
- 用户界面;
- API请求模块;
- 本地存储的适配器;
- API返回到应用层的适配器。
4. 实施细节
src├── domain│ ├── user│ │ ├── admin.ts│ │ ├── consumer.ts│ │ └── index.ts│ ├── work│ │ ├── album.ts│ │ ├── video.ts│ │ ├── demo.ts│ │ └── index.ts│ ├── cart│ │ └── index.ts│ ├── contract│ │ └── index.ts│ └── shared-kernel.d.ts├── application│ ├── selectWorkAuth.ts│ ├── authenticate.ts│ ├── signContract.ts│ └── ports.ts├── services│ ├── authAdapter.ts│ ├── notificationAdapter.ts│ ├── signAdapter.ts│ ├── storageAdapter.ts│ ├── api.ts│ └── store.ts├── lib└── ui
4.1 创建数据转换
用户领域
// domain/user/consumer.tsimport { UniqueId, Email } from '../shared-kernel.d.ts'export type UserName = string;export type User = {id: UniqueId;name: UserName;email: Email;};export function hasRegistered(user: User): boolean {return user.status === 'resistered';}export function hasAuthSuccess(user: User): boolean {return user.status === 'authSuccess';}
专辑领域
// domain/work/album.tsimport { UniqueId } from '../shared-kernel.d.ts'export type SongTitle = string;export type Song = {id: UniqueId;title: SongTitle;};export type AlbumTitle = string;export type Album = {id: UniqueId;title: AlbumTitle;songs:Song[]};export function addSong(album: Album, song: Song): Cart {return { ...album, songs: [...album.songs, song] };}export function containsSong(album: Album, song: Song): boolean {return album.songs.some(({ id }) => id === song.id);}
打包车领域
// domain/cart/index.tsimport { work } from "../work/index";export type Cart = {works: Work[];};export function addWork(cart: Cart, work: Work): Cart {return { ...cart, works: [...cart.works, work] };}
合约领域
// domain/contract/index.tsimport { User } from "../work/consumer/index";import { Cart } from "../cart/index";export type Contract {user: User;cart: Cart;created: boolean;status: string}export function createContract(user: User, cart: Cart): Contract {return {user: user.id,cart,created: new Date().toISOString(),status: "new"};}
4.2 应用层设计
编写应用层接口:外部方法永远要适配我们的需求。所以,在应用层,我们不仅要描述用例本身,也要定义调用外部服务的通信方式—端口。
我们按功能拆分接口:
- 签署服务接口
- 通知服务接口
- 存储服务接口 ```typescript // application/ports.ts
import { Contract } from “../domain/contract/index”
export interface SignService {
trySign(contract: Contract): Promise
用例方法,如 signContract:```typescript// application/signContract.tsimport { SignService, NotificationService, ContractsStorageService } from "./ports.ts"const sign: SignService = {};const notifier: NotificationService = {};const contractStorage: ContractsStorageService = {};export function createContract(user: User, cart: Cart): Contract {return {user: user.id,cart,created: new Date().toISOString(),status: "new"};}export async function signContract(user: User, cart:Cart) {const contract = createContract(user, cart);// Try to sign for the contract;// Notify the user if something is wrong:const isSignedSuccess = await sign.trySign(contract);if (!isSignedSuccess) return notifier.notify("Oops! ");// Save the resultconst { contracts } = contractStorage;contractStorage.updateContracts([...contracts,contract]);}
注意:用例不会直接调用第三方服务。它依赖于接口中描述的行为,所以只要接口保持不变,我们就不需要关心哪个模块来实现它以及如何实现它,这样的模块就是可替换的。
4.3 封装适配器
添加 UI 和用例:
// ui/components/ContractSign.tsximport { useSignContract } from "../../../application/signContract"export function ContractSign() {// Get access to the use case in the component:const { goSign } = useSignContract();async function handleSubmit(e: React.FormEvent) {setLoading(true);e.preventDefault();// Call the use case function:await goSign(user,cart);setLoading(false);}return (<section><h2>Checkout</h2><form onSubmit={handleSubmit}>{/* ... */}</form></section>);}
通过一个 Hook 来封装用例,建议把所有的服务都封装到里面,最后返回用例的方法。如 signContract 用例
// application/signContract.tsexport function createContract(user: User, cart: Cart): Contract {return {user: user.id,cart,created: new Date().toISOString(),status: "new"};}export function useSignContract() {const notifier: NotificationService = useNotifier();const sign: SignService = useSign();const contractStorage: ContractsStorageService = useContractStorage();async function goSign(user:User,cart:Cart) {// …const contract = createContract(user, cart);const isSignedSuccess = await sign.trySign(contract);if (!isSignedSuccess) return notifier.notify("Oops! ");const { contracts } = contractStorage;contractStorage.updateContracts([...contracts,contract]);}return { goSign };}
用例使用的服务的实现:
// services/signAdapter.tsimport { fakeApi } from "./api";import { SignService } from "../application/ports";export function useSign(): SignService {return {trySign(contract: Contract) {return fakeApi(true);},};}
// services/api.tsexport function fakeApi<TResponse>(response: TResponse): Promise<TResponse> {return new Promise((res) => setTimeout(() => res(response), 450));}
// services/notificationAdapter.tsimport { NotificationService } from "../application/ports";export function useNotifier(): NotificationService {return {notify: (message: string) => window.alert(message),};}
// services/store.tsxconst StoreContext = React.createContext<any>({});export const useStore = () => useContext(StoreContext);export const Provider: React.FC = ({ children }) => {// ...Other entities...const [contracts, setContracts] = useState([]);const value = {// ...contracts,updateContracts: setContracts,};return (<StoreContext.Provider value={value}>{children}</StoreContext.Provider>);};
// services/storageAdapter.tsximport { ContractsStorageService } from "../application/ports"import { useStore } from "./store.tsx"export function useContractStorage(): ContractsStorageService {return useStore();}
用户与 UI 层交互,但是 UI 只能通过端口访问服务接口。
用例是在应用层处理的,它可以准确地告诉我们需要哪些外部服务。
所有外部服务都隐藏在基础设施中,并且遵守我们的规范。若需要更改发送消息的服务,只需要修改发送消息服务的适配器
5. 注意点
- 按功能拆分代码,而不是按层
- 注意跨组件使用
- 注意领域中可能的依赖:领域的原则是不能依赖其他任何东西,如创建日期也需要依赖第三方库,让领域保持独立,也使测试更容易

