面向对象的三大特性和五大设计原则

  • 特性
    • 封装
    • 继承
    • 多态
  • SOLID 设计原则
    • 单一职责原则
    • 开放封闭原则
    • 里式替换原则
    • 接口隔离原则
    • 依赖倒置原则
      • 台灯和按钮的例子:
      • 替换数据库
  • 其他设计原则
    • 合成复用原则
    • 迪米特法则

面向对象的 SOLID 原则

特性

封装

隐藏对象的属性和实现细节,通过对外暴露的接口控制程序中属性的读写。

封装需要把所有属性私有化,对每个属性提供 getter 和 setter 方法。如果有一个带参的构造函数的话,还需要写一个不带参的构造函数。

继承

通过继承可以实现代码的复用。

缺点是提高了类之间的耦合性

多态

基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。

要想实现多态,需要在继承+覆盖或重载。

  • 覆盖,是指子类重新定义父类的虚函数的做法。
  • 重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

SOLID 设计原则

面向对象的 SOLID 设计原则:

  • S-单一职责原则
  • O-开放封闭原则
  • L-里氏替换原则
  • I-接口隔离原则(Interface segregation principles,ISP)
  • D-依赖倒置原则(Dependency inversion principle,DIP)

单一职责原则

一个类只做一件事。

比如订单和账单,都包括订单号、创建时间等信息,但是如果只用一个类来表达,会导致:

  • 特有属性和共有属性相互掺杂,难以理解
  • 修改一个场景可能会影响另一个场景

开放封闭原则

实体应该对扩展是开放的,对修改是封闭的。即可扩展而不可修改。哪里可能变化,就在哪里抽象。

例如,对于支付场景,如果将多种支付方式整合到一个类中,每次添加一种新的支付方式都需要变更这个类,并可能导致已有的支付方式受到影响而无法使用。可以将其抽象为抽象类或接口,每种支付方式继承抽象类或实现接口。

interface PayProcessor {
    Result handle(Param param);
}

public class AlipayProcessor implements PayProcessor {
    ...
}

public class WeChatPayProcessor implements PayProcessor {
    ...
}

里式替换原则

在对象出现的任何地方,都可以用其子类实例进行替换,而不会导致程序错误,且软件功能不受影响。

对于继承关系,只有满足里式替换原则才算合理。里氏替换原则的关键在于不能覆盖父类的非抽象方法。父类实现好的方法设定了一系列规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。

例如,B 在继承 A 时,修改了其非抽象方法:

class A { 
  public int func1(int a, int b) { 
    return a-b; 
  } 
}

class B extends A { 
  public int func1(int a, int b) { 
    return a+b; 
  } 

  public int func2(int a, int b) { 
    return func1(a,b)+100; 
  } 
}

public class Client {
  public static void main(String[] args) {
    A a = new A();
    System.out.println("100-50="+a.func1(100, 50));

    B b = new B();
    System.out.println("100-50="+b.func1(100, 50));
    System.out.println("100+20+100="+b.func2(100, 20));
  } 
} 

接口隔离原则

客户不应被强迫依赖它不使用的方法。一个类实现的接口中,包含了它不需要的方法。将接口拆分成更小和更具体的接口,有助于解耦,从而更容易重构、更改。

例如,假设对于支付接口,支付宝支持收费和退费,微信只支持收费。如果只抽象一个支付接口,则必须包括收费和退费两个方法:

interface PayChannel {
    void charge();
    void refund();
}

class AlipayChannel implements PayChannel {
    public void charge() {
        ...
    }

    public void refund() {
        ...
    }
}

class WeChatChannel implements payChannel {
    public void charge() {
        ...
    }

    public void refund() {
        // 没有任何代码
    }
}

此时微信不得不将退费实现成空方法。

要满足接口隔离原则,可以将支付接口拆成各包含一个方法的收费和退费两个接口。支付宝需要同时实现这两个接口,而微信只需要实现收费接口。

依赖倒置原则

  • 高层次的模块不应依赖低层次的模块,他们应该都依赖于抽象。
  • 抽象不应依赖于具体实现,具体实现应依赖抽象。

面向对象的三大特性和五大设计原则_第1张图片
可以用于将业务和底层模块解耦(例如底层逻辑经常变化,或预计会在一段时间后替换底层模块)。

台灯和按钮的例子:

Button 对象用于感知外部环境。Lamp 对象用于影响外部环境。

下图直接使用继承。此时 Lamp 对象的任何修改都会影响到 Button 对象。这个模型可以通过反转 Lamp 对象的依赖性来改进。

public class Button {
    private Lamp lamp;
    public void Poll() {
        if (/*some condition*/) {
            lamp.TurnOn(); 
        }
    }
}

下图使用了依赖倒置原则。Button 对象现在拥有一个与 ButtonServer 相关的关联,它提供了 Button 可以用来打开或关闭某些东西的接口。Lamp 实现 ButtonServer 接口。此时,Button 对象可以控制所有实现了 ButtonServer 接口的对象,更灵活,同时还可以控制目前尚未创建的对象。

替换数据库

假设目前使用 MySQL,但是后面可能换成其它数据库。针对 MySQL 的实现类 MySQLDBUtil 编程的话,后续假设又创建了 OracleDBUtil,则替换的工作量相当大。而如果加一个抽象层接口 DBUtil,所有的数据库类(MySQLDBUtil、OracleDBUtil)都实现 MySQLDBUtil 接口,就可以轻松替换。

原设计,直接继承数据库操作类:
面向对象的三大特性和五大设计原则_第2张图片

新设计,增加抽象层接口。注意,这里用关联关系代替了继承关系,具体参考下面的合成复用原则:
面向对象的三大特性和五大设计原则_第3张图片

其他设计原则

合成复用原则

合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合和聚合关系,而不是继承。组合和聚合关系可以降低类之间的耦合度。如果使用继承,需要严格遵循里氏替换原则。

数据库工具类:

class DBUtil {

}

继承关系:

class UserDAO extends DBUtil {
    ...
}

聚合关系:

class UserDAO {
    private DBUtil util;
    ...
}

迪米特法则

迪米特法则又叫最少知道原则:一个对象应该对其他对象保持最少的了解。

迪米特法则特点:

  • 类之间解耦,降低耦合度
  • 会产生大量的中转类,导致系统的复杂度提高。

举个例子:连长、班长、士兵之间布置任务,连长只需要传达给其直接下属班长,班长再传达给其负责的士兵(下图左侧示例)。如果违反了迪米特法则,将导致混乱及高耦合度(下图右侧示例)。

面向对象的三大特性和五大设计原则_第4张图片

你可能感兴趣的:(设计模式)