一、领域层
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.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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 result
const { contracts } = contractStorage;
contractStorage.updateContracts([...contracts,contract]);
}
注意:用例不会直接调用第三方服务。它依赖于接口中描述的行为,所以只要接口保持不变,我们就不需要关心哪个模块来实现它以及如何实现它,这样的模块就是可替换的。
4.3 封装适配器
添加 UI 和用例:
// ui/components/ContractSign.tsx
import { 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.ts
export 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.ts
import { fakeApi } from "./api";
import { SignService } from "../application/ports";
export function useSign(): SignService {
return {
trySign(contract: Contract) {
return fakeApi(true);
},
};
}
// services/api.ts
export function fakeApi<TResponse>(response: TResponse): Promise<TResponse> {
return new Promise((res) => setTimeout(() => res(response), 450));
}
// services/notificationAdapter.ts
import { NotificationService } from "../application/ports";
export function useNotifier(): NotificationService {
return {
notify: (message: string) => window.alert(message),
};
}
// services/store.tsx
const 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.tsx
import { ContractsStorageService } from "../application/ports"
import { useStore } from "./store.tsx"
export function useContractStorage(): ContractsStorageService {
return useStore();
}
用户与 UI 层交互,但是 UI 只能通过端口访问服务接口。
用例是在应用层处理的,它可以准确地告诉我们需要哪些外部服务。
所有外部服务都隐藏在基础设施中,并且遵守我们的规范。若需要更改发送消息的服务,只需要修改发送消息服务的适配器
5. 注意点
- 按功能拆分代码,而不是按层
- 注意跨组件使用
- 注意领域中可能的依赖:领域的原则是不能依赖其他任何东西,如创建日期也需要依赖第三方库,让领域保持独立,也使测试更容易