第一章:设计模式相关内容介绍《Java设计模式宝典:从入门到精通》

在第一章开始之前,整个设计模式专栏的快速访问地址给各位朋友们贴上:

《Java设计模式宝典:从入门到精通》专栏目录地址,点击我即可快速访问到所有设计模式的章节,不漏掉一处学习的知识 [点赞]

胡广有话说: 这一章主要涉及一些理论知识,可能会显得比较抽象。学习这些内容时,建议大家结合实际的代码实践来深刻理解,而不是简单地死记硬背。每个知识点都需要通过实际操作才能真正领会其中的奥妙,这也是为什么在接下来的章节中,我会逐步更新相关的实战代码和示例。

举个例子,比如在设计模式中的创建型模式里有一个常见的单例模式单例模式的核心思想一个类只会被创建一次,确保在整个程序运行期间只有一个实例存在。但是,如果在程序中不小心创建了多个不会被内存回收的类实例,那就破坏了单例模式的初衷。这不仅违反了设计模式的原则,还可能导致计算机的内存资源被过度消耗,最终可能引发内存溢出等问题。

我知道这个例子可能对初学者来说依然有些难以理解,但别担心,学习的过程就是这样。刚开始的时候,很多概念可能会让人感到困惑,但随着你不断地实践和思考,这些知识会在你的脑海中逐渐发酵,最终你会以全新的视角去理解它们。所以,保持耐心,坚持下去,你会看到自己的进步。加油!

第一章:设计模式相关内容介绍


1. 设计模式概述

1.1 软件设计模式的产生背景

"设计模式"一词最初并非源于软件设计领域,而是最早用于建筑设计中。

1977年,美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造》中提出了253种基本的建筑设计模式。这些模式用于解决城镇、邻里、住宅、花园和房间等设计中的常见问题,为建筑设计提供了系统化的解决方案。

看看这个亚历山大长啥样?嘻嘻嘻嘻,外国人长相特色

进入1990年代,软件工程界开始关注设计模式,并多次召开相关研讨会。1995年,艾瑞克·伽马(Erich Gamma)、理查德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)和约翰·威利斯迪斯(John Vlissides)这四位作者合作出版了《设计模式:可复用面向对象软件的基础》。这本书收录了23个设计模式,标志着软件设计模式领域的重要突破,并被誉为设计模式领域的里程碑。作者们也被称为软件开发领域的“四人组”(Gang of Four,GoF)。


1.2 软件设计模式的概念

软件设计模式(Software Design Pattern),是一种解决常见设计问题的标准化方法。它们是从实际编码经验中总结出来的,被广泛使用并经过验证的解决方案。为了更好地理解设计模式的概念,我们可以用以下方式来解释:

什么是设计模式?

设计模式就像是一种设计问题的“处方”或“配方”。它们告诉我们在特定情境下应该如何组织和构建代码,以解决常见的设计难题。想象一下你在厨房里做菜,你会使用一些标准的食谱来保证菜肴的味道和质量。设计模式就类似于这些食谱,帮助你在软件开发中“烹饪”出高质量的代码。

设计模式的特点

  • 反复使用:设计模式是经过多次实践验证的解决方案。比如,单例模式确保一个类只有一个实例,这在很多应用场景中都是常见且有效的。
  • 广泛认知:设计模式是软件开发领域内广泛使用的技巧。就像“豆腐脑”在中国是很多地方的传统美食一样,设计模式在软件开发中也是被普遍接受的“美食”。
  • 分类编目:设计模式分为几类,每一类针对不同类型的问题。例如,创建型模式解决如何创建对象的问题,结构型模式解决对象如何组合的问题,行为型模式解决对象如何交互的问题。
  • 解决方案:每个设计模式提供了一种标准化的方式来解决特定问题。例如,观察者模式可以用来处理对象之间的通知和更新问题,就像订阅了新闻邮件的人会在新闻发布时自动收到更新一样。

1.3 学习设计模式的必要性

胡广有话说:就是为了让我们编写的程序能够适配更多恶劣的环境,从而不受影响,跟建房子一个道理

学习设计模式不仅帮助我们掌握面向对象设计的核心原则,如封装、继承和多态,还能够在实际项目中提高编程效率和代码质量。以下是学习设计模式的主要优点及其具体说明:

1. 提高思维能力和编程技能

设计模式教会我们如何从宏观上考虑问题,并提供解决方案的标准化方法。这种系统化的思维方式有助于提高程序员的解决问题能力。例如,通过使用策略模式,你可以灵活地选择和切换算法,而不是硬编码各种实现,这样的设计让你在面对复杂问题时更加得心应手。

2. 提高开发效率和缩短开发周期

