软件设计的“七宗罪”及设计模式的七大原则

编写软件过程中,面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序,具有更好的代码重用性、可读性、可扩展性、可靠性,使程序呈现高内聚低耦合的特性。

软件设计的“七宗罪”:

  1. 僵化性

  2. 脆弱性

  3. 牢固性

  4. 粘滞性

  5. 不必要的重复

  6. 不必要的复杂性

  7. 晦涩性

1. 僵化性

僵化性是指难以对软件进行改动,即使是简单的改动。如果单一的改动会导致有依赖关系的模块中的连锁改动,那么设计就是僵化的。必须要改动的模块越多,设计就越僵化。

2. 脆弱性

脆弱性是指在进行一个改动时,程序的许多地方就可能出现问题,即设计易于遭破坏。并且,往往是出现新问题的地方与改动的地方并没有概念上的关联。

3. 牢固性

牢固性是指设计中包含了对其他系统有用的部分,但要想把这些部分分离出来所需要的努力和风险是巨大的,即设计难以复用。

4. 粘滞性

有的时候,一个改动可以以保持原有的设计意图和原有的设计框架的方式进行,也可以以破坏原始的意图和框架的方式进行。第一种办法无疑会对系统的未来有利,第二种办法是去权宜之计,可以解决短期问题,但是会牺牲长期利益。如果第二种办法比第一种办法容易得多的话,程序员就有可能牺牲长期利益,采取权宜之计,在一个通用的逻辑中建立一种特例,以便解决眼前的问题。一个系统的设计,如果总是使得第二种办法比第一种办法来得容易,说明粘滞性过高。一个粘滞性过高的系统会诱使维护它的程序员采取错误的维护方案。

5. 不必要的重复

大量的重复代码往往是由于开发人员忽略了抽象,从而使系统不易理解,而且,软件中的重复代码,也会使系统的改动变得困难,不易于系统的维护。

6. 不必要的复杂性

不必要的复杂性是指设计中包含了当前没有用的部分,即过分设计。例如,对于逻辑复杂、技术先进的过度追求,导致了技术框架虽看似华丽却复杂难用。再例如,在设计产品功能或界面交互时,过度追求体验完美、需求满足却导致实际体验下降、功能没人用。所以,软件设计应“有所为有所不为”。

7. 晦涩性

晦涩性是指模块难于理解。代码随时间而不断演化,往往会变得越来越晦涩、可读性差。代码晦涩难懂常体现在如下几点:

  1. 代码不良

  2. 代码的格式不正确或不一致

  3. 代码中包含冗余代码

  4. 代码中包含未备注的低层次优化

  5. 代码逻辑过于复杂

设计模式七大原则有:

  1. 单一职责原则

  2. 接口隔离原则

  3. 依赖倒置原则

  4. 里氏替换原则

  5. 开闭原则

  6. 迪米特法则

  7. 合成复用原则

1. 单一职责原则

单一职责原则SRP(The Single Responsibility Principle)指的就是一个类应该仅有一个引起它变化的原因。这是最简单、最容易理解却最不容易做到的一个设计原则。

对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。

单一职责原则注意事项和细节:

  1. 降低类的复杂度,一个类只负责一项职责。

  2. 提高类的可读性,可维护性

  3. 降低变更引起的风险

2. 接口隔离原则

接口隔离原则ISP(The Interface Segregation Principle)指的是“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上”。就是说,“不应该强迫客户依赖于它们不用的方法”。再通俗点说,不要强迫客户使用它们不用的方法,如果强迫客户使用它们不需要的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。

下面是一个违反了接口隔离原则的例子:

