2.5 DO系统易于理解
您可以查看代表系统高级设计的两个图:
- 图2.7中的数据思维导图中的数据实体
- 图2.8模块图中的代码模块
有点困惑,你问乔:
你:我不确定这个系统是否比传统的OO系统好,在传统的OO系统中,对象封装数据。
乔:与传统的OO系统相比,DO系统的主要好处是更容易理解。
你:是什么让它更容易理解?
乔:系统清晰地划分为代码模块和数据实体。
你:我不明白你的意思。
乔:当您尝试理解系统的数据实体时,您不必考虑操作数据实体的代码的细节。
你:你的意思是,当我看着我的图书馆管理系统的数据思维导图时,我可以自己理解它?
乔:没错。同样,当您尝试理解系统的代码模块时,您不必考虑代码操作的数据实体的细节。代码和数据之间存在明显的关注点分离。
再次查看图2.7中的数据思维导图,您会有一种恍然大悟的感觉:
![](https://img-blog.csdnimg.cn/20210619163211297.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM3MTY4NTk=,size_16,color_FFFFFF,t_70)
重点 DO系统更容易理解,因为系统分为两部分:数据实体和代码模块。
现在,您看到图2.8中的模块关系图,您会感到有点困惑:
- 一方面,模块图看起来类似于经典OO中的类图:用于类的方框和用于类之间关系的箭头。
- 另一方面,代码模块图看起来比经典OO中的类图简单得多,但是您无法解释原因。
你要乔澄清一下。
你:模块图似乎比我在OO中习惯的类图简单得多。我能感觉到,但我说不出来。
乔:原因是模块图有约束。
你:什么样的约束?
乔:正如我们之前看到的,对函数的约束:所有函数都是静态的(无状态)。而且还限制了模块之间的关系。
TIP DO模块中的所有函数都是无状态的。
你:你能解释一下吗?
乔:DO模块之间只有一种关系:使用关系。一个模块使用来自另一个模块的代码。模块之间没有关联,没有组合,也没有继承。这就是使DO模块图易于理解的原因。
你:我理解为什么DO模块之间没有关联,也没有组合:毕竟,关联和组合是数据关系。但是为什么没有继承关系呢?这是否意味着在DO中是反对多态性的?
乔:这是个很好的问题。简单的答案是,在DO中,我们使用与类继承不同的机制实现多态性。我们将在后面讨论这个问题(在第5章)。
你:现在,你引发了我的好奇心:我很确定继承是实现多态性的唯一途径。
TIP DO模块之间唯一的关系是:使用关系。
再次查看图2.8中的模块关系图,现在您不仅感觉到该关系图比经典的OO类关系图更简单,而且还理解了为什么它更简单:所有函数都是静态的,并且模块之间的所有关系都是使用类型的。
![](https://img-blog.csdnimg.cn/20210619163749440.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM3MTY4NTk=,size_16,color_FFFFFF,t_70)
表2.1 是什么使DO系统的每个部分易于理解
系统部分 | 对实体的约束 | 对关系的约束 |
---|---|---|
数据实体 | 仅限成员(无代码) | 联合与组合 |
代码模块 | 无状态函数(无成员) | 使用(无继承) |
TIP DO系统的每个部分都很容易理解,因为它有约束。
2.6 DO系统非常灵活
你:我知道代码和数据之间的明显分离使得DO系统比传统的面向对象系统更容易理解。但是如何适应需求的变化呢?
乔:DO系统的另一个好处是很容易使它们适应不断变化的需求。
您:我记得当Nancy要求我向系统中添加超级成员和VIP成员时,很难调整我的OO系统:我必须引入几个基类,类层次结构变得非常复杂。
乔:我完全知道你在说什么。我在做面向对象开发人员时也经历过同样的挣扎。告诉我对超级会员和VIP会员的要求有哪些变化,我相信你自己就会看到,调整你的DO系统很容易。
旁白
对超级会员和VIP会员的要求
- 超级会员是指允许列出其他会员的图书借阅情况的会员
- VIP会员是指允许向图书馆添加图书项目的会员
您打开IDE,开始编写Library模块的getBookLendings函数,首先没有解决超级成员的要求。您还记得乔在DO:中告诉您的关于模块函数的内容吗?
- 函数是无状态的
- 函数接收它们操作的数据作为第一个参数
在功能方面,getBookLendings分为两部分:
- 检查用户是否为图书管理员
- 从目录中检索借阅图书
基本上,getBookLendings的代码由两部分组成:
- 从UserManagement模块调用isLibrarian()函数并将UserManagementData传递给它
- 从Catalog模块调用getBookLendings()函数并将CatalogData传递给它
以下是Library.getBookLendings()的代码:
清单2.1获取会员的图书借阅量
class Library {
static getBookLendings(libraryData, userId, memberId) {
if(UserManagement.isLibrarian(libraryData.userManagement, userId)) {
return Catalog.getBookLendings(libraryData.catalog, memberId);
} else {
throw "Not allowed to get book lendings"; //1
}
}
}
class UserManagement {
static isLibrarian(userManagementData, userId) {
// will be implemented later //2
}
}
class Catalog {
static getBookLendings(catalogData, memberId) {
// will be implemented later //3
}
}
- 还有其他方法可以管理错误
- 在第3章中,我们将了解如何使用泛型数据集合管理权限
- 在第3章中,我们将了解如何使用泛型数据集合查询数据
这是您的第一段DO代码:传递所有这些数据对象LibraryData、LibraryData.userManagement和LibraryData.Catalog感觉有点笨拙。但你做到了。
乔看了看您的代码,似乎很满意。
乔:你会如何调整你的代码以适应超级会员?
您:我会将函数isSuperMember添加到UserManagement模块,并像这样Library.getBookLendings调用它
乔:没错!就这么简单。
你在你的笔记本电脑上输入这段代码:
清单2.2 允许超级会员获得会员的图书外借
class Library {
static getBookLendings(libraryData, userId, memberId) {
if(Usermanagement.isLibrarian(libraryData.userManagement, userId) ||
Usermanagement.isSuperMember(libraryData.userManagement, userId)) {
return Catalog.getBookLendings(libraryData.catalog, memberId);
} else {
throw "Not allowed to get book lendings"; //1
}
}
}
class UserManagement {
static isLibrarian(userManagementData, userId) {
// will be implemented later //2
}
static isSuperMember(userManagementData, userId) {
// will be implemented later //2
}
}
class Catalog {
static getBookLendings(catalogData, memberId) {
// will be implemented later //3
}
}
- 还有其他方法可以管理错误
- 在第3章中,我们将了解如何使用泛型数据集合管理权限
- 在第3章中,我们将了解如何使用泛型数据集合查询数据
现在,传递所有这些数据对象所造成的尴尬感觉主要是一种解脱的感觉:适应这种需求变化只需要几行代码,不需要对系统设计进行任何更改。
乔又一次看起来很满意。
TIP DO系统是灵活的。通常,它们在不改变系统设计的情况下适应不断变化的需求。
您为自己准备了一杯咖啡,然后开始编写addBookItem()代码
查看清单2.3中Library.addBookItem()的签名,您不清楚第三个参数bookItemInfo的含义。你要乔澄清一下。
清单2.3 Library.addBookItem的签名
class Library {
static addBookItem(libraryData, userId, bookItemInfo) {
}
}
你:boItemInfo是什么?
乔:让我们将其称为图书项目信息,并设想我们有一种方法可以在名为bookItemInfo的数据实体中表示该信息。
你:你是说一个对象?
乔:目前,把bookItemInfo看作一个对象是可以的。稍后(在第3章),我将向您展示如何在DO中表示数据。
除了这个关于图书项目信息如何由bookItemInfo表示的细微差别之外,清单2.4中Library.addBookItem()的代码与您在清单2.2中为Library.getBookLendings()编写的代码非常相似。再一次让您惊讶的是,添加对VIP成员的支持不需要更改设计。
清单2.4允许VIP成员向图书馆添加图书项目
class Library {
static addBookItem(libraryData, userId, bookItemData) {
if(UserManagement.isLibrarian(libraryData.userManagement, userId) ||
UserManagement.isVIPMember(libraryData.userManagement, userId)) {
return Catalog.addBookItem(libraryData.catalog, bookItemData);
} else {
throw "Not allowed to add a book item"; //1
}
}
}
class UserManagement {
static isLibrarian(userManagementData, userId) {
// will be implemented later //2
}
static isVIPMember(userManagementData, userId) {
// will be implemented later //2
}
}
class Catalog {
static addBookItem(catalogData, memberId) {
// will be implemented later //3
}
}
- 还有其他方法可以管理错误
- 在第3章中,我们将了解如何使用泛型数据集合管理权限
- 在第3章中,我们将了解如何使用泛型数据集合查询数据
你:我需要一个很大的心态转变来学习如何将代码从数据中分离出来。
乔:对你的头脑来说,最具挑战性的部分是什么?
你:数据没有封装在对象中的事实。
乔:我从OO转到DO的时候也是一样。
你:在我进入DO的旅程中,还会有其他的心态转变吗?
乔:还会有两次思维方式的转变,但我认为这比把代码从数据中分离出来更不具挑战性。
你:是关于什么的?
乔:用泛型数据结构表示数据实体(第3章),并将我们自己约束到不可变的数据对象(第4章)。
但在此之前,你和乔在你办公室附近的一家不错的小餐馆Simple吃午饭。
2.7 总结
在本章中,我们已经说明了关于代码与数据分离的DO原则#1:
NOTE 原则#1:以代码驻留在函数中的方式将代码与数据分开,这些函数的行为不依赖于以某种方式封装在函数上下文中的数据。
在DO中学习这一点需要相当大的心态转变:
- 代码与数据是分开的
- 代码在模块中聚合
- 数据在数据实体中聚合
- 代码由无状态函数组成
- 函数接收数据作为第一个参数
我们演示了如何在面向对象语言中应用这一原则。
这种分离的后果是:
- 我们可以自由地独立设计代码和数据
- 模块图很简单:它只与用法有关(没有继承)
- 数据实体图很简单:它只涉及关联和组合
第一条原则的细节总结在这个思维导图中:
![](https://img-blog.csdnimg.cn/20210619183135929.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM3MTY4NTk=,size_16,color_FFFFFF,t_70)
总体而言,DO系统比传统的OO系统更简单(更容易理解),也更灵活(更容易适应不断变化的需求)。