设计模式帮助程序员遵循最佳实践,使得代码编写更加标准化和工程化。比如,使用工厂方法模式来创建对象,可以避免在每个类中重复编写对象创建逻辑,从而减少了代码的冗余和维护工作。这种标准化的设计可以显著提高开发效率,缩短软件开发周期。

3. 增强代码的可重用性、可读性和可维护性

  • 可重用性:设计模式帮助你编写的代码具有更高的重用性。例如,使用单例模式可以确保一个类只有一个实例,这个实例可以在多个地方被共享使用。
  • 可读性:设计模式提供了统一的命名和结构,使得代码更容易被理解和维护。例如,使用观察者模式来实现事件监听,可以使代码的事件处理逻辑更加清晰和易于追踪。
  • 可靠性:设计模式经过实践验证,使用它们可以减少常见错误的发生,提高代码的可靠性。例如,使用适配器模式来处理不兼容的接口,可以减少因接口不匹配而导致的问题。
  • 灵活性和维护性:设计模式使得系统在面对变化时更具灵活性。例如,使用装饰者模式可以在不修改原有代码的情况下,动态地扩展对象的功能,这大大提高了系统的可维护性。

通过学习和应用设计模式,你将能够编写出更加高效、可靠和易于维护的代码,提升你的编程能力和软件开发水平。


1.4 设计模式分类

设计模式通常分为三大类:创建型模式、结构型模式和行为型模式。每种分类针对不同的设计问题提供解决方案。下面是每种模式的简要介绍:

1.4.1 创建型模式

创建型模式关注如何创建对象。它们的主要特点是将对象的创建过程与对象的使用过程分离,使得系统可以更加灵活地管理对象的实例。GoF(四人组)书中提供了以下五种创建型模式:

  • 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点。适用于需要全局控制的场景,例如日志记录器或配置管理器。

  • 原型模式(Prototype Pattern):通过复制现有对象来创建新对象,避免重复创建同样的对象。适用于创建复杂对象时,可以节省实例化的开销,例如图形编辑器中的图形复制功能。

  • 工厂方法模式(Factory Method Pattern):定义一个创建对象的接口,但由子类决定实例化哪个类。适合于需要创建多个不同类型对象的场景,例如创建不同的图形对象(圆形、方形)时。

  • 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关对象的接口,而无需指定具体类。适用于创建多个系列的相关对象,例如在不同的操作系统上创建不同风格的界面组件。

  • 建造者模式(Builder Pattern):将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。适用于创建复杂的对象,如构建一个定制的文档或复杂的产品。


1.4.2 结构型模式

结构型模式关注如何将类或对象按照某种结构组合成更大的结构。它们的主要特点是通过组合现有的类或对象来构建新的功能。GoF(四人组)书中介绍了以下七种结构型模式:

  • 代理模式(Proxy Pattern):为其他对象提供一个代理以控制对该对象的访问。适用于需要控制访问权限或延迟对象创建的场景,例如远程对象的代理或虚拟代理。

  • 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的接口,使得原本不兼容的接口可以一起工作。适用于需要将旧系统与新系统兼容的场景,例如将旧版接口适配到新版系统。

  • 桥接模式(Bridge Pattern):将抽象部分与实现部分分离,使得它们可以独立变化。适用于需要在多个维度上扩展系统的场景,例如图形系统中的绘图工具和形状。

  • 装饰模式(Decorator Pattern):动态地给对象添加额外的功能。适用于需要在运行时增加功能的场景,例如为图形对象添加边框或滚动条。

  • 外观模式(Facade Pattern):为复杂子系统提供一个统一的接口,使得子系统更易于使用。适用于简化复杂系统的使用,如为复杂的库或框架提供简化的访问接口。

  • 享元模式(Flyweight Pattern):通过共享对象来减少内存使用。适用于大量细粒度对象需要共享的场景,例如在文本编辑器中共享字符对象。

  • 组合模式(Composite Pattern):将对象组合成树形结构以表示部分-整体层次结构。适用于需要处理树形结构的场景,例如图形系统中的图形元素。


1.4.3 行为型模式