interface IWorker {public void work();public void eat();}class Worker implements IWorker {  // 普通工人public void work() {// ...}public void eat() {// ...}}class SuperWorker implements IWorker {  // 高级工人public void work() {// ...}public void eat() {// ...}}public Manager {  // 管理工人的管理者private Worker worker;public void setWorker(IWorker w) {this.worker = w;}public void manage() {worker.work();}}

上述例子中,有普通和高级两种工作者,他们都需要工作和吃饭。但是,现在来了一批机器人,机器人作为公司的工作者,一方面需要工作,需要实现IWorker接口;另一方面机器人不需要吃饭,又不需要实现IWorker接口。这种情况下,IWorker就被认为是一个被污染了的接口。

如果我们保持上面那样的设计,那么Robot类就将被迫实现eat()方法,当然我们可以写一个哑类让它什么也不做,但是这会对程序造成不可预料的结果,例如管理者看到报表中显示被带走的午餐多于实际的人数。

通过接口隔离原则,我们应该把IWorker分离成两个接口,如果Robot类需要添加特有而工人没有的方法,例如充电功能,我们可以再创建一个新的IRechargeable接口,其中包含一个重新充电的方法recharge。

更改后的代码如下:

interface IWorkable {public void work();}interface IFeedable {public void eat();}interface IRechargeable {public void recharge();}class Worker implements IWorkable, IFeedable  {  // 普通工人public void work() {// ...}public void eat() {// ...}}class SuperWorker implements IWorkable, IFeedable{  // 高级工人public void work() {// ...}public void eat() {// ...}}class Robot implements IWorkable, IRechargeable {  // 机器人public void work() {// ...}public void recharge() {// ...}}public Manager {  // 管理工人的管理者private Worker worker;public void setWorker(IWorker w) {this.worker = w;}public void manage() {worker.work();}}

总之,接口隔离原则是对接口进行规范约束,其包含以下含义:

  1. 接口尽量要小,这是接口隔离原则的核心定义,不要出现臃肿的接口

  2. 接口要高内聚

  3. 定制服务,只提供访问者需要的方法

  4. 接口设计是有限度的

3. 依赖倒置原则

依赖倒置原则DIP(The Dependency Inversion Principle)指的是“高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象”。

好抽象的定义,我们直接上代码,先看一个不符合DIP原则的例子

public abstract class Light {public abstract void turnOn();    public abstract void turnOff();}public class BulbLight extends Light {@Override    public void turnOn() {System.out.println("BulbLight turned on...");    }    @Override    public void turnOff() {System.out.println("BulbLight turned off...");    }}public class TubeLight extends Light {@Override    public void turnOn() {System.out.println("TubeLight turned on...");    }    @Override    public void turnOff() {System.out.println("TubeLight turned off...");    }}public class ToggleSwitch {  // 开关类    public void toggle(Light light) {light.turnOn();        light.turnOff();    }    public static void main(String[] args) {ToggleSwitch toggleSwitch = new ToggleSwitch();        toggleSwitch.toggle(new BulbLight());        toggleSwitch.toggle(new TubeLight());    }}

上述例子中,开关类ToggleSwitch依赖于Light类,在目前的设计中,ToggleSwitch可以控制灯,但是控制一台电视机就很困难,因为无法让电视机继承Light。这里ToggleSwitch属于高层模块,它依赖了低层模块Light,不符合依赖倒置原则的“高层模块不应该依赖于低层模块”。所以我们可以再定义一个开关接口,让ToggleSwitch依赖于改接口,而Light和TV抽象类只需继承该接口,让其子类去实现即可。

public interface Switchable {void turnOn();    void turnOff();}public abstract class Light implements Switchable  {}public abstract class TV implements Switchable {}public class BulbLight extends Light {@Override    public void turnOn() {System.out.println("BulbLight turned on...");    }    @Override    public void turnOff() {System.out.println("BulbLight turned off...");    }}public class TubeLight extends Light {@Override    public void turnOn() {System.out.println("TubeLight turned on...");    }    @Override    public void turnOff() {System.out.println("TubeLight turned off...");    }}public class Television extends TV {@Override    public void turnOn() {System.out.println("Television turned on...");    }    @Override    public void turnOff() {System.out.println("Television turned off...");    }}public class ToggleSwitch {public void toggle(Switchable switchable) {switchable.turnOn();        switchable.turnOff();    }    public static void main(String[] args) {ToggleSwitch toggleSwitch = new ToggleSwitch();        toggleSwitch.toggle(new BulbLight());        toggleSwitch.toggle(new TubeLight());        toggleSwitch.toggle(new Television());    }}

依赖倒置原则的核心就是面向接口编程,在实际编程中,一般需要做到如下三点:

