Lesson7 完善类的设计

1 面向对象的几个基本原则

1.1 UML类图简介

  • UML-Unified Modeling Language 统一建模语言,又称标准建模语言。是用来对软件密集系统进行可视化建模的一种语言。UML的定义包括UML语义和UML表示法两个元素。
  • 继承
    Pasted image 20241104161319.png
  • 实现
    Pasted image 20241104161334.png
  • 关联/组合(比依赖强):“拥有”
    Pasted image 20241104161449.png
  • 依赖:方法中”用到“
    Pasted image 20241104161514.png

1.2 面向抽象(接口、抽象类)原则

使用接口和同类型的组件通讯,即,对于所有完成相同功能的组件,应该抽象出一个接口或抽象类,它们都实现该接口/继承该抽象类。

形象化解释

面向抽象原则,就像是在一场盛大的舞会上,所有的舞者都遵循着一套优雅的舞蹈规则。在这个舞会上,每个舞者都代表着一个具体的类,而这套舞蹈规则就是抽象类或接口。抽象类或接口定义了舞者应该遵循的基本舞步,但具体的舞步如何跳,则由每个舞者自己决定。
例如,假设有一个抽象类叫做“舞者”,它定义了一个抽象方法叫做“跳舞”。这个抽象方法就像是一个基本的舞步模板,告诉所有的舞者,跳舞时应该有哪些基本的动作。但是,具体的舞步如何跳,则由每个具体的舞者类来决定。比如,有一个具体的舞者类叫做“华尔兹舞者”,它实现了“跳舞”方法,用自己独特的方式跳华尔兹。另一个具体的舞者类叫做“探戈舞者”,它也实现了“跳舞”方法,但用自己独特的方式跳探戈。
面向抽象原则的好处在于,它允许我们编写更加灵活和可扩展的代码。因为我们可以根据需要,随时添加新的舞者类,只要它们实现了“跳舞”方法,就可以加入到舞会中来。同时,我们也可以根据需要,随时修改已有的舞者类的舞步,而不需要修改其他的代码。
总的来说,面向抽象原则就像是一场盛大的舞会,所有的舞者都遵循着一套优雅的舞蹈规则,但每个舞者都可以用自己的方式跳自己的舞。这样的舞会,既优雅又充满活力,正是我们编写代码时所追求的目标。

1.3 优先使用组合少用继承原则

  • 继承的缺点
    • 继承破坏了封装性,通过继承进行复用也称“白盒复用,其缺点是父类的内部细节对于子类而言是可见的。
    • 子类和父类的关系是强耦合关系,也就是说当父类的方法的行为更改时,必然导致子类发生变化
    • 子类从父类继承的方法在编译时刻就确定下来了,所以无法在运行期间改变从父类继承的方法的行为
  • 组合的优点
    • 对象组合要求对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为被组合的对象的内部细节是不可见的,对象只以"黑箱"的形式出现。
    • 对象与所包含的对象属于弱耦合关系,因为,如果修改当前对象所包含的对象的类的代码,不必修改当前对象的类的代码。
    • 当前对象可以在运行时刻动态指定所包含的对象
举例说明

假设我们有一个 Car 类,它需要具备 Engine(引擎)和 Wheel(轮胎)的功能。我们可以通过组合 EngineWheel 类来创建 Car 类,而不是让 Car 类继承自 EngineWheel

1.4 开-闭原则

  • 对扩展开放,对修改关闭。指当一个设计中增加新的模块时,不需要修改现有的模块
  • 在给出一个设计时,应当首先考虑到用户需求的变化,将应对用户变化的部分设计为对扩展开放,而设计的核心部分是经过精心考虑之后确定下来的基本结构,这部分应当是对修改关闭的,即不能因为用户的需求变化而再发生变化,因为这部分不是用来应对需求变化的
举例说明