行为型模式关注类或对象之间如何协作,共同完成单个对象无法完成的任务,并分配职责。GoF(四人组)书中提供了以下十一种行为型模式:

  • 模板方法模式(Template Method Pattern):定义一个操作的算法框架,而将一些步骤延迟到子类中。适用于需要定义算法的固定步骤并允许子类实现具体步骤的场景,例如数据处理的流程控制。

  • 策略模式(Strategy Pattern):定义一系列算法,并将每一个算法封装起来,使它们可以互换。适用于需要在运行时选择算法的场景,例如选择不同的排序算法。

  • 命令模式(Command Pattern):将请求封装成一个对象,从而允许你使用不同的请求、队列请求和撤销操作。适用于需要将操作请求封装起来并支持撤销的场景,例如实现撤销和重做功能。

  • 职责链模式(Chain of Responsibility Pattern):将请求的发送者和接收者解耦,使得多个对象有机会处理请求。适用于需要多个对象处理请求的场景,例如请求处理流程中的多个处理器。

  • 状态模式(State Pattern):允许对象在内部状态改变时改变其行为。适用于状态依赖的行为变化,如状态机中的状态切换。

  • 观察者模式(Observer Pattern):定义对象之间的一对多依赖关系,以便当一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。适用于需要事件通知机制的场景,例如事件监听和通知机制。

  • 中介者模式(Mediator Pattern):定义一个中介对象来封装一组对象之间的交互,使得对象之间不需要直接交互。适用于复杂的对象交互场景,如GUI组件之间的协作。

  • 迭代器模式(Iterator Pattern):提供一种方法顺序访问集合对象中的元素,而不暴露集合的内部表示。适用于需要遍历集合的场景,如列表和树形结构的遍历。

  • 访问者模式(Visitor Pattern):定义一个操作对象结构中的元素的新操作,而无需改变元素类。适用于需要在不改变对象结构的情况下增加新操作的场景,如对不同类型元素进行操作。

  • 备忘录模式(Memento Pattern):在不暴露对象实现细节的情况下,捕获对象的内部状态,以便在以后恢复对象的状态。适用于需要保存和恢复对象状态的场景,例如实现撤销功能。

  • 解释器模式(Interpreter Pattern):给定一个语言,定义它的文法,并提供一个解释器来解释语言中的句子。适用于需要解释语言或表达式的场景,如计算器和编程语言解释器。


2. UML图

        UML(Unified Modeling Language,统一建模语言)是软件工程中非常重要的一部分。它提供了一套标准化的表示方法,用于说明、构建、可视化和记录软件系统的各种设计。


2.1 类图概述

类图(Class Diagram)是用于展示系统静态结构的图表。它主要表现模型中的类、类的内部结构,以及类之间的关系。不同于展示动态行为的图表,类图着重于静态方面,因此不显示暂时性的信息。作为面向对象建模的核心工具,类图在设计阶段帮助开发者清晰地描绘出系统的骨架和组成部分。


2.2 类图的作用

在软件工程中,类图是一种静态结构图,展示了系统中的类、类的属性以及类之间的关系。通过这种可视化的方式,类图帮助开发人员简化和加深对系统的理解,提供了一种全局视角来分析和设计系统。

类图不仅仅是系统分析和设计阶段的重要产物,还是编码和测试过程中不可或缺的模型工具。它为开发人员提供了明确的参考,使得系统的实现和验证更加高效、准确。


2.3 类图表示法

2.3.1 类的表示方式

在UML类图中,类通过一个带有分割线的矩形来表示。这个矩形被分为三部分:

  1. 类名:矩形的最上部分,用于显示类的名称。

  2. 属性(Field):矩形的中间部分,用于显示类的属性,每个属性的表示方式如下:

    • 可见性:使用符号表示,如+(public)、-(private)和#(protected)。
    • 名称:属性的名称。
    • 类型:属性的数据类型。
    • 缺省值(可选):属性的默认值。

    属性的完整表示方式是:可见性 名称 : 类型 [ = 缺省值]

  3. 方法(Method):矩形的下部分,用于显示类的方法,每个方法的表示方式如下:

    • 可见性:与属性相同,使用符号表示。
    • 名称:方法的名称。
    • 参数列表:方法的参数及其类型。
    • 返回类型(可选):方法的返回值类型。

    方法的完整表示方式是:可见性 名称(参数列表) [ : 返回类型]

代码示例

以下是一个 Employee 类的示例以及示例图,展示了类的表示方式:

+--------------------+
| Employee           |
+--------------------+
| - name : String    |
| - age : int        |
| - address : String |
+--------------------+
| + work() : void    |
+--------------------+

属性:

- name : String:表示一个私有属性 name,类型为 String。
- age : int:表示一个私有属性 age,类型为 int。
- address : String:表示一个私有属性 address,类型为 String。
方法:

+ work() : void:表示一个公共方法 work(),没有参数,返回类型为 void。

黑马程序员课件中的图形示例 

 

另一个示例

以下是一个 Demo 类的示例,展示了不同的访问修饰符和方法的表示方式:

+--------------------+
| Demo               |
+--------------------+
|                    |
+--------------------+
| + method() : void  |
| - method1() : String |
| # method2(int, String) : int |
+--------------------+

