引入间接隔离变化(一)
David Wheeler有一句名言:“计算机科学中的大多数问题都可以通过增加一层间接性来解决。”间接代表着迂回。世间没有哪一条道路是完全笔直的。蜿蜒曲折的道路并非出于美的灵感,不过是因为我们需要绕开路途中的障碍罢了。
我们在设计中遇到的最大障碍,无疑就是变化。若能御变化于实现之外,软件开发就会变得美好。
应对变化的要诀是隔离。设计者需要界定对象的不变部分与可变部分,然后将可变部分隐藏起来,即使发生了变化,也不会影响到外部。这就是封装的含义。正如地壳核心的变化如此的狂暴与迅捷,但对于地面上生活的人类而言,几乎微不可察。然而,一旦地壳的变化冲出地表,就会酿成天大的灾难。变化对软件系统造成的灾难,并不亚于地震或者火山。封装为对象内部的实现设定了一层隔离带,将复杂变化的业务逻辑或者算法策略隐藏在对象之内。只要保证对象的接口不发生变化,调用者与对象内部的实现就可以单独演化了。
当我们发现一个对象需要依赖另一个不稳定的对象,同时,还需要执行复杂的交互逻辑时,就可以考虑引入一个新的对象来封装这些逻辑,从而解除二者之间的耦合,隔离变化。Spring MVC中的ModelAndView对象扮演的正是这一角色。根据MVC模式,控制器需要将模型对象所持有的数据以及数据的变化呈现到视图中。它通过寻找正确的视图对象,完成页面的展现。控制器承担了这一职责,就意味它必须依赖于视图对象。例如这样的代码:
public class CustomerController implements Controller {
@Override
public View handleRequest(
HttpServletRequst request,
HttpServletResponse response) throws Exception {
Map model = new HashMap();
model.put(“customers”, getCustomerList());
return new InternalResourceView(”/WEB-INF/jsp/customerList.jsp”);
}
}
View具体对象的创建,使得CustomerController与InternalResourceView紧紧地绑定起来,失去了灵活性,导致我们无法自由改变View的实现。作为一个灵活的MVC框架,显然很难容忍这二者之间的强耦合。要打破这种耦合关系,就需要封装寻找以及创建视图的职责,并将这一职责放到合适的对象中。这正是引入ModelAndView类的缘由。Controller放心地将所有与View相关的职责转移给ModelAndView,而它只需要悠闲地传递一个视图名称即可。
public class CustomerController implements Controller {
@Override
public ModelAndView handleRequest(
HttpServletRequst request,
HttpServletResponse response) throws Exception {
Map model = new HashMap();
model.put(“customers”, getCustomerList());
return ModelAndView(”customerList”, model);
}
}
通过字符串类型的名称常量去寻找合适的视图,而不是具体的View对象,就使得Controller冲破了View类型的约束,变得自在而开放。因为封装的作用,Controller对象变得无知,然而,“无知者无畏”,它也不用害怕视图呈现所发生的变化了。 隔离变化的另一条途径是寻觅对象的共性,对这些共性进行抽象。我们不必考虑对象实现细节的不同之处,只需要把握对象的共同特征,即可完成接口的定义。接口可以看做是对象的角色。Rebecca认为:“客户访问接口比访问具体类要灵活得多,它们不需要知道具体实现,而只需明了接口中声明的公共角色即可。”【注:Rebecca Wirfs-Brock《对象设计:角色、责任和协作》】角色代表一种功能或职责的扮演,它并非演员本身,只是形象化地以某种形态或语言来表现角色的喜怒哀乐而已。例如,我们需要在项目中指定规则以限定渲染的格式。这个规则可以是数据区间,只要数据在这个区间范围之内,就应该设置为对应的格式;也可以是某种约束条件,当条件满足时,以相应的格式渲染。从实现细节来看,区间与约束是迥然不同的两种实现;可是从抽象的角度看,它们无疑扮演的都是同一种角色,那就是匹配器。只要规则匹配,就应该获得正确的格式。 public interface Matcher {
public boolean matches(Object value);
}
public class Range implements Matcher{
private double min;
private double max;
public Range(double min, double max) {
this.min = min;
this.max = max;
}
private boolean in(double data) {
//判断data是否在此区间
}
public boolean matches(Object value) {
try {
return in((double)value);
}catch (InvalidCastException) {
return false;
}
}
}
public class Constraint implements Matcher {
private String expected;
private boolean ignoreCase;
public Constraint(String expected) {
this.expected = expected;
ignoreCase = true;
}
public Constraint(String expected, boolean ignoreCase) {
}
public boolean matches(Object value) {
if (ignoreCase) {
return expected.equalsIgnoreCase(value.toString());
} else {
return expected.equals(value.toString());
}
}
}
Matcher接口抽象了区间与约束的共性特征,使得二者在规则中能够友好相处:
public class Rule {
public Rule(Matcher matcher, Format format){}
public Matcher getMatcher(){}
public void setMatcher(Matcher matcher){}
public Format getFormat(){}
public void setFormat(Format format){}
}
如果需要更多的匹配器,只要实现Matcher接口,就可以放入Rule中,作为格式规则的一部分。这种包容变化的能力,正是抽象能够提供的。