代码改变世界

依赖倒置之我见

2017-03-15 21:36  没有波澜的天空  阅读(290)  评论(0编辑  收藏  举报

  .net 程序员对面向对象设计原则以及设计模式的重视似乎不如Java,包括许多有经验.net的程序员,也并没有将面向对象的思想渗透进项目中。我本身就是这样一个例子。C#和Java都是面向对象的语言,设计模式对两者是通用的,今天就来谈一谈我对面向对象设计原则之一—依赖倒置原则的理解,之所以选择这个原则,因为之前对这个原则很重要(个人认为是最重要的),而且我之前对这个原则的了解相当模糊。

  首先,依赖倒置的标准定义:

  高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

  这个定义听得人云里雾里,不知所云。究其原因,是几个概念不清楚。

  1、依赖,什么叫依赖?

  这个问题我探索了好久。面向对象的设计中,依赖是程序模块(普通类、抽象类、接口)的一种关系(关系还包括,关联,聚合,组合,继承等),

  依赖主要包括以下几种情况。

  a、ClassA中某个方法的参数类型是ClassB;  这种情况成为耦合;
  b、ClassA中某个方法的参数类型是ClassB的一个属性; 这种情况成为紧耦合;
  c、ClassA中某个方法的实现实例化ClassB;
  d、ClassA中某个方法的返回值的类型是ClassB;

  但是,用这个“依赖”来解释依赖倒置的定义是解释不通的,因为从这来看,“依赖”和继承是两个概念。但在依赖倒置的概念中,低层模块“依赖”抽象,细节“依赖”抽象,这里的“依赖”中,继承以及实现接口,也是一种“依赖”,所以不能用这里的“依赖”来代替依赖倒置中的“依赖”。

  那么依赖倒置中的依赖到底是什么含义呢,目前没有找到让我满意的答案,我姑且谈一下自己理解。我认为如果A模块(普通类,抽象类,接口)依赖B模块,那么如果B发生了变化(包括内部变化以及用别的模块来替代B)则可能要修改A,不然要出现错误(编译或功能),那么就认为A依赖B。这个理解很绕,但我目前实在想不出更好的词来解释“依赖”这个词了。

  2、倒置,为什么叫倒置?

  倒置是相对于我们的一般思路而言的。一般情况下,高层模块是依赖低层模块的。比如A是高层模块类,B是低层模块类,如果A调用B,那就要再A中实例化一个B的对象。这样,A就会依赖B。而依赖倒置做的就是要解除这种依赖,用的方法就是两者都依赖于抽象。

  依赖倒置要求软件设计中“面向接口编程”,其基本的原理是接口比实现稳定

  依赖倒置的具体实现网上有太多例子,但总感觉不够实用。因为总举什么人吃东西,车能跑之类的白痴例子,形象是形象,就是离实际的项目相差太远。相信这些人也没有人真正用过依赖倒置。

  我来举个实际的项目的例子。

  假如有一张表叫Member,存储在Sql Server中。要实现对这个表的访问(增删改查),按照正常的逻辑,应该有有两个类MemberBll,MemberDal。MemberBll中创建MemberDal调用MemberDal中的方法。

  程序结构如下:

  public class MemberDal

  {

    void Add()

    {

      ......

    }

    void Delete()

    {
      ......

    }

  }
  public class MemberBll

  {

    void Add()

    {

      var memberDal=new MemberDal();

      memberDal.Add();
    }   

    void Delete()

    {

        var memberDal=new MemberDal();

        memberDal.Delete();

    }   

  }

  这样考虑清晰明了,但很明显,这违反了依赖倒置原则,因为高层模块依赖低层模块,一旦低层模块发生改变,必然要修改上层模块。可能有人会问,低层模块不是很稳定吗?怎么会随便变?这个可不一定,现实项目中,修改底层的情况也是很常见的。而对于我们这个真实的项目,底层确实变了,怎么变呢,底层要切换成MySQL数据库。这个时候原来的设计就显得比较脆弱了。那么怎么才能算是一个好的设计呢。根据依赖倒置原则,高层模块不应该依赖低层,他们应该共同依赖抽象。所以应该针对低层模块抽象出一个接口,高层模块和低层模块都依赖这个接口。具体的实现如下:

 public interface IMemberDal

  {

 

    void Add();

 

    void Delete();

 

  }

 public class SQLServerMemberDal:IMemberDal

  {

    void Add()

    {

      ......

    }

    void Delete()

    {
      ......

    }

  }

public class MySqlMemberDal:IMemberDal

  {

    void Add()

    {

      ......

    }

    void Delete()

    {
      ......

    }

  }

 

  public class MemberBll

  {

    IMemberDal _iMemberDal;

    public MemberBll(IMemberDal iMemberDal)

    {
      _iMemberDal=iMemberDal;

    }

    void Add()

    {

      _iMemberDal.Add();
    }   

    void Delete()

    {

        _iMemberDal.Delete();

    }   

  }

   通过以上方式,便取消了底层和上层的依赖,即使再换用其他数据库(如Oralcle,Mongo),也不用再改上层代码了。以上采用了构造函数的形式进行了依赖注入,客户端只要在构造MemberBll时将数据访问层的依赖注入即可。除了这种方式还有其他几种,这个以后再说。

  谁也无法预知项目中哪些模块会放生变化,我们能做的就是多考虑一些,防患于未然,同时,也一定得把握好考虑的尺度,过度设计和没有设计一样可怕。