现在的位置: 首页 > 综合 > 正文

和依赖的斗争——谈谈使用模式前后的依赖关系变化

2013年06月17日 ⁄ 综合 ⁄ 共 7081字 ⁄ 字号 评论关闭
                                  和依赖的斗争
                        ——谈谈使用模式前后的依赖关系变化
 
 
在面向对象的设计和编码过程中,类和类之间或多或少总有这样或那样的关系。除了继承和实现,其他的关系都可以归结到依赖这种关系里;所以说依赖是类之间最普遍的一种关系。而我们在设计和编码的时候,大部分时间都要和依赖打交道。可以说,一个类不可能不与其他类产生直接和间接的依赖关系,如果没有的话,那么这个类就失去了它存在的价值。但同时,值得注意的是,一个类不能与其他类产生过多的依赖关系;换句话说,一个类与其他类的依赖关系要尽量的少。这是面向对象的原则和模式一直试图向我们传输的一个原则。
可以说,一个类与其他类的依赖越少,那么这个模块或系统的复杂度就越低;这个模块或系统的可维护性和可扩展性就越高。这正是我们的设计和编码希望达到的。本文试图从依赖关系的角度来看看模式对依赖关系的影响,然后希望通过这种依赖的变化方向来指导我们的编码和设计;换句话说,不管我们的设计和编码是否是模式的,我们的类和类之间的关系都要符合这种依赖关系的变化。这样才是一个良好的设计和编码。
下面的代码,我们都非常熟悉:
Dog dog = new Dog();
dog.bark();
我们可以看到,客户端类和Dog类就有一种依赖关系。这种依赖关系是我们所需要的,不然Dog类就对我们的客户类没有什么价值了。但我们同时也要注意到:这种依赖关系在系统的维护和扩展的时候,给我们的客户类带来的很多的麻烦。
例如,我们给系统增加一个Cat类,那么客户类的调用可能变成了如下的样子:
if(condition1)
{

        Dog dog = new Dog();

       dog.bark();
}
else if(condition2)
{

        Cat cat = new Cat();

       cat.mew();
}
我们可以看到,系统的扩展对客户类是相当不利的,每当系统有扩展的时候,客户类就得不停的作相应的修改,来满足更多的扩展需要。我们可以说这样的系统的扩展性不好。下面,我们就要尝试着一步一步的把这个系统的扩展性设计得好起来。
我们首先想到的是,让这些Dog类、Cat类等都有一个公共的接口;然后如果系统有扩展,则新类都去实现这个接口,这是系统有了良好扩展性的基础。
public interface Animal
{

        public void say();

}
这样,则Dog类为:

public class Dog implements Animal

{

        public void say()

        {
               ……
}
}
我们的客户类调用如下:
Animal animal;
If(condition1)
{

        animal = new Dog();

}
else
{

        animal = new Cat();

}
animal.say();
现在我们看到,客户类的扩展性稍稍好了一些。分析一下各类之间的依赖关系,可以发现,客户类现在多了一个依赖:对接口的依赖。正是这个依赖关系使得该类的扩展性相好的方向发展。这就是模式所希望给我们带来的第一个依赖关系的变化:
1. 客户类要依赖于抽象,而不要依赖于具体
这个依赖的变化是系统扩展性的基础,以面向对象的基本原则:依赖颠倒原则来定义这个变化,它是系统的扩展性的基础。如上面的例子是一个工厂模式的开始。
绝大多数的模式都是以接口为基础的,我们都熟知和常用的命令模式、策略模式和状态模式都是建立在对行为、算法和状态的抽象的基础上的。从而实现了客户类从对具体的行为、算法和状态的依赖转移到对它们的抽象的依赖。下面以命令模式为例。
没有遵从命令模式的代码如下:
if(condition1)
{

        //do action 1

        ……
}
else if(condition2)
{

        //do action 2

        ……
}
else
{

        //do action 3

        ……
}
这段代码依赖具体,可以很明显的看出这样的代码的扩展性不好。如果使用命令模式的话,我们首先要做的是对这些行为抽象出接口来,如:
public interface Action
{

        public void doAction();

}
然后客户类的调用就变成下面的样子:
Action action;

if(condition1) action = new Action1();

else if(condition2) action = new Action2();