  1. 低层模块尽量都要有抽象类或接口,或者两者都有

  2. 变量的声明类型尽量是抽象类或接口

  3. 使用继承时要遵循里氏替换原则

4. 里氏替换原则

里氏替换原则LSP(The Liskov Substitution Principle)指的是“如果对每个类型为T1的对象o1,都有类型为T2的对象o2,对于所有定义了T2的所有程序P来说,在所有的对象o2都被替换成o1并且T1是T2的子类型时,程序P的行为没有发生变化”。通俗地讲,就是子类型能够完全替换父类型,而不会让调用父类型的客户程序从行为上有任何改变。

下面举一个例子来说明里氏替换原则

“鸵鸟非鸟”是一个理解里氏替换原则的经典例子。我们设计一个与鸟有关的系统,我们先假设鸵鸟属于鸟,将鸵鸟类继承鸟类,鸟类的所有特性和行为都被鸵鸟类继承,比如,羽毛,有翅膀,飞行;当然鸵鸟不会飞,只能把飞行速度设为0了。假设有以下鸟类:

class Bird {double velocity;public abstract void fly();public void setVelocity(double velocity) {this.velocity = velocity;}public double getVelocity() {return this.velocity;}}

鸵鸟类:

class Ostrich extends Bird {double velocity;public void fly() {System.out.println("I'm Ostrich, I cant't fly...");}@Overridepublic void setVelocity(double velocity) {this.velocity = 0;}@Overridepublic double getVelocity() {return 0;}}

测试类TestBird:

测试不同鸟飞行3000米所需的时间

class TestBird {public double calcFlyTime(Bird bird) {double distance = 3000;return distance / bird.getVelocity;}}

我们拿上述代码来测试,只要是会飞的鸟,速度再慢也不可能为0吧,所有应该都是没问题的;但如果我们用鸵鸟来测试,程序就会抛出一个 / by zero 的异常,明显不符合我们的预期。

我们得出结论:在calaFlyTime方法中,Bird类型的参数是不能被Ostrich类型的参数所代替,如果进行了替换就得不到预期的结果。因此,Ostrich类和Bird类之间的继承关系违反了里氏替换原则,它们之间的继承关系不成立,所以,鸵鸟不是鸟!

简言之,里氏替换原则为继承定义一个规范,简单地概括为4层含义:

  1. 子类必须完全实现父类的方法,且方法对子类是有意义的

  2. 子类可以有自己的个性

  3. 覆盖或者实现父类方法时输入参数可以被放大(只要传入的参数是子类的类型,都可以当做参数进行传递)

