面向对象设计原则

Posted by fzy on January 26, 2024

第13章: 面向对象设计原则

面向对象贼拉好,主要就是可以实现可维护性的复用。

重点掌握面向对象的一些基本设计原则

image-20231212170524566

LSP:里氏替换原则

任何父类出现的地方,子类都可以出现

image-20231212171955165

子类对象必须可以替换基类对象,但是反过来不成立

只要有可能,就应该从抽象类中继承,不要从具体类中继承。

OCP:开闭原则

对拓展开放,对修改关闭

  • 对拓展开放:模块的行为可拓展
  • 对修改关闭:对模块行为拓展的时候,不必改动模块的源代码或者二进制代码。

也就是说OCP原则认为应该试图去设计出永远都不用改变的模块,其关键在于抽象

可以看课件中手开门开冰箱的例子。

image-20231213122808404

SRP:单一职责原则

对于一个类,应该仅有一个引起它变化的原因。

  • 一个类承担的职责越多,其被复用的可能性越小。因为承担的职责过多就相当于将职责耦合在一起。
  • 类的职责包括:
    • 数据职责:通过属性体现
    • 行为职责:通过方法体现
  • SRP体现“高内聚,低耦合”

增加新的类,让每一个类只有一个职责

image-20231213121709348

上面的图中表示的类职责过多,耦合在了一起,违反了SRP原则,用SRP进行重构之后得到以下的类图

image-20231213121810401

ISP:接口隔离原则

  • 客户不应该依赖他们用不到的方法,只给每个用户它所需要的接口
  • 以一个类实现多个接口,而客户仅仅获知必须的接口

ISP的本质

  • 使用多个专门的接口比使用单一的接口好。
  • 一个类对另一个类的依赖性应当是建立在最小的接口上
  • 避免接口污染

下图中2比1好,因为客户仅获取了其需要的接口

image-20231213122252370

DIP:依赖倒置原则

  • 高层模块不应该依赖于低层模块,而是都应该依赖于抽象
  • 抽象不应该依赖于细节,细节应该依赖于抽象
  • 针对接口编程,而不要针对实现编程

总的来说,依赖倒转就是:代码要依赖于抽象的类,不要依赖具体的类;要针对接口或对象编程,而不是针对具体编程。

DIP是实现OCP的一种主要手段。

image-20231213122904875

启发式原则

  • 任何变量都不应该拥有指向具体类的指针或者引用
  • 任何类都不应该从具体类派生
  • 任何方法都不应该改写其任何基类中已经实现的方法

向上转型、接口、多态

针对接口编程

  • 不将变量声明为某个特定的具体类的实例对象,而让其遵从抽象类定义的接口。实现类仅实现接口,不添加方法。

依赖于抽象

  • 任何变量都不应该持有一个指向具体类的指针或者引用
  • 任何类都不应该从具体类派生
  • 任何方法都不应该覆写它的任何基类中已经实现的方法

image-20231213125413939

组合复用原则

  • 优先使用(对象)组合,而非(类)继承

实现复用的方法:

  • 继承复用:实现简单,易于拓展。破坏系统的封闭性,从基类继承而来的实现是静态的,不能在运行时动态改变,缺乏灵活性(白盒复用)
  • 组合复用:耦合度较低,可以灵活选择成员对象的操作,可以在运行时动态改变。(黑盒复用)

组合的优点和缺点

优点

  • 仅通过被包含的对象的接口来对其进行访问
  • 内部实现细节不可见
  • 相互依赖小
  • 每个类只专注于一个任务
  • 通过获取指向其他的具有相同类型的对象引用,可以在运行期间动态定义组合

缺点

  • 导致系统中对象过多
  • 为了能将多个对象组合使用,必须仔细定义接口

继承的优点和缺点

优点

  • 容易进行新的实现
  • 易于修改和拓展被复用的实现

缺点

  • 破坏了封装性
  • 白盒复用,内部细节可见
  • 父类更改了,子类不得不更改
  • 从父类继承来的实现不能在运行期间改变