else action = new Action3();
action.doAction();
这就是一个使用了命令模式的客户类的初始样子。它转移了部分的依赖为对接口的依赖,从而使得客户类有了一定的扩展性。
而我们的组合模式却是将整体和部分抽象出统一的接口,从而使得对整体和部分的操作完成了统一。
没有使用组合模式的代码:
//如果条件为部分
if(…)
{
        //完成对部分的操作
        ……
}
//如果条件为整体
else if(…)
{
        //完成对整体的操作
}
使用组合模式的话,我们为部分和整体做一个统一的接口:Component,然后让部分和整体都去实现这个接口。客户类的调用如下:
Component part = new Part();
Component whole = new Whole();
whole.add(part);
//然后对whole对象进行操作,从而完成对part对象的操作。
……
从以上的这些例子,模式一直所倡导的“对接口编程”其实就是想将客户类对具体的依赖转移到对抽象的依赖。只有把依赖从对具体的依赖转移到对抽象的依赖,我们才可以获得系统的扩展性。这是我们转移依赖的开始。
下面我们再回过头来看看我们在文章的开始就拿出来的那个猫狗的例子,我们通过对猫狗的抽象,已经得到如下的客户类的代码:
Animal animal;
If(condition1)
{

        animal = new Dog();

}
else
{

        animal = new Cat();

}
animal.say();
我们说这个客户类的扩展性仍然不是很好,因为客户类除了对抽象的接口有依赖关系,仍然对具体类,如Dog类和Cat类有依赖关系。这使得如果我们增加一个新类的话,客户类仍然需要做一定的修改。
如果要让客户类有良好的扩展性,必须消除掉这种对具体类,如Dog类和Cat类的依赖关系。工厂模式告诉我们,我们可以把客户类对具体类的依赖转移到工厂类里面去,这样彻底消除了客户类对具体类的依赖。这就是我们改变依赖关系的第二种方法:
2. 转移依赖
下面我们来看工厂模式是如何做的。首先是创建一个工厂类来生产各种不同的具体类,其代码如下:
public class Factory
{

        public static Animal getInstance(String type)

        {
               if(type.equals(“dog”))
               {
                      return new Dog();
}
else if(type.equals(“cat”))
{
   return new Cat();
}
else
{
   ……
}
}
}
这样,我们的客户类代码就是像下面的样子:

Animal animal = Factory.getInstance(type);

animal.say();
我们的客户类已经看不到具体类了,这样无论增加多少新类,都与客户类无关,客户类有了很好的扩展性。
看到这里,大家可能会问:上面的工厂模式只是将客户类对具体类的依赖转移到了工厂类对具体类的依赖,有了新类增加的话,虽然我们不用修改客户类了,但是我们仍然需要修改工厂类。客户类的扩展性好了,但工厂类的扩展性并不好,这样整个系统的扩展性也还是和以前一样。这样的工厂模式使用起来有什么意义呢?
我们可以说,当然有意义,而且这一小步对于我们系统的扩展具有决定性的意义。我们的猫狗类可能在很多的客户类中使用,四个、五个、十个、二十个都不定,如果不是用工厂模式,那么我们增加一个新类的话,需要到所有的客户类中去逐一作相应的修改,这可是吃力不讨好的活。而我们使用工厂模式的话,我们只需要对工厂类进行相应的修改就行,这样的扩展性是不是比没有使用工厂模式强了很多?
工厂模式的依赖转移可以说是我们为了系统的扩展性而做的额外的工作。在很多时候,我们却不得不将我们的依赖关系作一定转移,以获得我们想要的功能。请看我们下面的例子。
由于某种原因,我们需要在我们的类中使用一个没有源代码的类,我们不能对该类做任何的修改。下面是那个HaHa类的一些方法:
public class HaHa
{

        public void f()

        {
               ……
}
public void g()
{
       ……
}
……
}
现在我们希望使用HaHa类的f()方法和g()方法:
public void doAll()
{

        HaHa hh = new HaHa();

        hh.f();
        hh.g();
}
这样,本来我们已经皆大欢喜。但是现实是残酷的,实际的需求要求我们的doAll()方法不但要处理HaHa类的一些动作,而且也处理对XiXi类的如下操作:
public class XiXi
{

        public void f()

        {
               ……
}
public void g()
{
       ……
}
}
XiXi类是我们自己写的类,我们可以控制。对于这样的需求,大家说,很容易啊,我们让XiXi类和HaHa类都继承同一个接口Fg,对doAll()方法作如下修改:
public void doAll(Fg fg)
{
        fg.f();
        fg.g();
}
我们高高兴兴的来改写XiXi类,如下:
public class XiXi implements Fg
{

        public void f()

        {
               ……
}
public void g()
{
       ……
}
}
然后,我们在准备改写HaHa类的时候,顿时傻了眼,我们发现HaHa类我们根本不能做修改,怎么办?
doAll方法根本不能依赖HaHa类,不管是直接的还是间接的。这时候,我们就不得不对HaHa类做一定的依赖转移了。Adapter模式告诉我们,我们需要对HaHa类做一定的适配才能间接的为我们的doAll方法所使用,代码如下:

public class AdapterFg implemnets Fg

{

        private HaHa haha = new HaHa();

        public void f()

