常见的设计臭味和设计原则

来源:互联网 发布:linux cal windows 编辑:程序博客网 时间:2024/04/27 02:22

  在软件开发的过程中,常常有这样一种现象:起初我们对开发的系统架构非常清晰,但是随着开发的深入,或者因为功能的增加,或者因为需求的变更,我们可能逐渐偏离原来的设计并且发现开发工作很难进行下去。最后软件即使发生最细微的变化也会带来灾难性的后果,有人把这时的软件比作“坏面包”或者“坏鸡蛋”。它们都说明了一个共同的问题——腐化的软件设计,这时软件设计的臭味就表现出来了。

常见的软件设计中的臭味有:

  1. 僵化性:软件难以修改。修改花费的代价巨大;
  2. 脆弱性:一个修改可能引发很多意想不到的问题;
  3. 顽固性:设计中包含了对其他系统有用的部分,但是把这部分从系统中分离出来所需要的努力和风险非常之大;
  4. 粘滞性:当面临修改时,开发人员有两类修改方法:一是保持设计;而是破坏设计(拼凑方法。当可以保持系统设计的方法比破坏设计的方法更难应用时,系统就有很高的粘滞性;
  5. 不必要的重复:是忽略抽象的结果;
  6. 不必要的复杂性:系统包含了当前没有用的组成部分;
  7. 晦涩性:模块难以理解,代码晦涩难懂。

  软件为什么会腐化,简而言之就是因为没有遵循设计原则。经典的几种面向对象设计原则(不同于GOF设计模式)包括:SRP、OCP、LSP、DIP、ISP、LoD六种设计原则。下面分别进行详细介绍(附带实例)。

NO1 SRP(Single Responsibility Principle)单一职责原则

  单一职责,简单地说即尽量让一个类的职责集中单一,如果它有多个职责应该把他们分出去。单一职责体现了“高内聚,低耦合”的理念。
  假如有一个Server接口,如下图:
Server接口
  由UML图可以看出Server主要有两种职责,连接管理(建立和断开)和消息管理(发送和接收)。也许你会说这样的设计很合理啊,但是如果应用程序的变化影响到二者中的一个,就不得不都进行重新编译。所以我们必须把它们分离。如下图:
分成两个接口

NO2 OCP(Opean Closed Principle)开放封闭原则

  开放封闭原则的核心思想是:软件实体应该是可扩展,而不可修改的。也就是对扩展是开放的,而对修改是封闭的。
  或许你会疑惑,认为这两个特征相互矛盾。因为扩展模块的常用方法就是修改模块的源代码。那么怎么样才能扩展模块的功能但是不去修改这个模块呢?答案在于“抽象”。实现开放-封闭的核心思想就是抽象编程而不是具体编程。让类依赖于固定的抽象体,对修改就是封闭的;通过面向对象的继承和多态可以实现对抽象体的继承,通过覆盖其方法来改变固有行为,实现新的扩展方法,所以对扩展是开放的。
  场景:去银行办业务时,如果业务人员面对多种多样的客户需求,而且有很多人排在队伍中等待时,那么人们常常一等就是几十分钟甚至几个小时。因为业务人员要在不同的需求之间不断切换,电脑的界面也不断地切换。如下图:
这里写图片描述
对于BankHandle类我们可能有以下代码:

/**银行业务员操作入口**/public class BankHandle{   //声明银行业务操作进程对象   private BankProcess bProc = new BankProcess();   //定义银行员工的业务操作   public void handleProcess(Client client){       switch(client.ClientType){          case "存款用户":                bProc.deposit();                break;          case "转账用户":                bProc.transfer();                break;          case "取款用户"                bProc.drawmoney();               break;          }      }}

   每个BankHandle对象接收不同客户的请求,并选择不同的操作流程处理(switch语句),这种”被动式”的选择浪费了很多时间。而且容易在不同的流程中发生错误。更糟的是,当添加新的业务时,必须修改BankProcess中的方法,同时还得修改switch增加新业务(即对修改是开放的)。
  考虑OCP原则,我们将银行系统中最有可能扩展的部分分离出来,形成统一的接口处理。在银行系统中最有可能扩展的因素就是业务变更或者增加。我们应该将业务流程抽象为借口,而各种不同的业务只需要通过继承或者实现抽象出来的业务抽象体就可以了。这样既达到了对修改的封闭,也实现了对扩展的开放。
  修改后的设计图如下:
这里写图片描述
  有了上述抽象后,BankHandle类中代码就变得很精简了。如下:

public class BankHandle{    private IBankProcess bProc = null;    public void HandleProcess(){        bProc = client.createProcess();        bProc.process();    }}

  这样当有新业务添加时你也应该知道该怎么办了吧?就不再赘述。

NO3 LSP(Liskov Substitution Principle)-Liskov替换原则

  LSP原则的核心思想是:子类型必须能够替换它们的基类型。即:在S对象中用到了对象O1(属于类型T1), 那么用O2(属于类型T2, T2是T1的子类型)替换O1后,对于S能够起到相同的作用。这点在《代码大全》第五章“软件构建中的设计”也有提到。
  Liskov替换原则是关于继承机制的应用原则,是实现OCP原则的具体性规范,违反了Liskov原则也必然违反OCP原则。
  经典的一个场景是“正方形不是矩形”。用面向对象的程序设计方法来看,正方形和矩形之间应该是IS-A关系,因此正方形类Square应该是长方形Rectangle类的子类。即如下图:
正方形和矩形
我们先构造一个长方形(Rectangle)类:

//Rectangle.javapublic class Rectangle{    private double width;    private double height;    public Rectangle(double width, double height){        this.width = width;        this.height = height;    }    //get和set方法略}

接下来构造正方形Square类。

public class Square{    public Square(double value){        width = height = value;    }    public setWidth(double widthVal){        width = widthVal;    }    public setHeight(double heightVal){        height = heightVal;    }//get方法省略}

当设置了Square的宽时,它的长也会随着改变。同理设置长时宽也随之改变。
现在我们再考虑函数f,

public Rectangle f(Rectangle r){    while(r.height<r.width){        r.height++;    }    return r;}

  如果传给f一个Rectangle(长宽不同)的对象,是没有问题的,但是如果传给一个Square对象问题就暴露了。因为正方形的宽是不会变的,这样就破坏了Square对象。
  对于一般的数学思想而言,正方形可以看做是长方形,但是从程序设计的角度(行为方式上)来看,正方形决不是长方形。对象的行为方式才是软件真正关注的问题。Liskov替换原则清楚地指出,面向对象设计中的IS-A关系是就行为而言的,行为方式是可以合理假设的,是客户程序所依赖的。
  Liskov原则让我们得出一个结论:一个模型如果孤立地看,并不具有真正意义上的有效性,模型的有效性只能通过它的客户程序来表现。
  解决上述问题的方案是:用提取公共部分来代替继承。
  即增加一个Quardrangle类,Square类和Rectangle类都继承它。Quardangle类中有长方形和正方形的公共方法。长方形和正方形中除了继承下来的公共方法外还可以添加各自的方法。UML图描述如下:
公共父类

NO4 DIP(Dependency Inversion Principle)-依赖倒置原则

  在传统的结构化编程中,最上层的模块通常都要依赖下面的子模块来实现,也称为高层模块依赖低层模块。在面向对象程序设计中,这种依赖关系式不允许的。依赖倒置原则就是要逆转这种依赖关系。
  如果高层模块依赖低层模块,那么对低层模块的改动就会直接影响到高层模块,从而迫使他们依次改动。本来应该是高层的策略设置模块去影响低层的细节实现模块的。
  依赖倒置原则就是通过抽象机制有效地解决类层次之间的关系,降低耦合的力度。依赖倒置原则的核心是实现对抽象的依赖。具体体现在两方面:

  1. 高层模块不应该依赖底层模块,二者都应该依赖抽象;
  2. 抽象不应该依赖细节,细节应该依赖于抽象。

  依赖于抽象是指程序中所有依赖关系都应该终止于抽象类和接口,而不应该依赖于具体类。具体描述如下:
  任何变量都不应该持有一个指向具体类的引用。
  任何类都不应该从具体类派生。
  任何方法都不应该重写它的基类中的已实现的任何方法。

  一般而言我们日常面对的具体类都是不稳定的,不应该依赖于这些不稳定的具体类,而应该把它们隐藏在相对稳定的抽象接口后面,这样就隔离了它们的不稳定性。
  仍然讨论银行储蓄业务,我们现在讨论该业务系统中的客户。客户类代码如下:

public class Client{    private String clientType;    public Client(String clientType){        this.clientType = clientType;    }    public IBankProcess createProcess(){    //创建业务        switch(clientType){            case “储蓄用户”:                return new DepositProcess();            case “转账用户”:                return new TransferProcess();            case “取款用户”:                return new DrawMoneyProcess();        }        return null;    }}

  上面的代码中如果增加新的业务,我们就必须在很长的分支语句中加入新的处理选项,即在系统中增加一次对客户类别的依赖。
  要解决这个问题,我们必须找出潜在的抽象,让银行业务员依赖于此抽象。而抽象的办法是在BankHandle和Client之间增加一个IClient接口来消除客户类别的影响。如图:
这里写图片描述

  这样之后如果我们要添加一个新业务,只需要派生出相关的业务操作和用户即可。

NO5 ISP(Interface Segregation Principle)-接口分离原则

  一个类对另外一个类应该表现成依赖尽可能小的接口。接口分离原则的核心是使用一个小的专门的接口而不是使用一个大的总接口。具体而言体现在两个方面:

  1. 接口应该是内聚的,应该避免出现“胖”接口;
  2. 一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖于不用的方法,这将导致接口污染。

  关于ISP原则具体就不再展开。

NO6 LoD(Law of Demeter, LoD) -迪米特法则

  一个对象应当对其他对象有尽可能少的了解
  核心观念就是类间解耦——弱耦合
  对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息

六大设计原则介绍:http://www.cnblogs.com/dolphin0520/p/3919839.html