当我们将对象的行为看作职责时,就赋予了对象的生命与意识,使得我们能够以拟人的方式对待对象。一个聪明的对象是对象自己知道应该履行哪些职责,拒绝履行哪些职责,以及该如何与其他对象协作共同履行职责。这时的对象绝不是一个愚笨的数据提供者,它学会了如何根据自己拥有的数据来判断请求的响应方式、行为的执行方式,这就是所谓的对象的“自治”。
我在《领域驱动战略设计实践》中提及了限界上下文的自治特性,事实上,从更小的粒度来看,对象仍然需要具备自治的这四个特性,即:
如何来理解对象的“最小完备”?John Kern谈到对象的设计时,提到:“不要试着把对象在现实世界中可以想象得到的行为都实现到设计中去。相反,只需要让对象能够合适于应用系统即可。对象能做的、所知的最好是一点不多一点不少。”因此,对象的最小完备取决于该对象具备的知识,恰如其分地履行职责。不放弃该自己履行的职责,也不越权对别人的行为指手画脚。
例如,我们需要设计一个Web服务器,它提供了一个对象HttpProcessor,能够接收由HttpConnector发送来的Socket请求,并在处理请求后返回响应消息。请求和响应被定义为HttpRequest和HttpResponse类。请求的处理过程中需要对Socket消息进行解析,这个解析职责应该分配给哪个对象呢?
如果我们将解析职责完全交给HttpProcessor来完成,那么HttpRequest和HttpResponse将沦为两个仅提供数据的“哑对象”,这就违背了
自治原则
,没有满足对象职责的完备性。如果我们将对请求和响应的解析工作完全放到各自的HttpRequest与HttpResponse对象中,似乎又超出了这两个对象的能力范围。
仔细分析解析过程,解析Socket消息获得请求头和请求体,实际上等同于是创建HttpRequest对象,这个职责显然不应该交给HttpRequest。然而,在解析请求时,还涉及一些系统开销大的字符串操作或其他操作,这些请求参数并不是Servlet所必须要的。也就是说,服务端的HttpProcessor在接收到请求后,并没有必要处理全部的请求参数,因为它的职责是快速响应请求,不应该将时间浪费在大量目前并不需要的请求消息上。
这时,就可以将这些不曾解析的消息直接赋给HttpRequest与HttpResponse。由于二者都拥有了这些信息,就可以提供解析它们的职责:
遵循
最小完备
原则,使得HttpProcessor、HttpRequest与HttpResponse三者之间的权责变得更加清晰。此外,这一设计方式还有利于改善性能。由于解析开销较大的字符串操作并未由HttpProcessor承担,而是将这些数据流塞给了HttpRequest与HttpResponse,使得HttpProcessor的process()操作可以快速完成。当请求者真正需要相关请求信息时,就可以调用HttpRequest与HttpResponse对象的parse()方法。
所谓“自我履行”就是对象利用自己的属性完成自己的任务,不需要假手他人。这也是“信息专家模式”的体现,即信息的持有者即为操作该信息的专家。只有专业的事情交给专业的对象去做,对象的世界才能做到各司其职、各尽其责。
Martin Fowler提到的“依恋情结(Feature Envy)”坏味道就违背了对象的自我履行原则,只是二者的立场不同。特性依恋是指在一个对象的行为中,总是使用别的对象的数据和特性,就好像是羡慕别人拥有的好东西似的。自我履行指的是我守住自己的一亩三分地,该自己操作的数据绝不轻易交给别人。
例如在一个报表系统中,需要根据客户的Web请求参数作为条件动态生成报表。这些请求参数根据其数据结构的不同划分为三种:
-
单一参数(SimpleParameter):代表key和value的一对一关系
-
元素项参数(ItemParameter):一个参数包含多个元素项,每个元素项又包含key和value的一对一关系
-
表参数(TableParameter):参数的结构形成一张表,包含行头、列头和数据单元格
这些参数都实现了Parameter接口,该接口的定义为:
public interface Parameter {
String getName();
}
public class SimpleParameter implements Parameter {}
public class ItemParameter implements Parameter {}
public class TableParameter implements Parameter {}
在报表的元数据中已经配置了各种参数,包括它们的类型信息。服务端在接收到Web请求时,通过ParameterGraph加载配置文件,并利用反射创建各自的参数对象。此时,ParameterGraph拥有的参数都没有值,需要通过ParameterController从ServletHttpRequest获得参数值对各个参数进行填充。代码如下:
public class ParameterController {
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph)
{
for (Parameter para : parameterGraph.getParmaeters()) {
if (para instanceof SimpleParameter) {
SimpleParameter simplePara = (SimpleParameter) para;
String[] values = request.getParameterValues(simplePara.getName());
simplePara.setValue(values);
} else {
if (para instanceof ItemParameter) {
ItemParameter itemPara = (ItemParameter) para;
for (Item item : itemPara.getItems()) {
String[] values = request.getParameterValues(item.getName());
item.setValues(values);
}
} else {
TableParameter tablePara = (TableParameter) para;
String[] rows = request.getParameterValues(tablePara.getRowName());
String[] columns = request.getParameterValues(tablePara.getColumnName());
String[] dataCells = request.getParameterValues(tablePara.getDataCellName());
int columnSize = columns.length;
for (int i = 0; i < rows.length; i++) {
for (int j = 0; j < columns.length; j++) {
TableParameterElement element = new TableParameterElement();
element.setRow(rows[i]);
element.setColumn(columns[j]);
element.setDataCell(dataCells[columnSize * i + j]);
tablePara.addElement(element);
}
}
}
}
}
}
}
显然,这三种参数对象没有能够做到自我履行,它们把自己的数据“屈辱”地交给了ParameterController,却没有想到其实自己才是拥有填充请求数据能力的对象,毕竟只有它们才最清楚各自参数的数据结构。如果让这些参数对象都变为能够自我履行的自治对象,Do it myself,情况就完全不同了:
public class SimpleParameter implements Parameter {
public void fill(ServletHttpRequest request) {
String[] values = request.getParameterValues(this.getName());
this.setValue(values);
}
}
public class ItemParameter implements Parameter {
public void fill(ServletHttpRequest request) {
ItemParameter itemPara = this;
for (Item item : itemPara.getItems()) {
String[] values = request.getParameterValues(item.getName());
item.setValues(values);
}
}
}
当参数自身履行了填充参数的职责时,ParameterController履行的职责就变得简单了:
public class ParameterController {
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
if (para instanceof SimpleParameter) {
((SimpleParameter) para).fill(request);
} else {
if (para instanceof ItemParameter) {
((ItemParameter) para).fill(request);
} else {
((TableParameter) para).fill(request);
}
}
}
}
}
这时,我们发现各种参数由于数据结构结构的不同,导致填充行为的差异,但从抽象层面看,都是将一个ServletHttpRequest填充到Parameter中。如果将fill()方法提升到Parameter接口中,哪里还需要分支语句进行类型判断与类型转换呢?
public class ParameterController {
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
para.fill(request);
}
}
}
当一个对象能够自我履行时,就可以让调用者仅仅需要关注对象能够做什么(what to do),而不需要操心其实现细节(how to do),从而将实现细节隐藏起来。当我们让各种参数对象都履行填充职责时,ParameterController就可以只关注抽象的Parameter提供的公开接口,而无需考虑实现,对象之间的协作就变得更加松散耦合,对象的多态能力才能得到充分地体现。
一个自治的对象具有稳定空间,使其具备抵抗外部变化的能力。要做到这一点,就需要处理好外部对象与自治对象之间的依赖关系。方法就是遵循“高内聚松耦合”原则来划分对象的边界。这就好比两个行政区,各自拥有一个居民区和一家公司。居民区A的一部分人要跨行政区到公司B上班,同理,居民区B的一部分人也要跨行政区到公司A上班:
这样的两个行政区是紧耦合的,因为居民区与公司之间的关系没有做到高内聚,只是一种松散随意的划分。现在我们按照居民区与公司之间的关系,对居民区的人重新调整,就得到了两个完全隔离的行政区:
在这个例子中,调整后的系统并没有改变任何本质性的事情。所有的人都还在原来的公司上班,没有人失业;没有人流离失所,只是改变了居住地。但仅仅由于居民居住区域的改变,两个行政区的依赖关系就大为减弱。事实上,对于这个理想模型,两个行政区之间已经没有任何关系,它们之间桥梁完全可以拆除。这就是“高内聚松耦合”原则的体现,通过将关联程度更高的元素控制在一个单位内部,就可以达到降低单位间关联的目的。
要实现自治对象的稳定空间,还需要识别变化点,对变化的职责进行分离和封装。实际上,许多设计模式都可以说是“分离和封装变化”原则的体现。例如,当我们发现一个对象包含的职责既有不变的部分,又有可变的部分,只是就可以将可变的部分分离出去,将其抽象为一个接口,再以委派的形式传入到原对象,如下图所示:
此时抽象出来的接口IChangable其实就是策略模式(Strategy Pattern)或者命令模式(Command Pattern)的体现。例如Java线程的实现机制是不变的,但运行在线程中的业务却随时可变,将这部分可变的业务部分分离出来,并抽象为Runnable接口,再以构造函数参数的方式传入到Thread中:
public class Thread ... {
private Runnable target;
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public void run() {
if (target != null) {
target.run();
}
}
}
模板方法模式(Template Method Pattern)同样分离了不变与变,只是分离变化的方向是向上提取为抽象类的抽象方法而已:
例如授权认证功能的主体是对认证信息token进行处理完成认证。如果通过认证,则返回认证结果,认证无法通过,就会抛出AuthenticationException异常。整个认证功能的执行步骤是不变的,但对token的处理需要根据认证机制的不同提供不同实现,甚至允许用户自定义认证机制,就需要对这部分可变的内容进行抽象。AbstractAuthenticationManager是一个抽象类,定义了authenticate()模板方法:
public abstract class AbstractAuthenticationManager {
public final Authentication authenticate(Authentication authRequest)
throws AuthenticationException {
try {
Authentication authResult = doAuthentication(authRequest);
copyDetails(authRequest, authResult);
return authResult;
} catch (AuthenticationException e) {
e.setAuthentication(authRequest);
throw e;
}
}
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
protected abstract
Authentication doAuthentication(Authentication authentication)
throws AuthenticationException;
}
该模板方法调用的doAuthentication()是一个受保护的抽象方法,没有任何实现。这就是可变的部分,交由子类完成实现。例如ProviderManager子类就实现了doAuthentication()方法:
public class ProviderManager extends AbstractAuthenticationManager {
public Authentication doAuthentication(Authentication authentication)
throws AuthenticationException {
Class toTest = authentication.getClass();
AuthenticationException lastException = null;
for (AuthenticationProvider provider : providers) {
if (provider.supports(toTest)) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
Authentication result = null;
try {
result = provider.authenticate(authentication);
sessionController.checkAuthenticationAllowed(result);
} catch (AuthenticationException ae) {
lastException = ae;
result = null;
}
if (result != null) {
sessionController.registerSuccessfulAuthentication(result);
applicationEventPublisher.publishEvent(new AuthenticationSuccessEvent(result));
return result;
}
}
}
throw lastException;
}
}
如果一个对象存在两个可能变化的职责,则违背了“单一职责原则”,即“引起变化的原因只能有一个”。分离这两个可变的职责,且分别进行抽象,然后再形成这两个抽象职责的组合,就是桥接模式(Bridge Pattern)的体现:
例如在实现数据权限控制时,需要根据解析配置内容获得数据权限规则,然后再根据解析后的规则对数据进行过滤。需要支持多种解析规则,同时也需要支持多种过滤规则,这时就不能将这两个可变的职责放到同一个类或者接口中,如下定义:
public interface DataRuleParser {
List parseRules();
T List filterData(List srcData);
}
分离规则解析与数据过滤职责,定义到两个独立的接口中。在数据权限控制功能中,过滤数据才是实现数据权限的目标,因此应以数据过滤职责为主,在实现类中,将规则解析器作为参数传入:
public interface DataFilter<T> {
List filterData(List srcData);
}
public interface DataRuleParser {
List parseRules();
}
public class GradeDataFilter<Grade> implements DataFilter {
private DataRuleParser ruleParser;
public GradeDataFilter(DataRuleParser ruleParser) {
this.ruleParser = ruleParser;
}
@Override
public List filterData(List sourceData) {
if (sourcData == null || sourceData.isEmpty() {
return Collections.emptyList();
}
List gradeResult = new ArrayList<>(sourceData.size());