引入间接隔离变化(一)

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对象变得无知,然而,“无知者无畏”,它也不用害怕视图呈现所发生的变化了。image 隔离变化的另一条途径是寻觅对象的共性,对这些共性进行抽象。我们不必考虑对象实现细节的不同之处,只需要把握对象的共同特征,即可完成接口的定义。接口可以看做是对象的角色。Rebecca认为:“客户访问接口比访问具体类要灵活得多,它们不需要知道具体实现,而只需明了接口中声明的公共角色即可。”【注:Rebecca Wirfs-Brock《对象设计:角色、责任和协作》】角色代表一种功能或职责的扮演,它并非演员本身,只是形象化地以某种形态或语言来表现角色的喜怒哀乐而已。例如,我们需要在项目中指定规则以限定渲染的格式。这个规则可以是数据区间,只要数据在这个区间范围之内,就应该设置为对应的格式;也可以是某种约束条件,当条件满足时,以相应的格式渲染。从实现细节来看,区间与约束是迥然不同的两种实现;可是从抽象的角度看,它们无疑扮演的都是同一种角色,那就是匹配器。只要规则匹配,就应该获得正确的格式。image 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中,作为格式规则的一部分。这种包容变化的能力,正是抽象能够提供的。

posted @ 2011-01-14 09:25 张逸 阅读(...) 评论(...) 编辑 收藏