  4. 覆盖或者实现父类方法时输出参数可以被缩小(父类的一个方法的返回值是一个类型T,子类的相同方法(重载或重写)的返回值是S,要么S和T是同一个类型,要么S是T的子类)

5. 开闭原则

开闭原则OCP(The Open Closed Principle)指的是一个软件实体应当对扩展开放,对修改关闭

下面通过一个例子来说明开闭原则。

假设现在需要实现一个加法的功能,代码如下:

public class Calculate {public int add(int a, int b) {return a   b;}}

现在的问题是,需求变了,要求是需要实现一个减法的功能,如下:

public class Calculate {public int add(int a, int b) {return a   b;}public int sub(int a, int b) {return a - b;}}

如果需求再变,还要求实现乘法和除法的工程,继续修改代码:

public class Calculate {public int add(int a, int b) {return a   b;}public int sub(int a, int b) {return a - b;}public int mul(int a, int b) {return a * b;}public int div(int a, int b) {return a / b;}}

如果需求再变,那么又要推翻之前设计的系统,很明显这样的做法是不可取的,在设计上出现了问题,明显违反了“开闭原则”。对此,我们可以通过创建抽象来隔离以后将要发生的同类变化。

6. 迪米特法则

迪米特法则DP(Demeter Principle)又称最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。

迪米特法则还有个更简单的定义:只与直接的朋友通信。直接的朋友是指:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。

下面我们通过一个例子来说明:

假设一公司有技术部和销售部两个部门,两个部门的员工类分别为TechDepEmployee和SaleDepEmployee ,公司方便管理员工,两个部门都有独自的管理者,分别为TcehDepManager 和SaleDepManager ;另外,又有一个公司的总管理者EnterpriseManager ,设计如下:

public class TechDepEmployee {  // 技术部员工private String id;// ...}public class SaleDepEmployee {  // 销售部员工private String id;// ...}public class TcehDepManager {  // 技术部管理者public List<TechDepEmployee> getAllEmployee() {// ...}}public class SaleDepManager {  // 销售部管理者public List<SaleDepEmployee> getAllEmployee() {// ...}}public class EnterpriseManager {  // 公司管理者public void printAllEmployee(TcehDepManager tech, SaleDepManager sale) {List<TechDepEmployee> techList = tech.getAllEmployee();List<SaleDepEmployee> saleList = sale.getAllEmployee();for (TechDepEmployee e : techList) {System.out.println(e);}for (SaleDepEmployee e : saleList) {System.out.println(e);}}}

现在我们的公司的管理者想要查看两个部门的员工信息,调用了printAllEmployee方法,在这个方法中,传入的参数TcehDepManager 类和SaleDepManager 类是EnterpriseManager 类的直接朋友,而TechDepEmployee类和SaleDepEmployee类都是通过调用getAllEmployee()方法得到的,不是EnterpriseManager 类的直接朋友,所以违反了 迪米特法则

改进后的代码如下:

public class TechDepEmployee {private String id;// ...}public class SaleDepEmployee {private String id;// ...}public class TcehDepManager {public List<TechDepEmployee> getAllEmployee() {// ...}public void printAllEmployee() {List<TechDepEmployee> list = this.getAllEmployee();for (TechDepEmployee e : list) {System.out.println(e);}}}public class SaleDepManager {public List<SaleDepEmployee> getAllEmployee() {// ...}public void printAllEmployee() {List<SaleDepEmployee> list = this.getAllEmployee();for (SaleDepEmployee e : list) {System.out.println(e);}}}public class EnterpriseManager {public void printAllEmployee(TcehDepManager tech, SaleDepManager sale) {tech.printAllEmployee();sale.printAllEmployee();}}

迪米特法则注意事项和细节:

  1. 迪米特法则的核心是降低类之间的耦合

  2. 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系

7. 合成复用原则

合成复用原则CRP(Composite Reuse Principle)又叫组合/聚合复用原则,指的是尽量使用组合/聚合的方式,而不是使用继承。因为继承是对象间耦合度最大的一种关系,而在程序中增加耦合无疑是给后期的开发和维护增加负担。

关于组合/聚合等类与类之间的关系可以参考UML类图简介及类与类之间的关系

来源:https://www.icode9.com/content-4-843551.html

(0)

相关推荐

  • 如何学习23种设计模式及其思想?

    感觉设计模式是看着简单 ,但是一用就不会,23种设计模式,学的人头大,相信大家都是这样的 设计模式在程序员的面试中会被考到,通常是介绍其原理并说出优缺点.或者对比几个比较相似的模式的异同点.在笔试中可 ...