在Java中,开-闭原则通常通过以下方式实现:

  1. 抽象类和接口:通过定义抽象类或接口,可以为未来的扩展提供一个稳定的API。这样,当需要添加新功能时,可以创建新的实现类,而不需要修改现有的代码
  2. 多态:通过多态,可以在运行时动态地替换对象,而不需要修改代码。这样,当需要添加新功能时,可以创建一个新的类,并将其作为现有类的子类或实现类。
  3. 模板方法模式:模板方法模式是一种行为设计模式,它定义了一个算法的骨架,并将一些步骤延迟到子类中实现。这样,可以在不修改现有代码的情况下,通过扩展子类添加新功能
  4. 策略模式:策略模式是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。这样,可以在不修改现有代码的情况下,通过选择不同的策略实现不同的功能

以下是一个简单的Java示例,展示了如何使用开-闭原则:

// 抽象类
abstract class Shape {
    abstract void draw();
}

// 具体实现类
class Circle extends Shape {
    void draw() {
        System.out.println("Drawing Circle");
    }
}

class Rectangle extends Shape {
    void draw() {
        System.out.println("Drawing Rectangle");
    }
}

// 添加新功能
class Square extends Shape {
    void draw() {
        System.out.println("Drawing Square");
    }
}

// 使用多态
public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle();
        Shape rectangle = new Rectangle();
        Shape square = new Square();
		
        drawShape(circle);
        drawShape(rectangle);
        drawShape(square);
    }
		
    public static void drawShape(Shape shape) {
        shape.draw();
    }
}

在这个示例中,我们定义了一个抽象类Shape,它有一个抽象方法draw()。然后,我们创建了三个具体实现类CircleRectangleSquare,它们都实现了draw()方法。这样,我们就可以在不修改现有代码的情况下,通过添加新的实现类来扩展功能。同时,我们使用多态来调用drawShape()方法,它可以根据传入的Shape对象的不同,调用不同的draw()方法。

1.5 高内聚-低耦合原则

  • 内聚主要描述模块内部,一个模块应当尽可能独立完成某个功能。模块内部的元素, 关联性越强, 则内聚越高, 模块单一性则更强。
    • 判定:系统存在AB两个模块儿进行交互,如果修改了A模块儿不影响B模块的工作,那么认为A模块儿有足够的内聚。
  • 耦合主要描述模块之间的关系。模块的独立性高,将使应用程序便于扩展, 维护, 写单元测试。
举例说明

在Java中,高内聚-低耦合原则通常通过以下方式实现:

  1. 单一职责原则:每个类或模块应该只负责一项任务或功能。这样可以提高内聚性,减少与其他类或模块的依赖关系。
  2. 开闭原则:类或模块应该对扩展开放,对修改关闭。这样可以减少对现有代码的修改,降低与其他类或模块的耦合性。
  3. 依赖倒置原则:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。这样可以减少模块之间的直接依赖关系,提高内聚性。
  4. 接口隔离原则:客户端不应该被迫依赖于它们不使用的接口。这样可以减少不必要的依赖关系,提高内聚性。
  5. 组合/聚合复用原则:尽量使用组合或聚合来复用代码,而不是继承。这样可以减少类之间的耦合性,提高内聚性。

以下是一个简单的Java示例,展示了如何使用高内聚-低耦合原则:

// 单一职责原则
class Circle {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle {
    private double width;
    private double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double getArea() {
        return width * height;
    }
}

// 依赖倒置原则
interface Shape {
    double getArea();
}

class Circle implements Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double width;
    private double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double getArea() {
        return width * height;
    }
}

class AreaCalculator {
    public double calculateTotalArea(Shape... shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.getArea();
        }
        return totalArea;
    }
}

// 接口隔离原则
interface Shape {
    double getArea();
}

interface Shape3D {
    double getVolume();
}

class Sphere implements Shape3D {
    private double radius;
    public Sphere(double radius) {
        this.radius = radius;
    }
    public double getVolume() {
        return (4.0 / 3) * Math.PI * Math.pow(radius, 3);
    }
}