        {
               haha.f();
}
public void g()
{
       haha.g();
}
}
这样,我们的doAll方法就可以通过依赖AdapterFg的抽象从而达到依赖HaHa类的目的。通过我们的AdapterFg类,我们可以看到,通过将doAllHaHa类的依赖转移到AdaterFgHaHa类的依赖,doAll方法不依赖HaHa类,从而达到了doAll方法的扩展性良好的目的:doAll不只是操作HaHa类,而是对所有实现了Fg接口的类都能操作。
代理模式也是这样,客户类不能直接操作核心类,因为除了对核心类的操作,客户类还需要做一些额外的工作。这时候,我们就将客户类对核心类的依赖转移到代理类中去,通过代理类来依赖核心类,然后客户类再依赖代理类,从而达到了客户类对核心类的操作。代理模式的详细做法,请大家参考有关代码模式的文章。
Façade模式同样如此,它将客户类对后台的依赖(通常是对后台多个类的依赖)统统转移到Façade类对后台的依赖,然后客户类来依赖Facase类。这样的例子很多,这里不再一一列出。
下面我们还是来看我们前面的工厂类:
public class Factory
{

        public static Animal getInstance(String type)

        {
               if(type.equals(“dog”))
               {
                      return new Dog();
}
else if(type.equals(“cat”))
{
   return new Cat();
}
else
{
   ……
}
}
}
我们前面说过,我们的工厂类对具体类的依赖也是蛮大,这种依赖也是我们所不希望看到的。我们上面的工厂模式准确来说是简单工厂模式,它仍然对具体的实现类有了很大的依赖,所以我们的模式来对简单工厂做进一步的抽象,于是有了抽象工厂模式和工厂方法模式。关于这两个模式是如何实现对简单工厂模式进一步抽象的,我们会有一个专门的话题来探讨它们,这里不再详说。
这里,我们将通过另一个途径来对上面的工厂类进一步消除该类对具体类的依赖,这种途径就是使用反射来完成对一个类的依赖的消除:
3. 消除依赖
使用反射来对工厂类的依赖进行消除十分简单,代码如下:
public class Factory
{

        public static Animal getInstance(String type)

        {
               try
               {
                      Class cls = Class.forName(type);
                      return (Animal)cls.newInstance();
}
catch(Exception e)
{
   return null;
}
}
}
这个工厂类的代码十分简单,其中的输入参数type应该为带路经的类名。正是通过反射机制,使得我们的工厂类也仅仅依赖于各个产品的抽象接口:Animal。这样,我们的整个系统就有了良好的扩展性,无论要增加多少个产品都没有关系,只要我们的产品实现的是Animal接口。
不要以为使用了反射,消除了客户类对具体类的依赖就百事大吉。其实,依赖是无处不在的,消除了对具体类的依赖,我们又引入了其他的烦恼。我们来看客户类对工厂类的调用,其代码如下:
String path = “……”;
String className = “……”;

Animal animal = Factory.getInstance(path+className);

If(animal!=null)
{
       animal.say();
}
我们可以看到,这两个变化:pathclassName又成了我们的新的麻烦,如果有新的类增加进来,我们又不得不到客户类来维护这两个变量。
要消除客户类对这两个变量的维护,我们有两种办法:一是做一个常量类,将这些带路经的类名和它们的常量名一一对应起来,这样,我们就不用到客户类里去维护类名变量了。但我们知道,我们将这种维护转移到了那个常量类里去了。每增加一个新类,我们都要去维护那个常量类。如下:
public class MyConstant
{

        public static String DOG = “……”+”.Dog”;

        public static String CAT = “……”+”.Cat”;

        …...
}
客户类的调用:

Animal animal = Factory.getInstance(MyConstant.DOG);

If(animal!=null)
{
       animal.say();
}
第二种方法是我们的众多的开发框架所使用的方法:使用配置文件。
我们首先使用一个xml文件将所有的关系配置起来,如下:
……
<item>
       <name>dog</name>
       <path>xxxxx.Dog</path>
</item>
<item>
       <name>cat</name>
       <path>xxxxx.Cat</path>
</item>
……
注意:我们的配置文件中的xxxxx代表的是该类的路径。
然后,我们使用一个解析类NameParser来解析我们的配置文件,在这里我们对这个类省略不讲,大家可以下去自己实现。
最后,我们的客户类调用为:

Animal animal = Factory.getInstance(NameParser.getName(“cat”));

If(animal!=null)
{
       animal.say();
}
这样,我们每增加一个新类,只需要到配置文件里去做相应的配置即可。
有了上面的这个方法以后,我们就能想明白我们大名鼎鼎的Struts,它就是用了模板方法模式,将我们的业务逻辑延迟到我们自己的Action类里去实现,然后再通过配置文件,得到我们自己的Action类所对应的带路经的类名,然后通过工厂模式取得对应的对象,最后调用我们的业务逻辑。正是通过反射加工厂模式,再结合配置文件,使得Struts有了很好的扩展性:无论我们增加多少个Action类,只要我们在配置文件里将这些Action配置好,Struts就能将我们的Action正确的执行。
同样的道理,大家也可以去看看我们现在用得很广泛的IOC容器Spring,看看它是怎么通过配置文件和反射来完成对我们的对象的管理的。

抱歉!评论已关闭.