======== 接上文《软件设计不是CRUD(4):耦合度的强弱(上)》
在模块间耦合强度已经降低至控制耦合的基础上,如果被调用的模块要求传入的是简单的数值,或者一种抽象的结构。这种依赖强度叫做数据依赖。前文已经说过,数据依赖和内容依赖是有本质区别的,两者只是在称呼上有所接近(不再赘述区别)。
在Java语言(或其他高级语言)中,这种抽象结构可能是某种业务模型的上层接口,也可能是某几种具体业务模型的父级抽象类。例如:动物对象(不是某种具体的动物)、植物对象(不是某种具体的植物)、车辆对象(不是某种具体的车辆)。这就要求被调用的模块内部,能够适应这些抽象对象并保证内部逻辑适应于所有抽象场景,而不是像控制耦合或者标记耦合那样,要求调用者按照被调用的模块内部逻辑做适应。
// 这是一个接口,只要实现了该接口的任何具体动物信息都能传入
// 接口中只规定了必须返回目前的动物类型
public interface Animal {
// 传入的动物必须指定一种动物的说明
public String getType();
}
// 这是专门给调用者使用的调用方式
public interface AnimalService {
public void create(Animal animal);
}
// 由于动物模块内部实际上也不知道会有哪些动物类型被传入
// 所以关于AnimalService接口的实现,只能按照现有需求中明确的几种动物书写代码
@Service
public class AnimalServiceImpl implements AnimalService {
@Override
public void create(Animal animal) {
// 在进行边界校验后,通过if分支来处理不同动物的不同添加行为
// 这些动物的属性和关联信息可能完全不一样
if(StringUtils.equals(animal.getType(), "lion")) {
// ...... 处理狮子的逻辑
}
if(StringUtils.equals(animal.getType(), "tiger")) {
// ...... 处理老虎的创建逻辑
}
if(StringUtils.equals(animal.getType(), "seal")) {
// ...... 处理海豹的创建逻辑
}
throw new UnsupportedOperationException("不支持的动物类型");
}
}
/**
* 在调用方中进行具体的长颈鹿这种动物的对象定义。
* 注意,这种耦合强度下,如何定义具体的数据结构,话语权在调用方
* 长颈鹿显然被调用方是没法处理的,只可能抛出异常
* @author yinwenjie
*/
public class GiraffeInfo implements Animal {
private static final String GIRAFFE = "giraffe";
private Integer age;
private String fieldA;
private String fieldB;
// ..... 还有其它和长颈鹿特点有关的字段
@Override
public String getType() {
return GIRAFFE;
}
// ......其他的get、set省略
}
// 然后调用方以如下方式进行调用
// ......
animalService.create(new GiraffeInfo());
// ......
数据耦合的核心在于,上文介绍的更强的耦合度中,调用模块必须适应被调用模块的数据结构,也就是说数据结构的要求由被调用方说了算,调用者需要符合这个规范。而从这个耦合强度开始,被调用者不再规定数据结构的具体要求,只提出一个泛化的结构概念(例如只要传入的动物对象有一个类型,被调用者就认为这是一个合法的动物对象),怎么定义数据结构的话语权在调用者一侧。
但是也由于被调用者失去了对数据结构定义的话语权,所以被调用方就需要完整考虑调用方传入数据结构的可能性。这在实际工作中又是不现实的:自然界中涉及的动物何止千种,本应用软件需要管理的动物何止3种,难道每新增一种动物的支持都需要被调用方修改create方法的具体实现?通过以上代码我们就发现了这个情况,如果外部调用者传入的是一个抽象对象,那么模块内部通过“if…if…if…”的处理分支方式是无法穷举所有的场景情况的。上文的演示代码中,调用方最终会收到一个由被调用方抛出的异常“UnsupportedOperationException”,因为被调用的模块中,不支持对“长颈鹿”这种动物的处理 !_^。
另外,细心的读者可能会发现,被调用方(动物模块)做了这么多改动,但是外部调用者目前的调用代码还并没有报错。显然动物模块的内聚性正在逐步形成,设计调整的涟漪效果也开始减少。不过调用者传入的长颈鹿数据仍然会抛出异常,这主要是因为系统调用需求之初就没有考虑到,或者说无法考虑的那么周全:调用者会有这种名叫长颈鹿的动物会需要进行业务支撑。看来实现数据耦合确实有一定难度,那么有没有一种更便于扩展、更降低耦合性的方式呢?
实际上上文各小节介绍的模块间耦合强度都是可以优化的耦合强度,真正需要设计达到的耦合强度目标是间接耦合。间接耦合是指被调用的模块本身“不知道”如何处理业务逻辑,只是负责“缝合”模块内部处理逻辑和业务场景的关系。是不是不好理解,那么换句话进行表述:模块内部没有处理逻辑,只负责组装处理逻辑,且处理逻辑本身是可以增加。这里我们来看一种利用行为模式达到模块的间接耦合的示例:
注意:由于是基于上一小节中“数据耦合”的代码进行改造,所以有的代码片段就直接省略了。
// 首先我们再被调用方的模块内,再增加一个除了service形式的接口以外的业务策略接口
// 这个接口定义了当某个具体的动物需要进行创建时,应该如何进行处理
public interface AnimalStrategy {
/**
* 在创建操作发生前该方法会触发,外部调用者调用create方法所传入的具体动物对象,会作为该方法的参数被传入
* @param animal
* @return 如果该策略的实现支持这种动物的处理过程,则返回true;其他情况返回false
*/
public boolean matched(Animal animal);
/**
* 如果当前处理策略的matched方法返回true,则该方法会被触发,用于处理这个具体动物数据的添加操作
* @param animal
*/
public void doCreate(Animal animal);
}
// =================
// 接着,由于在需求之初被调用方(动物模块)就知道了系统中有狮子和老虎两种动物需要支持
// 且这两种动物在使用了本模块的各个系统中都经常被使用,且需求重合度很高
// 于是就直接在被调用方内,为狮子和老虎这两种具体动物做了业务策略的实现(作为模块提供的默认实现),如下所示:
// 首先是狮子的实现
@Component
public class AnimalForLionStrategy implements AnimalStrategy {
@Override
public boolean matched(Animal animal) {
return StringUtils.equals(animal.getType(), "lion");
}
@Override
public void doCreate(Animal animal) {
Lion lion = (Lion)animal;
// ......
// 这里进行具体的狮子这种动物数据的创建过程
}
}
// ==========
// 然后是老虎的实现
@Component
public class AnimalForTigerStrategy implements AnimalStrategy {
@Override
public boolean matched(Animal animal) {
return StringUtils.equals(animal.getType(), "tiger");
}
@Override
public void doCreate(Animal animal) {
// ......
// 这里进行具体的老虎这种动物数据的创建过程
}
}
最后我们修改上文中AnimalService的实现,不在AnimalService的实现类中处理具体的业务过程,而是用它来做已经实现的各个具体业务处理策略的逻辑控制。为了简化,这里的示例代码业务用到一些spring中的常用注解:
// 被调用的“动物模块”,默认的具体实现。其中是一个控制逻辑而不是某种具体业务的处理过程
@Service
public class AnimalServiceImpl implements AnimalService {
@Autowired
private List<AnimalStrategy> animalStrategies;
@Override
@Transactional
public void create(Animal animal) {
// 进行边界校验后查询可以使用的策略
AnimalStrategy currentAnimalStrategy = null;
for (AnimalStrategy animalStrategy : animalStrategies) {
if(animalStrategy.matched(animal)) {
currentAnimalStrategy = animalStrategy;
break;
}
}
// 如果找到了对应的处理策略,就进行doCreate方法的调用
// 其它情况抛出异常
if(currentAnimalStrategy != null) {
currentAnimalStrategy.doCreate(animal);
return;
}
throw new UnsupportedOperationException("不支持的动物类型");
}
}
有了AnimalStrategy策略接口这样的设计,我们就可以将动物模块的对业务的具体处理过程和对动物模块的调用过程分离,具体来说就是,这种“长颈鹿”动物的业务逻辑处理不需要再动物模块形成之初就进行穷举考虑(前文说过这个要求不现实),当调用者需要自定义这种业务场景时,其业务模型和处理过程都可以由调用方自行扩展,具体代码示例过程如下:
// 由调用者根据自己特有的业务场景,扩展对“长颈鹿”这种动物的处理过程
// 这个“长颈鹿”的处理业务,其它应用系统中用不到
public class AnimalForGiraffeStrategy implements AnimalStrategy {
@Override
public boolean matched(Animal animal) {
return StringUtils.equals(animal.getType(), "giraffe");
}
@Override
public void doCreate(Animal animal) {
// ......
// 首先进行边界校验,
// 这里进行具体的长颈鹿这种动物数据的创建过程
}
}
以上代码是一种典型的策略模式(行为模式的一种),service被改造成了典型的门面模式。如下图所示:
在以上的示例中,模块调用层专门的接口实现中,其工作逻辑并不是描述某种具体的业务实现过程,而是描述了一种特定的控制过程,这个控制过程压根不知道具体的业务实现是什么样的,只是按照既定的控制顺序激活某种实现,最后再由具体的某种实现处理对应的业务分支,而且这种业务分支可以无限的扩展下去,而不会影响已有的其它业务分支。
由于所有的业务逻辑都是根据数据模型映射的独立业务处理过程,所以各个业务逻辑间不会受到各自的变化影响;外部调用者的业务逻辑也不会受到被调用模块的影响,因为具体的数据结构如何定义的话语权在被调用模块。这样的设计很好的控制了设计调整的涟漪效果。
控制耦合除了关注业务逻辑的隔离,还关注业务逻辑的扩展性。可以在模块内部已有逻辑和模块外部已有调用不改变的情况下,扩展出新的业务处理过程。这是一个针对“面向新增开发,面向修改关闭”的重要原则落地。这个原则保证了,如果开发人员按照提供的业务接口进行新的业务开发,就不会产生循环依赖。
本专题的目标就是帮助读者在实际工作中,依靠业务抽象的需求分析方式,在应用程序中各模块的设计中运用不同的设计模式来满足复杂的业务需求场景,降低模块和模块间的耦合强度直到达到间接耦合的要求。