方法:
+ method() : void:公共方法,没有参数,没有返回值。
- method1() : String:私有方法,没有参数,返回类型为 String。
# method2(int, String) : int:受保护方法,接收两个参数(int 和 String),返回类型为 int。
注意事项
中括号中的内容是可选的:在属性和方法的表示中,中括号部分(如 [ = 缺省值] 和 [ : 返回类型])是可选的,根据实际需要可以选择是否包括。

可选的表示方式:也可以将类型放在变量名前面,返回值类型放在方法名前面,例如 int method2(int, String) 作为一种可选的表示方式。

2.3.2 类与类之间关系的表示方式

2.3.2.1 关联关系
        1.单向关联

        关联关系表示类与类之间的联系。关联关系的类型包括一般关联关系、聚合关系和组合关系。关联关系可以进一步分为单向关联、双向关联和自关联。

+-----------------+          +-----------------+
|   Customer      |---------->|   Address       |
+-----------------+          +-----------------+

        单向关联表示一个类对象对另一个类对象的引用。在UML类图中,单向关联用一个带箭头的实线表示,箭头指向被引用的类。

        示例:假设一个 Customer 类有一个 Address 类型的成员变量,这表示每个顾客都有一个地址。 

        2. 双向关联

        双向关联表示两个类对象互相引用。在UML类图中,双向关联用一个不带箭头的直线表示,线的两端各指向一个类。

+-----------------+          +-----------------+
|   Customer      |----------|   Product       |
+-----------------+          +-----------------+

        示例:Customer 类和 Product 类之间的关系,一个顾客可以购买多个商品,一个商品被多个顾客购买。

        3.自关联

        自关联表示一个类对象与自身的关系。在UML类图中,自关联用一个带箭头且指向自身的线表示。

+-----------------+
|   Node          |
+-----------------+
| - next : Node   |
+-----------------+

示例:Node 类包含一个类型为 Node 的成员变量,表示一个节点包含自己。

2.3.2.2 聚合关系

聚合关系表示整体与部分的关系,其中部分可以独立于整体存在。在UML类图中,聚合关系用带空心菱形的实线来表示,菱形指向整体。

+-----------------+      +-----------------+
|   University    |<>----|   Teacher       |
+-----------------+      +-----------------+

示例:University 类和 Teacher 类之间的关系,大学包含老师,但老师可以存在于没有大学的情况下。

2.3.2.3 组合关系

组合关系是聚合关系的一种更强烈的形式,表示整体与部分之间的关系,部分不能独立于整体存在。在UML类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。

+-----------------+      +-----------------+
|      Head       |<>----|      Mouth       |
+-----------------+      +-----------------+

注意这个组合关系这个菱形箭头为实心的,不要与聚合关系搞混淆了

示例:Head 类和 Mouth 类之间的关系,头部包含嘴部,一旦头部不存在,嘴部也不存在。

2.3.2.4 依赖关系

依赖关系表示一个类对另一个类的使用,通常是临时性的。在UML类图中,依赖关系用带箭头的虚线表示,箭头从使用类指向被依赖的类。

+-----------------+      +-----------------+
|    Driver       |------>|      Car         |
+-----------------+      +-----------------+

示例:Driver 类依赖于 Car 类,司机驾驶汽车。

2.3.2.5 继承关系

继承关系表示父类与子类之间的关系,表示一般与特殊的关系。在UML类图中,继承关系用带空心三角箭头的实线表示,箭头从子类指向父类。

+-------------------+
|      Person       |
+-------------------+
    ^           ^
    |           |
+--------+ +---------+
| Student| | Teacher |
+--------+ +---------+

示例:Student 类和 Teacher 类都是 Person 类的子类。

2.3.2.6 实现关系

实现关系表示一个类实现了一个接口。在UML类图中,实现关系用带空心三角箭头的虚线表示,箭头从实现类指向接口。

+-----------------+       +-----------------+
|   Car           |------>|     Vehicle     |
+-----------------+       +-----------------+
                       
+-----------------+
|   Boat          |
+-----------------+

示例:Car 类和 Boat 类实现了 Vehicle 接口。

这些关系在UML类图中帮助我们理解类之间的各种联系和依赖。实际上,这些理论知识在我们深入掌握后,会大大提升我们的编程水平。当我们将这些设计理念应用到实际编程中时,常常会发现自己的程序达到了一个新的高度。借助这些设计,我们的程序不仅会变得更加稳健,还能提高代码的复用性,从而使系统更加优秀。这是我在实际工作中的真实感受,也是我不断追求的目标。

3.软件设计原则


3.1 开闭原则(OCP)

开闭原则(Open/Closed Principle) 是一种软件设计原则,旨在使软件在进行功能拓展时不需要修改原有代码。这意味着系统应该对扩展开放,对修改封闭。简而言之,开闭原则的目标是提高程序的扩展性,同时保持系统的稳定性和可维护性。