1. 单一职责原则(Single Responsibility Principle - SRP):

概念: 一个类应该只有一个改变的理由,即一个类应该只有一个单一的责任

通俗解释: 就像是一个人应该只有一份工作一样,一个类也应该只负责一种类型的任务。这使得类更加可维护和灵活。

2. 开闭原则(Open/Closed Principle - OCP):

概念: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。只增不改

通俗解释: 就像是房子的插座,你可以插入不同的电器而无需改变插座结构。这鼓励通过扩展而不是修改来添加新功能。

3. 里氏替换原则(Liskov Substitution Principle - LSP):

概念: 子类型必须能够替换其基类型不影响程序的正确性。

通俗解释: 就像是可以用猫替换狗,因为它们都是动物。子类应该能够被父类替代而不引起问题。

4. 接口隔离原则(Interface Segregation Principle - ISP):

概念: 不应该强迫客户端依赖于它们不使用的接口。多个接口

通俗解释: 就像是一个人只需要了解他们需要的信息一样,一个类也应该只提供客户端需要的接口。这减少了对不必要信息的依赖。

5. 依赖反转原则(Dependency Inversion Principle - DIP):

概念: 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象接口通信

通俗解释: 就像是电灯不依赖于电源的类型一样,而是依赖于插座的抽象接口。高层次的模块和底层次的模块都应该依赖于抽象的接口,而不是具体的实现。

6. 组合复用原则(Composition/Aggregation Reuse Principle):

概念: 尽量使用组合或聚合,而不是继承。

通俗解释: 就像是一个汽车可以由许多不同的零部件组成一样,而不是通过继承一个庞大的“超级汽车”类。这样更加灵活,降低了耦合性。

工厂模式(Factory Pattern):

概念: 工厂模式是一种创建型设计模式,它提供了创建对象的接口,但将对象的实际创建过程推迟到子类

通俗解释: 就像是在工厂里定制产品一样,工厂模式通过一个共同的接口来创建对象,但具体创建的方式由子类实现。

例子: 如果有一个抽象类 Vehicle,工厂模式可以有两个子类 CarFactoryBikeFactory,它们分别创建汽车和自行车。

策略模式(Strategy Pattern):

概念: 策略模式是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以互相替换

通俗解释: 就像是在商场购物时可以选择不同的支付方式一样,策略模式允许在运行时选择算法

例子: 如果有一个支付类 Payment,策略模式可以定义多个支付算法,如 CreditCardPaymentPayPalPayment,并在运行时选择使用哪种支付方式。

适配器模式(Adapter Pattern):

概念: 适配器模式是一种结构型设计模式,它允许接口不兼容的类能够一起工作,将一个类的接口转换成另一个类的接口。

通俗解释: 就像是使用电源适配器来让不同国家的插头能够连接到同一个电源上一样,适配器模式允许不同接口的类一起工作。

例子: 如果有一个旧的类 OldSystem,而新的系统需要使用 NewSystem,适配器模式可以创建一个适配器,使得旧系统的接口变得兼容新系统。

观察者模式(Observer Pattern):

概念: 观察者模式是一种行为型设计模式,它定义了一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新

通俗解释: 就像是订阅了报纸的人会在新报纸出来时收到通知一样,观察者模式允许一个对象改变状态时通知其他相关的对象。

例子: 如果有一个主题(Subject)类,如天气类,多个观察者类(如手机、电视)可以订阅主题,当天气变化时,观察者们会收到通知并更新显示。

黑板模式(Blackboard Pattern):

概念: 黑板模式是一种协作型设计模式,它允许各个组件之间共享信息,通过一个中央的数据结构(黑板)进行通信。

通俗解释: 就像是在黑板上写下信息,各个组件可以读取和修改这些信息,实现共享。

例子: 在一个协同编辑系统中,黑板模式可以用于共享文档的状态,各个编辑器组件可以读取和修改这个状态,以保持同步。