  • 软件设计的六大原则剖析

    我们平时编写代码时,很少有人为了刻意迎合软件设计原则而编写.其实,有时候是你用到了其中的某个或多个设计原则,而不自知而已.也有可能是有的人压根就不知道设计原则是什么. 不过,没关系,为了搞明白既抽象又 ...

  • 图解Java设计模式之设计模式七大原则

    图解Java设计模式之设计模式七大原则 2.1 设计模式的目的 2.2 设计模式七大原则 2.3 单一职责原则 2.3.1 基本介绍 2.3.2 应用实例 2.4 接口隔离原则(Interface S ...

  • 设计模式的七大原则(3) --依赖倒置原则

    前言 上一节我们说了接口隔离原则,就是让接口的职责最小化.这样对维护代码简单,调用方法也清晰. 这节我们来研究依赖倒置原则.这个原则我认为是特别特别重要的.在很多地方我们能看到.比如Dubbo中使用到 ...

  • 无废话设计模式(10)结构型模式--外观模式

    0-前言 外观模式定义:为子系统中的一组接口提供一个一致的界面,此模式定了一个高层接口    这一接口使得这一子系统更加容易使用: 1-实现 1-1.简单UML图: 1-2.代码实现 //1.子系统A ...

  • 阿里大佬告诉我,想学习设计模式,得先学好这些硬核技能

    回复"000"获取大量电子书 写在前面 我们继续学习架构师技能,今天是本系列的第二篇,希望大家持续关注. 可能你不是科班出生,甚至大学都没念,没背景没关系.我们只要每天进步一点点, ...

  • 设计模式的七大原则(6) --迪米特法则

    前言 迪米特法则,听名字有点奇怪,但是这个法则真的非常非常有意思,在我看来,这个法则其实描述的就是一个矜持的小姑娘,害羞的惹人怜爱.但是啊,姑娘虽好,切不可"贪杯"哦~ 基本介绍 ...

  • 设计模式的七大原则(5) --开闭原则

    前言 我们已经学习了单一职责原则,依赖倒置原则,接口隔离原则,李氏替换原则.可以说前面几个原则都是为了开闭原则奠定基础. 我们写的程序由于实际的情况可以一定程度上违背各种设计原则.但是,开闭原则我认为 ...

  • 设计模式的七大原则(4) --里氏替换原则

    前言 上一节中我们介绍了,依赖倒置,依赖倒置利用抽象的稳定性来架构我们的系统,是我们经常能遇到的一种原则,比如说面向接口编程. 这一节中,我们来说说里氏替换原则,这个原则其实非常非常的简单,其实与依赖 ...

  • 设计模式的七大原则(1) --单一职责原则

    前言 最近工作中备受打击,之前设计的很多程序都被老大否决,需要重构,让我好好看看设计模式.之前对这一块内容的确不怎么重视,感觉枯燥无聊又派不上用场.后来沉下心来研究了一番... 我靠,原来如此,之前写 ...

  • 设计模式-七大软件设计原则

    设计模式 参考资料 图解设计模式 大话设计模式 设计模式之禅 github我见过最好的设计模式 http://c.biancheng.net/view/1326.html 基本原则 开闭原则 在设计的 ...

  • Java设计模式-软件设计原则

    在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率.节约软件开发成本和维护成本. 1 开闭原则 对扩展开放,对修改 ...

  • 最简单直接地理解Java软件设计原则之里氏替换原则

    理论性知识 定义 里氏替换原则,Liskov Substitution principle(LSP). 抽象定义是下面这样的 如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义 ...

  • TRIZ在软件设计中的思考

    TRIZ是在创新领域应用非常广泛的方法,特别是硬件设计领域的创新发挥很大作用.其中:基于39个工程参数的39*39矛盾矩阵,是专门针对硬件领域的总结.通过查表可以查到对应的40个发明原则. 在软件领域 ...