image.png

一、领域层

1. 概念

描述应用程序主题区域的实体和数据,以及转换该数据的代码。领域是区分不同程序的核心。
如在商店这个应用中,领域就是产品、订单、用户、购物车以及更新这些数据的方法

2. 特点

数据结构和他们之间的转化与外部世界是相互隔离的。外部的事件调用会触发领域的转换,但是并不会决定他们如何运行。

二、应用层

1. 概念

围在领域外面的是应用层,这一层描述用例
例如,“添加到购物车”这个场景就是一个用例。它描述了单击按钮后应执行的具体操作。

  • 向服务器发送一个请求;
  • 执行领域转换;
  • 使用响应的数据更新 UI。

此外,在应用层中还有端口 — 它描述了应用层如何和外部通信。通常一个端口就是一个接口(interface),一个行为契约。端口也可以被认为是一个现实世界和应用程序之间的“缓冲区”。输入端口会告诉我们应用要如何接受外部的输入,同样输出端口会说明如何与外部通信做好准备。

三、适配器层

1. 概念

最外层包含了外部服务的适配器,我们通过适配器来转换外部服务的不兼容 API
适配器可以降低我们的代码和外部第三方服务的耦合,适配器一般分为:

  • 驱动型 - 向我们的应用发消息;
  • 被动型 - 接受我们的应用所发送的消息。

一般用户最常和驱动型适配器进行交互,例如,处理UI框架发送的点击事件就是一个驱动型适配器。它与浏览器 API 一起将事件转换为我们的应用程序可以理解的信号。

2. 特点

注意,离中心越远,代码的功能就越 “面向服务”,离应用的领域就越远,这在后面我们要决定一个模块是哪一层的时候是非常重要的。

四、整洁架构的设计

image.png
整洁架构分层与设计 - 图3

1. 设计领域

程序设计中最重要的就是领域设计,它们表示了实体到数据的转换

商店的领域可能包括:

  • 每个实体的数据类型:用户、饼干、购物车和订单;
  • 如果你是用OOP(面向对象思想)实现的,那么也要设计生成实体的工厂和类;
  • 数据转换的函数。

领域中的转换方法应该只依赖于领域的规则,而不依赖于其他任何东西。比如方法应该是这样的:

  • 计算总价的方法
  • 检测用户口味的方法
  • 检测商品是否在购物车的方法

如果应用是音乐版权管理平台,那么其领域可能包括:

A、用户实体及其数据转换

  • 普通用户:consumer
    • 音乐人
    • 词曲创作人
    • 厂牌/公司
  • 管理员用户:admin
  • 超级管理员用户:superadmin
  • 财务人员
  • 审核人员
  • 运营人员
  • 内容提供商…

对应领域的方法可能包括:

  • 普通用户
    • 发布/修改作品的方法
    • 合约签署的方法
    • 分发结算的方法
    • 咨询、活动/公告/任务查看、数据总览的方法…
  • 管理员用户
    • 用户/作品/合约/结算(报表、预付、提现、投产)等模块审核的方法
    • 用户/作品/合约/结算等模块管理的方法
    • 配置管理的方法
    • 权限管理的方法
    • 咨询管理、活动/公告/任务管理、数据总览的方法…

B、作品实体及其数据转换
C、合约实体及其数据转换
D、结算记录实体及其数据转换

2. 设计应用

应用层包含用例,一个完整用例包含一个参与者、一个动作和一个结果

在商店应用里,我们可以这样区分:

  • 一个产品购买场景;
  • 支付,调用第三方支付系统;
  • 与产品和订单的交互:更新、查询;
  • 根据角色访问不同页面。

我们一般都是用主题领域来描述用例,比如“购买”包括下面的步骤:

  • 从购物车中查询商品并创建新订单;
  • 创建支付订单;
  • 支付失败时通知用户;
  • 支付成功,清空购物车,显示订单。

用例方法就是描述这个场景的代码。
此外,在应用层中还有端口—用于与外界通信的接口

3. 设计适配器

在适配器层,我们为外部服务声明适配器。适配器可以为我们的系统兼容各种不兼容的外部服务

在前端,适配器一般是UI框架和对后端的API请求模块。比如在我们的商店程序中会用到:

  • 用户界面;
  • API请求模块;
  • 本地存储的适配器;
  • API返回到应用层的适配器。