// 组合/聚合复用原则
class Car {
    private Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public void start() {
        engine.start();
    }
}

class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

在这个示例中,我们定义了几个类来展示高内聚-低耦合原则:

  1. CircleRectangle类分别实现了Shape接口,它们只负责计算自己的面积,符合单一职责原则。
  2. AreaCalculator类通过依赖Shape接口来计算多个形状的总面积,符合依赖倒置原则。
  3. Sphere类实现了Shape3D接口,它只负责计算自己的体积,符合接口隔离原则。
  4. Car类通过组合Engine类来启动汽车,符合组合/聚合复用原则。
    通过这些示例,我们可以看到如何使用高内聚-低耦合原则来提高代码的可维护性和可扩展性。

1.6 其它原则

  • 开闭原则:一个软件实体应当对扩展开放,对修改关闭
  • 依赖倒转原则:抽象不应该依赖于细节, 细节应当依赖于抽象。 换言之, 要针对接口编程, 而不是针对实现编程。
  • 单一职责原则:一个类只负责一个功能领域中的相应职责。
  • 接口隔离原则:使用多个专门的接口, 而不使用单一的总接口, 即客户端不应该依赖那些它不需要的接口。
  • 迪米特法则: 一个软件实体应当尽可能少地与其他实体发生相互作用
  • 里氏代换原则:所有引用基类父类)的地方必须能透明地使用其子类的对象
  • 合成复用原则:尽量使用组合或者聚合关系实现代码复用,少使用继承

Pasted image 20241104165644.png

2 设计模式

分类

  • 创建型:涉及对象的实例化,这类模式的特点是,不让用户代码依赖于对象的创建或排列方式,避免用户直接使用new运算符创建对象。

    • 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
    • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例
    • 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
    • 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品
    • 建造者(Builder)模式:将一个复杂对象分解多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
  • 行为型:涉及怎样合理地设计对象之间的交互通信,以及怎样合理为对象分配职责,让设计富有弹性,易维护,易复用。

    • 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
    • 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作
    • 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
    • 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
    • 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问
    • 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用
    • 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性
  • 结构型:涉及如何组合类和对象以形成更大的结构,和类有关的结构型模式涉及如何合理地使用继承机制,和对象有关的结构型模式涉及如何合理地使用对象组合机制。例如:装饰模式等

    • 模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤
    • 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户
    • 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任执行请求的责任分割开。
    • 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
    • 状态(State)模式:允许一个对象在其内部状态发生改变改变其行为能力
    • 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为
    • 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解
    • 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
    • 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
    • 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
    • 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。

2.1 策略模式

Pasted image 20241104224241.png

  • 策略(Strategy):策略是一个接口,该接口定义若干个算法标识,即定义了若干个抽象方法核心就是将类中经常需要变化的部分分割出来,并将每种可能的变化对应地交给抽象类的一个子类或实现接口的一个类去负责,从而让类的设计者不去关心具体实现,避免所设计的类依赖于具体的实现。
  • 上下文(Context):上下文是依赖于策略接口的(是面向策略设计的类),即上下文包含有用策略声明的变量。上下文中提供一个方法,该方法委托策略变量调用具体策略所实现的策略接口中的方法
  • 具体策略(ConcreteStrategy):具体策略是实现策略接口的类。具体策略实现策略接口所定义的抽象方法,即给出算法标识的具体算法

例1

问题

问题:在多个裁判负责打分的比赛中,每位裁判给选手一个得分,选手的最后得分是根据全体裁判的得分计算出来的。请给出几种计算选手得分的评分方案(策略),对于某次比赛,可以从你的方案中选择一种方案作为本次比赛的评分方案。

  • 在这里我们把策略接口命名为:Strategy。在具体应用中,这个角色的名字可以根据具体问题来命名。
  • 在本问题中将上下文命名为 AverageScore,即让 AverageScore 类依赖于 Strategy 接口。
  • 每个具体策略负责一系列算法中的一个。

