分享

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

 印度阿三17 2021-02-02

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

软件设计的“七宗罪”:

  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...");
}

@Override
public void setVelocity(double velocity) {
this.velocity = 0;
}

@Override
public 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./content-4-843551.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多