原则概述

为了实现开闭原则,我们通常使用接口抽象类。通过定义抽象层(如接口或抽象类),我们可以确保系统的核心逻辑不受具体实现的影响。具体的功能扩展可以通过实现这些抽象层来完成,而不需要直接修改已有的代码。这种方法不仅提高了代码的灵活性,还使系统更加容易维护和升级。

实现方式

  1. 使用接口或抽象类:定义一个稳定的接口或抽象类,供其他类实现或继承。

  2. 扩展新功能:通过实现新的具体类或方法来扩展功能,而不更改已有的代码。

  3. 保持系统稳定:核心逻辑保持不变,减少了因修改现有代码而引入的风险。

示例:搜狗输入法的皮肤设计

场景分析搜狗输入法 的皮肤包括输入法背景图片、窗口颜色、声音等元素。用户可以根据个人喜好更换输入法的皮肤,或者从网上下载新的皮肤。为了满足开闭原则,我们可以将皮肤设计实现如下:

1.定义抽象类
public abstract class AbstractSkin {
    public abstract void applySkin();
}
2.实现具体皮肤
public class DefaultSkin extends AbstractSkin {
    @Override
    public void applySkin() {
        // 实现默认皮肤的应用逻辑
    }
}

public class HeimaSkin extends AbstractSkin {
    @Override
    public void applySkin() {
        // 实现黑色皮肤的应用逻辑
    }
}
3.选择和应用皮肤
public class SkinManager {
    private AbstractSkin skin;

    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }

    public void applySkin() {
        skin.applySkin();
    }
}

解释

  • AbstractSkin 是一个抽象类,定义了皮肤的基本功能。
  • DefaultSkinHeimaSkin 是具体的皮肤实现类,它们继承自 AbstractSkin 并实现了 applySkin 方法。
  • SkinManager 类负责管理和应用皮肤,可以根据用户的选择或需求切换不同的皮肤,而不需要修改皮肤管理的核心逻辑。

这种设计允许用户在不修改 SkinManager 类的情况下添加新皮肤,只需创建新的 AbstractSkin 子类并实现其功能即可。这符合开闭原则,使得系统对功能扩展开放,对修改封闭。


3.2 里氏替换原则(LSP)

里氏替换原则(Liskov Substitution Principle) 是面向对象设计中的一个重要原则,它规定了子类与父类之间的替换关系。

原则概述

里氏替换原则:任何基类可以出现的地方,子类一定可以出现。通俗地说,子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类在继承父类时,除了添加新的方法来实现额外的功能外,尽量不要重写父类的方法,从而保持继承体系的稳定性和可复用性。

如果子类通过重写父类的方法来实现新的功能,可能会导致继承体系的可复用性降低,特别是在多态使用频繁的情况下,程序可能会出现运行时错误。因此,遵循里氏替换原则能够提高代码的稳定性和可维护性。

示例:正方形与长方形

场景分析:在数学中,正方形确实是长方形的一种特殊情况,因为正方形的所有边长度相等,而长方形的边长度可以不同。在设计一个几何图形处理系统时,可以考虑让 Square 类继承自 Rectangle 类,以体现正方形和长方形之间的关系。

问题示例代码
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = this.height = height;
    }
}
问题分析
  • 在上述设计中,Square 类继承自 Rectangle 类,但它重写了 setWidthsetHeight 方法,以保持正方形的特性(宽度等于高度)。
  • 这种实现方式导致当调用 Rectangle 类的 setWidthsetHeight 方法时,不能正确处理正方形的约束(即宽度和高度总是相等)。
解决方案

要遵循里氏替换原则,可以考虑将 Square 类的设计改为不继承自 Rectangle 类,而是作为一个独立的类。这样可以避免因继承导致的功能异常:

class Shape {
    // 基类定义
}

class Rectangle extends Shape {
    protected int width;
    protected int height;

    // Rectangle 类的实现
}

class Square extends Shape {
    private int side;

    public void setSide(int side) {
        this.side = side;
    }

    public int getSide() {
        return side;
    }
}

类与类的解释

  • 在这个改进后的设计中,RectangleSquare 不再存在继承关系。Square 不继承自 Rectangle,避免了因继承引发的问题。
  • 这样,RectangleSquare 都是 Shape 类的子类,各自独立管理自己的属性和方法,从而避免了继承带来的约束问题。

3.3 依赖倒置原则(DIP)

依赖倒置原则(Dependency Inversion Principle) 是面向对象设计中的一个重要原则,旨在减少系统模块之间的耦合度,提高系统的灵活性和可维护性。

原则概述

