前端领域模型 - 列表

关于「领域驱动设计」在前端的应用,断断续续都能看到一些博客,但大部分看完后不明觉厉,转眼就忘记了,最重要的原因是无法应用在自己项目中。
其实有一个大部分前端同学每天都接触的领域模型 - 「列表领域」。

领域模型是公共认知的体现

我们说到列表时,都知道它在 PC 端一般是表格形式,移动端是列表形式。

  1. 表格形式下它有页码,可以点击切换页码,表格显示当前页数据
  2. 列表形式它是无限滚动加载或点击加载更多,列表显示当前页及之前所有页数据
  3. PC 端移动端都有搜索功能,搜索时需要将页码重置到第一页

移动端与PC端列表

不仅仅是上面的描述,还有更多对于「列表」这个概念的「公共认知」就不赘述了。这个公共认知不仅对于研发,对于产品也是一样的。 产品经理在 PRD 中对于列表功能从不会详细描述,不会写出这样的 PRD

… 该页面展示客户表格,默认请求第一页数据,下方展示可以点击的页码 1 - n,点击页码时,将当前表格数据更新为点击页码对应的数据;根据关键字对客户进行搜索时,不使用当前已点击的页码而是第一页;点击重置按钮时,清空所有搜索条件,页码和表格数据回到第一页。

只需要一句「该页面展示客户列表,支持分页和搜索」,研发就知道怎么做了。对于某个业务逻辑大家有一致的认知,它就是领域模型。

领域模型在代码上的表现

下面假设存在 fetchCustomers 函数,它是一个支持分页、搜索的客户列表请求方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 声明领域模型
class List {
constructor(fn) {
this.fn = fn;
}
async fetch(params = { page: 1, pageSize: 10 }) {
const response = await this.fn(params);
this.response = response;
return response;
}
}

const customerList = new List(fetchCustomers);
// 初始化时这样调用
customerList.fetch().then(({ list, total }) => {
// 获取列表数据成功
console.log(list, total);
});

// 点击页码时这样调用(后面均省略 .then)
customerList.fetch({ page: 2 });

// 搜索时这样调用
customerList.fetch({ page: 1, name: "ltaoo" });

List 虽然有方法有属性,但它没有表达「业务逻辑」所以不能算领域模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class List {
constructor(fn) {
this.fn = fn;
}
async fetch(params = { page: 1, pageSize: 10 }) {
const response = await this.fn(params);
this.response = response;
return response;
}
init() {
return this.fetch({ page: 1, pageSize: 10 });
}
goto(page) {
return this.fetch({ page, pageSize: 10 });
}
search(params = {}) {
return this.fetch({ page: 1, pageSize: 10, ...params });
}
}

const customerList = new List(fetchCustomers);
customerList.init().then(({ list, total }) => {
// ...
});
customerList.goto(2);
customerList.search({ name: "ltaoo" });

将业务逻辑以合适的方法名封装在 List 中,调用时无需关心逻辑内部实现,只需要调用对应方法,这才是领域模型。 除了这几个方法,我们还可以有 loadMore 实现移动端加载更多、reset 重置列表等。

有生命力的领域模型

其实上面的逻辑写成一个 react hook 是完全没有问题的,类似这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function useList(fn) {
const [response, setResponse] = useState({ list: [], total: 0 });
async function fetch(params = { page: 1, pageSize: 10 }) {
const newResponse = await fn(params);
setResponse(newResponse);
}
return [
response,
{
init() {
fetch({ page: 1 });
},
goto(page) {
fetch({ page });
},
},
];
}

// 使用时
function CustomerMangePage() {
const [response, { init, goto }] = useList(fetchCustomers);
useEffect(() => {
init();
}, []);
// 渲染客户列表
}

写法上仍然简洁清晰,不过写成类的好处是不和框架耦合,无论框架怎么变更,只要仍使用 js 开发,这个 List 类是可以一直使用的。 当然,不和框架耦合,不意味着写法只能固定,我们可以创建一层框架与领域的粘合层,仍以 react hook 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function useList(fn) {
const list = useRef(new List(fn));
const [response, setResponse] = useState({ list: [], total: 0 });
useEffect(() => {
// 这个需要 List 内实现每次调用 fetch 后调用 onChange
list.onChange = (nextResponse) => {
setResponse(nextResponse);
};
}, []);
return [
response,
{
init: list.init,
goto: list.goto,
},
];
}

和直接实现为 react hook 用法是一样的,但它还可以在 vue 等框架中使用。

当然,说了这么多,这里是一个可运行的示例,实际上手体验下吧

实际示例演示

点击体验