4. 实施细节

  1. src
  2. ├── domain
  3. ├── user
  4. ├── admin.ts
  5. ├── consumer.ts
  6. └── index.ts
  7. ├── work
  8. ├── album.ts
  9. ├── video.ts
  10. ├── demo.ts
  11. └── index.ts
  12. ├── cart
  13. └── index.ts
  14. ├── contract
  15. └── index.ts
  16. └── shared-kernel.d.ts
  17. ├── application
  18. ├── selectWorkAuth.ts
  19. ├── authenticate.ts
  20. ├── signContract.ts
  21. └── ports.ts
  22. ├── services
  23. ├── authAdapter.ts
  24. ├── notificationAdapter.ts
  25. ├── signAdapter.ts
  26. ├── storageAdapter.ts
  27. ├── api.ts
  28. └── store.ts
  29. ├── lib
  30. └── ui

4.1 创建数据转换

用户领域

  1. // domain/user/consumer.ts
  2. import { UniqueId, Email } from '../shared-kernel.d.ts'
  3. export type UserName = string;
  4. export type User = {
  5. id: UniqueId;
  6. name: UserName;
  7. email: Email;
  8. };
  9. export function hasRegistered(user: User): boolean {
  10. return user.status === 'resistered';
  11. }
  12. export function hasAuthSuccess(user: User): boolean {
  13. return user.status === 'authSuccess';
  14. }

专辑领域

  1. // domain/work/album.ts
  2. import { UniqueId } from '../shared-kernel.d.ts'
  3. export type SongTitle = string;
  4. export type Song = {
  5. id: UniqueId;
  6. title: SongTitle;
  7. };
  8. export type AlbumTitle = string;
  9. export type Album = {
  10. id: UniqueId;
  11. title: AlbumTitle;
  12. songs:Song[]
  13. };
  14. export function addSong(album: Album, song: Song): Cart {
  15. return { ...album, songs: [...album.songs, song] };
  16. }
  17. export function containsSong(album: Album, song: Song): boolean {
  18. return album.songs.some(({ id }) => id === song.id);
  19. }

打包车领域

  1. // domain/cart/index.ts
  2. import { work } from "../work/index";
  3. export type Cart = {
  4. works: Work[];
  5. };
  6. export function addWork(cart: Cart, work: Work): Cart {
  7. return { ...cart, works: [...cart.works, work] };
  8. }

合约领域

  1. // domain/contract/index.ts
  2. import { User } from "../work/consumer/index";
  3. import { Cart } from "../cart/index";
  4. export type Contract {
  5. user: User;
  6. cart: Cart;
  7. created: boolean;
  8. status: string
  9. }
  10. export function createContract(user: User, cart: Cart): Contract {
  11. return {
  12. user: user.id,
  13. cart,
  14. created: new Date().toISOString(),
  15. status: "new"
  16. };
  17. }

4.2 应用层设计

编写应用层接口:外部方法永远要适配我们的需求。所以,在应用层,我们不仅要描述用例本身,也要定义调用外部服务的通信方式—端口

我们按功能拆分接口:

  • 签署服务接口
  • 通知服务接口
  • 存储服务接口 ```typescript // application/ports.ts

import { Contract } from “../domain/contract/index”

export interface SignService { trySign(contract: Contract): Promise; } export interface NotificationService { notify(message: string): void; } export interface ContractsStorageService { contracts: Contract[]; updateContracts(contracts: Contract[]): void; }

  1. 用例方法,如 signContract
  2. ```typescript
  3. // application/signContract.ts
  4. import { SignService, NotificationService, ContractsStorageService } from "./ports.ts"
  5. const sign: SignService = {};
  6. const notifier: NotificationService = {};
  7. const contractStorage: ContractsStorageService = {};
  8. export function createContract(user: User, cart: Cart): Contract {
  9. return {
  10. user: user.id,
  11. cart,
  12. created: new Date().toISOString(),
  13. status: "new"
  14. };
  15. }
  16. export async function signContract(user: User, cart:Cart) {
  17. const contract = createContract(user, cart);
  18. // Try to sign for the contract;
  19. // Notify the user if something is wrong:
  20. const isSignedSuccess = await sign.trySign(contract);
  21. if (!isSignedSuccess) return notifier.notify("Oops! ");
  22. // Save the result
  23. const { contracts } = contractStorage;
  24. contractStorage.updateContracts([...contracts,contract]);
  25. }