依赖倒置原则的核心思想是:

  • 高层模块不应该依赖低层模块,两者都应该依赖于抽象
  • 抽象不应该依赖于细节,细节应该依赖于抽象

简单来说,就是要求在编程时依赖于抽象接口或抽象类,而不是具体的实现。这种方式可以降低系统中客户代码与具体实现模块之间的耦合度,使得系统更容易进行扩展和维护。

实现方式

  1. 定义抽象层:创建接口或抽象类,定义系统需要的操作和行为。
  2. 实现具体细节:让具体的实现类依赖于这些抽象层,提供实际的功能实现。
  3. 高层模块依赖于抽象:将系统的高层逻辑依赖于抽象层,而不是具体实现。

示例:组装电脑

场景分析:组装一台电脑需要多个配件,如CPU、硬盘和内存条。不同的配件有不同的品牌和型号,如Intel或AMD的CPU,希捷或西数的硬盘等。为了遵循依赖倒置原则,我们可以将电脑组装的过程抽象成接口,让不同的配件实现这些接口。

实现方式

1.定义抽象接口
public interface CPU {
    void process();
}

public interface HardDrive {
    void store();
}

public interface RAM {
    void load();
}
2.实现具体配件
public class IntelCPU implements CPU {
    @Override
    public void process() {
        System.out.println("Intel CPU 处理数据...");
    }
}

public class SeagateHardDrive implements HardDrive {
    @Override
    public void store() {
        System.out.println("Seagate 硬盘存储数据...");
    }
}

public class KingstonRAM implements RAM {
    @Override
    public void load() {
        System.out.println("Kingston 内存条加载数据...");
    }
}
3.定义电脑组装类
public class Computer {
    private CPU cpu;
    private HardDrive hardDrive;
    private RAM ram;

    public Computer(CPU cpu, HardDrive hardDrive, RAM ram) {
        this.cpu = cpu;
        this.hardDrive = hardDrive;
        this.ram = ram;
    }

    public void assemble() {
        cpu.process();
        hardDrive.store();
        ram.load();
        System.out.println("电脑组装成功!");
    }
}
4.使用组装类
public class Main {
    public static void main(String[] args) {
        CPU cpu = new IntelCPU();
        HardDrive hardDrive = new SeagateHardDrive();
        RAM ram = new KingstonRAM();

        Computer computer = new Computer(cpu, hardDrive, ram);
        computer.assemble();
    }
}
代码具体解释
  • 在这个示例中,CPUHardDriveRAM 是抽象接口,定义了电脑组装所需的功能。
  • IntelCPUSeagateHardDriveKingstonRAM 是具体的实现类,提供了不同品牌的具体实现。
  • Computer 类依赖于抽象接口而不是具体实现,这样可以很方便地替换具体的配件实现,例如更换为 AMD 的 CPU 或西数的硬盘,而无需修改 Computer 类的代码。

这种设计方式遵循了依赖倒置原则,使得高层模块(Computer 类)依赖于抽象层(接口),而不是具体的实现,从而降低了模块之间的耦合度,提高了系统的灵活性和可维护性。


3.4 接口隔离原则(ISP)

接口隔离原则(Interface Segregation Principle) 是面向对象设计中的一个重要原则,旨在确保客户端不依赖于它不使用的方法,从而提高系统的灵活性和可维护性。

原则概述

接口隔离原则:客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。

换句话说,接口应该小而专一,使得实现这些接口的类只需要关注它们实际需要的方法。这有助于减少不必要的耦合和复杂性,提高系统的灵活性和可维护性。

示例:安全门案例

场景分析

假设我们需要设计一个安全门系统,这个安全门具有防火、防水、防盗的功能。最初,我们可能会将这些功能提取到一个接口中,例如 SafetyDoor 接口。

注意注意!!!以下代码为了方便大家不混淆,已将对应防火、防水、防盗的英文翻译成了中文!!!大家在编码中请不要学习我,还是已英文为主!!

初始设计
public interface SafetyDoor {
    void防火();
    void防水();
    void防盗();
}

public class BlackHorseSafetyDoor implements SafetyDoor {
    @Override
    public void 防火() {
        System.out.println("黑马品牌安全门防火功能...");
    }

    @Override
    public void 防水() {
        System.out.println("黑马品牌安全门防水功能...");
    }

    @Override
    public void 防盗() {
        System.out.println("黑马品牌安全门防盗功能...");
    }
}
问题分析
  • 如果我们再创建一个只需要防盗和防水功能的 ChuanZhiSafetyDoor 类,这种设计会导致这个类实现了不需要的 防火 方法,违反了接口隔离原则。
改进设计
  • 为了遵循接口隔离原则,我们可以将不同的功能分离到多个小接口中。这样,每个类只需实现它需要的接口,而不需要实现它不需要的方法。

