在第一章开始之前,整个设计模式专栏的快速访问地址给各位朋友们贴上:
《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类图中,类通过一个带有分割线的矩形来表示。这个矩形被分为三部分:
-
类名:矩形的最上部分,用于显示类的名称。
-
属性(Field):矩形的中间部分,用于显示类的属性,每个属性的表示方式如下:
- 可见性:使用符号表示,如
+
(public)、-
(private)和#
(protected)。 - 名称:属性的名称。
- 类型:属性的数据类型。
- 缺省值(可选):属性的默认值。
属性的完整表示方式是:
可见性 名称 : 类型 [ = 缺省值]
。 - 可见性:使用符号表示,如
-
方法(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.定义抽象类
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
是一个抽象类,定义了皮肤的基本功能。DefaultSkin
和HeimaSkin
是具体的皮肤实现类,它们继承自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
类,但它重写了setWidth
和setHeight
方法,以保持正方形的特性(宽度等于高度)。 - 这种实现方式导致当调用
Rectangle
类的setWidth
和setHeight
方法时,不能正确处理正方形的约束(即宽度和高度总是相等)。
解决方案
要遵循里氏替换原则,可以考虑将 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;
}
}
类与类的解释:
- 在这个改进后的设计中,
Rectangle
和Square
不再存在继承关系。Square
不继承自Rectangle
,避免了因继承引发的问题。 - 这样,
Rectangle
和Square
都是Shape
类的子类,各自独立管理自己的属性和方法,从而避免了继承带来的约束问题。
3.3 依赖倒置原则(DIP)
依赖倒置原则(Dependency Inversion Principle) 是面向对象设计中的一个重要原则,旨在减少系统模块之间的耦合度,提高系统的灵活性和可维护性。
原则概述
依赖倒置原则的核心思想是:
- 高层模块不应该依赖低层模块,两者都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
简单来说,就是要求在编程时依赖于抽象接口或抽象类,而不是具体的实现。这种方式可以降低系统中客户代码与具体实现模块之间的耦合度,使得系统更容易进行扩展和维护。
实现方式
- 定义抽象层:创建接口或抽象类,定义系统需要的操作和行为。
- 实现具体细节:让具体的实现类依赖于这些抽象层,提供实际的功能实现。
- 高层模块依赖于抽象:将系统的高层逻辑依赖于抽象层,而不是具体实现。
示例:组装电脑
场景分析:组装一台电脑需要多个配件,如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();
}
}
代码具体解释
- 在这个示例中,
CPU
、HardDrive
和RAM
是抽象接口,定义了电脑组装所需的功能。 IntelCPU
、SeagateHardDrive
和KingstonRAM
是具体的实现类,提供了不同品牌的具体实现。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("传智品牌安全门防盗功能...");
}
}
代码内容解释
- 在这个改进后的设计中,功能被拆分成了三个独立的接口:
FireProtection
、WaterProtection
和TheftProtection
。 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
类通过组合Engine
和Color
对象来实现汽车的分类。这种方式避免了使用继承来处理不同的分类组合,减少了类的数量,提高了系统的灵活性和可维护性。 - 通过组合,我们可以轻松地添加新的动力源或颜色,只需创建新的
Engine
或Color
对象,而无需修改Car
类或创建大量的新子类。
原则总结
合成复用原则强调通过对象的组合和聚合来实现功能的复用,而不是单纯依赖继承。这种方式可以有效地减少系统中的类数量,降低类之间的耦合度,提高系统的灵活性和可维护性。通过实际的例子,我们可以看到合成复用的优势及其在软件设计中的实际应用。
结束语:
在第一章的探索中,我们一起走过了设计模式的起点,揭开了软件设计中那些经典且至关重要的原则。通过对设计模式的概述、UML图的解读以及软件设计原则的深入分析,我们为接下来的学习奠定了坚实的基础。
设计模式不仅仅是编程中的一套规则,更是一种思维方式和设计理念。它们帮助我们在复杂的系统中找到更简洁、更高效的解决方案。了解这些原则和模式,让我们的代码不仅更具可读性和可维护性,也能在面对各种挑战时,灵活应对、迅速调整。
希望你能将这些理论知识内化为实践的能力。将开闭原则、里氏代换原则、依赖倒转原则、迪米特法则和合成复用原则运用到实际开发中,让它们成为你解决问题的利器。不断尝试、不断实践,你会发现这些设计模式和原则将成为你编程道路上不可或缺的伙伴。
在接下来的章节中,我们将深入探讨各种设计模式,逐步揭开它们的神秘面纱。通过对创建者模式、结构型模式、行为型模式的逐一解析,你将能够更全面地理解如何在实际项目中应用这些模式。我们还将通过综合练习,实践这些模式,进一步巩固我们的学习成果。
让我们带着对编程的热情和对设计的执着,继续前行,探索更广阔的编程世界。无论是面对设计中的挑战,还是追求代码的优雅,设计模式都将是你实现卓越的钥匙。期待在接下来的章节中与你一起深入探讨,让我们在实践中不断成长,迎接每一个编程挑战的到来!
让我们一起学习,一起进步!期待在评论区与你们见面。
祝学习愉快!