注意:用例不会直接调用第三方服务。它依赖于接口中描述的行为,所以只要接口保持不变,我们就不需要关心哪个模块来实现它以及如何实现它,这样的模块就是可替换的

4.3 封装适配器

添加 UI 和用例:

  1. // ui/components/ContractSign.tsx
  2. import { useSignContract } from "../../../application/signContract"
  3. export function ContractSign() {
  4. // Get access to the use case in the component:
  5. const { goSign } = useSignContract();
  6. async function handleSubmit(e: React.FormEvent) {
  7. setLoading(true);
  8. e.preventDefault();
  9. // Call the use case function:
  10. await goSign(user,cart);
  11. setLoading(false);
  12. }
  13. return (
  14. <section>
  15. <h2>Checkout</h2>
  16. <form onSubmit={handleSubmit}>{/* ... */}</form>
  17. </section>
  18. );
  19. }

通过一个 Hook 来封装用例,建议把所有的服务都封装到里面,最后返回用例的方法。如 signContract 用例

  1. // application/signContract.ts
  2. export function createContract(user: User, cart: Cart): Contract {
  3. return {
  4. user: user.id,
  5. cart,
  6. created: new Date().toISOString(),
  7. status: "new"
  8. };
  9. }
  10. export function useSignContract() {
  11. const notifier: NotificationService = useNotifier();
  12. const sign: SignService = useSign();
  13. const contractStorage: ContractsStorageService = useContractStorage();
  14. async function goSign(user:User,cart:Cart) {
  15. // …
  16. const contract = createContract(user, cart);
  17. const isSignedSuccess = await sign.trySign(contract);
  18. if (!isSignedSuccess) return notifier.notify("Oops! ");
  19. const { contracts } = contractStorage;
  20. contractStorage.updateContracts([...contracts,contract]);
  21. }
  22. return { goSign };
  23. }

用例使用的服务的实现:

  1. // services/signAdapter.ts
  2. import { fakeApi } from "./api";
  3. import { SignService } from "../application/ports";
  4. export function useSign(): SignService {
  5. return {
  6. trySign(contract: Contract) {
  7. return fakeApi(true);
  8. },
  9. };
  10. }
  1. // services/api.ts
  2. export function fakeApi<TResponse>(response: TResponse): Promise<TResponse> {
  3. return new Promise((res) => setTimeout(() => res(response), 450));
  4. }
  1. // services/notificationAdapter.ts
  2. import { NotificationService } from "../application/ports";
  3. export function useNotifier(): NotificationService {
  4. return {
  5. notify: (message: string) => window.alert(message),
  6. };
  7. }
  1. // services/store.tsx
  2. const StoreContext = React.createContext<any>({});
  3. export const useStore = () => useContext(StoreContext);
  4. export const Provider: React.FC = ({ children }) => {
  5. // ...Other entities...
  6. const [contracts, setContracts] = useState([]);
  7. const value = {
  8. // ...
  9. contracts,
  10. updateContracts: setContracts,
  11. };
  12. return (
  13. <StoreContext.Provider value={value}>{children}</StoreContext.Provider>
  14. );
  15. };
  1. // services/storageAdapter.tsx
  2. import { ContractsStorageService } from "../application/ports"
  3. import { useStore } from "./store.tsx"
  4. export function useContractStorage(): ContractsStorageService {
  5. return useStore();
  6. }

用户与 UI 层交互,但是 UI 只能通过端口访问服务接口。
用例是在应用层处理的,它可以准确地告诉我们需要哪些外部服务。
所有外部服务都隐藏在基础设施中,并且遵守我们的规范。若需要更改发送消息的服务,只需要修改发送消息服务的适配器

5. 注意点

  • 按功能拆分代码,而不是按层
  • 注意跨组件使用
  • 注意领域中可能的依赖:领域的原则是不能依赖其他任何东西,如创建日期也需要依赖第三方库,让领域保持独立,也使测试更容易

参考资料