注意注意!!!以下代码为了方便大家不混淆,已将对应防火、防水、防盗的英文翻译成了中文!!!大家在编码中请不要学习我,还是已英文为主!!

public interface FireProtection {
    void 防火();
}

public interface WaterProtection {
    void 防水();
}

public interface TheftProtection {
    void 防盗();
}

public class BlackHorseSafetyDoor implements FireProtection, WaterProtection, TheftProtection {
    @Override
    public void 防火() {
        System.out.println("黑马品牌安全门防火功能...");
    }

    @Override
    public void 防水() {
        System.out.println("黑马品牌安全门防水功能...");
    }

    @Override
    public void 防盗() {
        System.out.println("黑马品牌安全门防盗功能...");
    }
}

public class ChuanZhiSafetyDoor implements WaterProtection, TheftProtection {
    @Override
    public void 防水() {
        System.out.println("传智品牌安全门防水功能...");
    }

    @Override
    public void 防盗() {
        System.out.println("传智品牌安全门防盗功能...");
    }
}
代码内容解释
  • 在这个改进后的设计中,功能被拆分成了三个独立的接口:FireProtectionWaterProtectionTheftProtection
  • BlackHorseSafetyDoor 实现了所有三个接口,而 ChuanZhiSafetyDoor 只实现了它需要的 WaterProtection TheftProtection 接口。
  • 这种方式避免了 ChuanZhiSafetyDoor 不需要的 防火 方法,从而更符合接口隔离原则,提高了设计的灵活性和可维护性。

3.5 迪米特法则(LoD)

迪米特法则(Law of Demeter),又称为最少知识原则,是面向对象设计中的一个重要原则。其核心思想是减少类之间的耦合,提高系统的模块独立性。

原则概述

迪米特法则:只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。

具体来说,迪米特法则要求:如果两个软件实体无须直接通信,就不应该直接相互调用。应该通过第三方进行转发,从而减少类之间的直接耦合。这种方式有助于降低系统的复杂性,提高模块的独立性和可维护性。

在迪米特法则中,“朋友” 是指:

  • 当前对象本身
  • 当前对象的成员对象
  • 当前对象所创建的对象
  • 当前对象的方法参数等

这些对象与当前对象之间存在关联、聚合或组合关系,可以直接访问这些对象的方法。

示例:明星与经纪人的关系

场景分析

明星通常会将一些日常事务交给经纪人处理,如与粉丝的见面会、与媒体公司的洽谈等。明星的工作主要集中在艺术创作,而经纪人则负责处理各种外部事务。在这个例子中,明星与经纪人是“朋友”关系,而粉丝和媒体公司则是“陌生人”。

实现方式
定义明星和经纪人的类
public class Star {
    private Agent agent;

    public Star(Agent agent) {
        this.agent = agent;
    }

    public void meetFans() {
        System.out.println("明星与粉丝见面...");
        // 明星委托经纪人安排见面会
        agent.arrangeMeeting();
    }

    public void negotiateWithMedia() {
        System.out.println("明星与媒体公司洽谈...");
        // 明星委托经纪人进行洽谈
        agent.handleMediaNegotiation();
    }
}

public class Agent {
    public void arrangeMeeting() {
        System.out.println("经纪人安排见面会...");
    }

    public void handleMediaNegotiation() {
        System.out.println("经纪人处理媒体洽谈...");
    }
}
代码内容解释
  • 在这个例子中,Star 类与 Agent 类之间是“朋友”关系。明星通过经纪人来处理与粉丝和媒体公司的事务,而不是直接与这些外部实体进行交互。
  • 明星只依赖于经纪人(即直接朋友),不直接依赖粉丝和媒体公司(即陌生人)。这样减少了系统中各个模块之间的直接耦合,提高了系统的可维护性和灵活性。
类图
+----------------+          +----------------+
|      Star      |          |     Agent      |
+----------------+          +----------------+
| - agent: Agent|<>--------|                |
+----------------+          +----------------+
| + meetFans()  |          | + arrangeMeeting() |
| + negotiateWithMedia() |  | + handleMediaNegotiation() |
+----------------+          +----------------+

原则总结

通过迪米特法则,我们可以确保类之间的交互仅限于直接的“朋友”,从而减少类之间的紧密耦合。这样,不仅有助于提高系统的模块独立性,还能使得系统在面对变化时更加稳定和容易维护。


3.6 合成复用原则(CRP)

合成复用原则:尽量优先使用组合或聚合等关联关系来实现复用,其次才考虑使用继承关系。合成复用原则提倡在软件设计中优先使用对象组合来实现功能复用,而非直接依赖继承关系。