java代码

  • 策略( Strategy
    Pasted image 20241104225221.png
  • 上下文( Context
    Pasted image 20241104225245.png
  • 具体策略StrategyA.java
    Pasted image 20241104225309.png
  • 具体策略StrategyB.java
    Pasted image 20241104225324.png
  • 模式的使用
    Pasted image 20241104225338.png
    Pasted image 20241104225651.png
    Pasted image 20241104230022.png

例2

Pasted image 20241104230051.png

例3

Pasted image 20241104230103.png

2.2 访问者模式

  • 模式优点:在不改变一个集合中的元素的类的情况下,可以增加新的施加于该元素上的新操作。保持一定的扩展性。
  • 使用场景:需要对集合中的对象进行很多不同的并且不相关的操作,而我们又不想修改对象的类,就可以使用访问者模式。访问者模式可以在Visitor类中集中定义一些关于集合中对象的操作。

Pasted image 20241105171558.png

  • 抽象元素(Element):一个抽象类,该类定义了接收访问者的accept操作。
  • 具体元素(Concrete Element):Element的子类。
  • 抽象访问者(Visitor):一个接口,该接口定义操作具体元素的方法。
  • 具体访问者(Concrete Visitor):实现Visitor接口的类。

例1

问题

  • 门诊部是一个类似于访问者的对象,它可以访问不同类型的病人对象,例如普通病人、急诊病人、儿科病人等。
  • 不同类型的病人对象可以有不同的处理方法,例如看病、输液、检查等。
  • 门诊部可以对不同类型的病人对象进行不同的操作,而不需要改变病人对象的类层次结构。

java代码

  • 抽象访问者
    Pasted image 20241105171806.png
  • 具体访问者
    Pasted image 20241105171826.png
  • 抽象元素
    Pasted image 20241105172142.png
  • 具体元素
    Pasted image 20241105172214.png
  • 结构对象
    Pasted image 20241105172341.png
  • 测试案例
    Pasted image 20241105172420.png

例2

问题

根据电表显示的用电量计算用户的电费。用户包括居民和企业。访问同一个电表,即分别按家用电标准和工业用电标准计算电费。

  • 抽象访问者:访问电表
  • 具体访问者:按家用电标准/工业用电标准访问电表
  • 抽象元素:抽象类AmmeterElement
  • 具体元素:Ammeter(模拟电表)

java代码

  • 抽象访问者:设置访问方法
    Pasted image 20241105173009.png
  • 具体访问者
    Pasted image 20241105173032.png
    Pasted image 20241105173048.png
  • 抽象元素:提供访问入口
    Pasted image 20241105173018.png
  • 具体元素
    Pasted image 20241105173113.png
  • 模式的使用
    Pasted image 20241105173224.png
    Pasted image 20241105173230.png

2.3 装饰模式

Pasted image 20241111170010.png

  • 抽象组件(Component):抽象组件(是抽象类)定义了需要进行装饰的方法。抽象组件就是“被装饰者”角色
  • 具体组件(ConcreteComponent):具体组件是抽象组件的一个子类。
  • 装饰(Decorator):该角色是抽象组件的一个子类,是“装饰者”角色,其作用是装饰具体组件。Decorator角色需要包含抽象组件的引用
  • 具体装饰(ConcreteDecotator):具体装饰是Decorator角色的一个非抽象子类

例1

问题

Pasted image 20241111170614.png

代码实现

抽象组件
Pasted image 20241111170648.png
具体组件
Pasted image 20241111170704.png
装饰
Pasted image 20241111170717.png
具体装饰
Pasted image 20241111170759.png
模式的使用
Pasted image 20241111170846.png
Pasted image 20241111172407.png
最后的bird
Pasted image 20241111172356.png

优点

  • 你无需创建新子类即可扩展对象的行为
  • 你可以在运行时添加或删除对象的功能
  • 你可以用多个装饰封装对象来组合几种行为。
  • 被装饰者和装饰者是松耦合关系。程序的弹性和可扩展性更优。

2.4 适配器模式

Pasted image 20241111214751.png

  • 目标(Target):目标是一个接口,该接口是客户想使用的接口
  • 被适配者(Adaptee):被适配者是一个已经存在的接口或抽象类,这个接口或抽象类需要适配。
  • 适配器(Adapter):适配器是一个类,该类实现了目标接口并包含有被适配者的引用,即适配器的职责是对被适配者接口(抽象类)与目标接口进行适配

例1

问题

Pasted image 20241111221807.png

java实现

目标(Target)

Pasted image 20241111221832.png

被适配者

Pasted image 20241111221909.png

适配器

Pasted image 20241111222107.png

被适配者的具体类

Pasted image 20241111222401.png

录音机和洗衣机

Pasted image 20241111222421.png
Pasted image 20241111222425.png

模式的使用

Pasted image 20241111222528.pngPasted image 20241111222901.png

适配器的适配程度

  • 完全适配
    如果目标(Target)接口中的方法数目与被适配者(Adaptee)接口的方法数目相等,那么适配器(Adapter)可将被适配者接口(抽象类)与目标接口进行完全适配。
  • 不完全适配
    如果目标(Target)接口中的方法数目少于被适配者(Adaptee)接口的方法数目,那么适配器(Adapter)只能将被适配者接口(抽象类)与目标接口进行部分适配。
  • 剩余适配
    如果目标(Target)接口中的方法数目大于被适配者(Adaptee)接口的方法数目,那么适配器(Adapter)可将被适配者接口(抽象类)与目标接口进行完全适配,但必须将目标多余的方法给出用户允许的默认实现。

2.5 责任链模式

责任链模式是使用多个对象处理用户请求的成熟模式,责任链模式的关键是将用户的请求分派给许多对象。

Pasted image 20241111223041.png

  • 处理者(Handler):处理者是一个接口,负责规定具体处理者处理用户的请求的方法以及具体处理者设置后继对象的方法。
  • 具体处理者(ConcreteHandler):具体处理者是实现处理者接口的类的实例。具体处理者通过调用处理者接口规定的方法处理用户的请求,即在接到用户的请求后,处理者将调用接口规定的方法,在执行该方法的过程中,如果发现能处理用户的请求,就处理有关数据,否则就反馈无法处理的信息给用户,然后将用户的请求传递给自己的后继对象

例1

问题

Pasted image 20241111223258.png
Pasted image 20241111223315.png

java实现

抽象处理者:领导类

Pasted image 20241111223609.png

具体处理者1:班主任类

Pasted image 20241111223953.png

具体处理者2:系主任类

Pasted image 20241111224038.png

具体处理者:院长类

Pasted image 20241111224102.png

测试类

Pasted image 20241111224230.png

dlc:具体处理者4:教务处长类

Pasted image 20241111224328.png

优点及使用场景

  • 优点:当在处理者中分配职责时,责任链给应用程序更多的灵活性
  • 使用场景
    • 许多对象可以处理用户的请求。
    • 程序希望动态制定可处理用户请求的对象集合

2.6 外观模式(Facade模式/门面模式)

Pasted image 20241111225054.png

  • 外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。
  • 该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,降低其与子系统的耦合,提高了程序的可维护性。
  • 是“迪米特法则”的典型应用
    迪米特法则: 一个软件实体应当尽可能少地与其他实体发生相互作用

例1

问题

Pasted image 20241111225054.png

java实现

Pasted image 20241111225136.png
Pasted image 20241111225143.png

优点

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
  • 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象

缺点

  • 不能很好地限制客户使用子系统类,很容易带来未知风险。
  • 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”

外观模式的扩展

Pasted image 20241111225334.png

  • 在外观模式中,当增加或移除子系统时需要修改外观类,这违背了“开闭原则”。
  • 如果引入抽象外观类,则在一定程度上解决了该问题。
Built with MDFriday ❤️