继承复用 vs 合成复用

继承复用
  • 优点
    • 实现简单,直接继承父类即可复用功能。
  • 缺点
    • 破坏封装性:继承会暴露父类的实现细节给子类,使得子类对父类的实现有依赖,这种复用被称为“白箱”复用。
    • 高耦合度:子类与父类紧密耦合,父类的任何修改都会影响到子类,降低了系统的可扩展性和维护性。
    • 复用灵活性差:继承关系在编译时已经确定,运行时无法改变,这限制了复用的灵活性。
合成复用
  • 优点
    • 维持封装性:组合或聚合通过将已有对象纳入新对象中,新对象只知道如何使用这些对象的接口,而不需要了解其内部实现细节,这种复用被称为“黑箱”复用。
    • 低耦合度:通过接口而非实现进行交互,降低了类之间的耦合度。
    • 复用灵活性高:可以在运行时动态地进行对象组合,允许新对象动态地引用与成分对象类型相同的对象。

示例:汽车分类管理程序

场景分析

在管理汽车分类时,汽车可以按“动力源”进行分类(如汽油汽车、电动汽车等),也可以按“颜色”进行分类(如白色汽车、黑色汽车、红色汽车等)。如果同时考虑这两种分类,组合起来的分类就会非常多。

使用继承复用

假设使用继承来实现分类,可能会定义如下类:

  • GasolineCar
  • ElectricCar
  • WhiteGasolineCar
  • BlackElectricCar
  • 等等

这种方法的问题在于每增加一个新的动力源或颜色,就需要创建新的子类,类的数量迅速增加,维护和扩展变得复杂。

使用合成复用

改用组合复用,我们可以定义更通用的类:

public class Car {
    private Engine engine; // 引擎类型(例如汽油引擎或电动引擎)
    private Color color;   // 颜色(例如白色、黑色、红色)

    // 构造函数、方法等
}
定义相关类
public class Engine {
    private String type; // 引擎类型

    // 构造函数、方法等
}

public class Color {
    private String colorName; // 颜色名称

    // 构造函数、方法等
}
类图示例
+------------------+
|       Car        |
+------------------+
| - engine: Engine |
| - color: Color   |
+------------------+
| + Car(Engine, Color) |
+------------------+

+------------------+
|      Engine      |
+------------------+
| - type: String   |
+------------------+

+------------------+
|      Color       |
+------------------+
| - colorName: String |
+------------------+
代码内容解释
  • 在这种设计中,Car 类通过组合 EngineColor 对象来实现汽车的分类。这种方式避免了使用继承来处理不同的分类组合,减少了类的数量,提高了系统的灵活性和可维护性。
  • 通过组合,我们可以轻松地添加新的动力源或颜色,只需创建新的 EngineColor 对象,而无需修改 Car 类或创建大量的新子类。

原则总结

合成复用原则强调通过对象的组合和聚合来实现功能的复用,而不是单纯依赖继承。这种方式可以有效地减少系统中的类数量,降低类之间的耦合度,提高系统的灵活性和可维护性。通过实际的例子,我们可以看到合成复用的优势及其在软件设计中的实际应用。


结束语:

在第一章的探索中,我们一起走过了设计模式的起点,揭开了软件设计中那些经典且至关重要的原则。通过对设计模式的概述UML图的解读以及软件设计原则的深入分析,我们为接下来的学习奠定了坚实的基础。

设计模式不仅仅是编程中的一套规则,更是一种思维方式和设计理念。它们帮助我们在复杂的系统中找到更简洁、更高效的解决方案。了解这些原则和模式,让我们的代码不仅更具可读性和可维护性,也能在面对各种挑战时,灵活应对、迅速调整。

希望你能将这些理论知识内化为实践的能力。将开闭原则里氏代换原则依赖倒转原则迪米特法则合成复用原则运用到实际开发中,让它们成为你解决问题的利器。不断尝试、不断实践,你会发现这些设计模式和原则将成为你编程道路上不可或缺的伙伴。

在接下来的章节中,我们将深入探讨各种设计模式,逐步揭开它们的神秘面纱。通过对创建者模式结构型模式行为型模式的逐一解析,你将能够更全面地理解如何在实际项目中应用这些模式。我们还将通过综合练习,实践这些模式,进一步巩固我们的学习成果。

让我们带着对编程的热情和对设计的执着,继续前行,探索更广阔的编程世界。无论是面对设计中的挑战,还是追求代码的优雅,设计模式都将是你实现卓越的钥匙。期待在接下来的章节中与你一起深入探讨,让我们在实践中不断成长,迎接每一个编程挑战的到来!

让我们一起学习,一起进步!期待在评论区与你们见面。

祝学习愉快!

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值