瑞_23种设计模式_概述(含代码)

文章目录

    • 1 设计模式
      • 1.1 概念
      • 1.2 背景
      • 1.3 优点
      • 1.4 分类
        • 1.4.1 创建型模式
        • 1.4.2 结构型模式
        • 1.4.3 行为型模式
        • 1.4.4 图表
    • 2 UML图
      • 2.1 概述
      • 2.2 作用
      • 2.3 UML类图
        • 2.3.1 概念
        • 2.3.2 作用
        • 2.3.3 类图中类的表示法
      • 2.4 类与类之间的表示方式
        • 2.4.1 关联关系
        • 2.4.2 聚合关系
        • 2.4.3 组合关系
        • 2.4.4 依赖关系
        • 2.4.5 继承(泛化)关系
        • 2.4.6 实现关系
    • 3 设计模式6大法则
      • 3.1 开闭原则(Open Close Principle)
      • 3.2 里氏代换原则(Liskov Substitution Principle)
      • 3.3 依赖倒转原则(Dependence Inversion Principle)
      • 3.4 接口隔离原则(Interface Segregation Principle)
      • 3.5 迪米特法则,又称最少知道原则(Demeter Principle)
      • 3.6 合成复用原则(Composite Reuse Principle)

前言:本文章为瑞_系列专栏之《23种设计模式》的概述篇,主要介绍软件设计模式的概念、背景、优点、分类、以及UML图的基本知识和设计模式的6大法则。由于博主是从菜鸟教程|设计模式以及黑马程序员Java设计模式详解学习设计模式的相关知识,所以文中的部分图和概念是出于它们。本系列专栏需要具备基本的Java编程知识概念,建议您有一定编程基础再阅读本系列专栏。




1 设计模式

1.1 概念

  软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。也就是说,它是解决特定问题的一系列套路,是软件开发人员在软件开发过程中面临的一般问题的解决方案,这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的,具有一定的普遍性,可以反复使用。

设计模式是构建高效软件工程的基石。能够帮助我们创建出更灵活、可复用、可维护的软件系统。

1.2 背景

  说到背景,就不得不提及四人组GOF。即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四位作者合作出版了《 Design Patterns - Elements of Reusable Object-Oriented Software》(中文译名:设计模式 - 可复用的面向对象软件元素) 一书,在此书中收录了23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这4位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)著称。

  他们所提出的设计模式主要是基于以下的面向对象设计原则:

  • 对接口编程而不是对实现编程
  • 优先使用对象组合而不是继承

  但神奇的是,这四位大佬一开始并非从事计算机科学领域,而是建筑领域的大师。果然,打败你的不⼀定是同⾏,⽽是跨界。所以设计模式这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中。

1.3 优点

  设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解,正确使用设计模式具有以下优点:

  • 提高代码的可复用性:设计模式提供了一种共享的设计词汇和概念,使得我们可以在不同的项目中重用相同的设计思路,促进了代码的重用,避免了重复的设计和实现,从而提高代码的可复用性,且能够更好地沟通和理解彼此的设计意图。
  • 提高代码的灵活性:设计模式使得我们的代码更加灵活,可以轻松地应对需求的变化。通过使用设计模式,我们可以将变化的影响限制在一个较小的范围内,而不会影响到整个系统。
  • 提高代码的可维护性:设计模式使得我们的代码更加清晰、易于理解。通过使用设计模式,我们可以将复杂的问题分解成更小、更易于管理的部分,从而提高代码的可维护性。
  • 提高代码的质量:通过遵循设计模式,可以减少系统中的错误和问题,使程序设计更加标准化、代码编制更加工程化。

设计模式不是新的技术,是面向对象的实际运用。学习设计模式主要是学习这些模式的逻辑思维,使用设计模式去编写代码可以让代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强

1.4 分类

  根据意图、复杂程度和流行程度,设计模式有多种分类方法,通常分为三大类:创建型模式、结构型模式和行为型模式。

1.4.1 创建型模式

  关注点是怎样创建对象即对象的创建过程,它的主要特点是将对象的创建与使用分离。以此降低系统的耦合度,使用者不需要关注对象的创建细节。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。

创建型模式某种意义上:就是解耦

1.4.2 结构型模式

  关注对象之间的组合和关系,旨在解决如何构建灵活且可复用的类和对象结构。这类模式描述如何将对象和类组合成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。结构型模式又分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足合成复用原则,所以对象结构型模式比类结构型模式具有更大的灵活性

1.4.3 行为型模式

  关注对象之间的通信和交互,旨在解决对象之间的责任分配和算法的封装。用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。行为型模式又分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。

由于组合关系或聚合关系比继承关系耦合度低,满足合成复用原则,所以对象行为模式比类行为模式具有更大的灵活性

1.4.4 图表
分类 模式 描述
创建型模式 1 单例模式(Singleton Pattern) 提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象
创建型模式 2 工厂模式(Factory Pattern) 提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象
创建型模式 3 原型模式(Prototype Pattern) 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象
创建型模式 4 抽象工厂模式(Abstract Factory Pattern) 是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象
创建型模式 5 建造者模式(Builder Pattern) 使用多个简单的对象一步一步构建成一个复杂的对象
结构型模式 1 代理模式(Proxy Pattern) 一个类代表另一个类的功能,给某对象提供一个代理以控制对该对象的访问
结构型模式 2 适配器模式(Adapter Pattern) 作为两个不兼容的接口之间的桥梁,它结合了两个独立接口的功能
结构型模式 3 装饰者模式(Decorator Pattern) 允许向一个现有的对象添加新的功能,同时又不改变其结构
结构型模式 4 桥接模式(Bridge Pattern) 用于把抽象化与实现化解耦,使得二者可以独立变化。通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦
结构型模式 5 外观模式(Facade Pattern) 隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。它向现有的系统添加一个接口,来隐藏系统的复杂性
结构型模式 6 组合模式(Composite Pattern) 是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次,创建了对象组的树形结构
结构型模式 7 享元模式(Flyweight Pattern) 用于减少创建对象的数量,以减少内存占用和提高性能,它提供了减少对象数量从而改善应用所需的对象结构的方式
行为型模式 1 模板模式(Template Pattern) 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤
行为型模式 2 策略模式(Strategy Pattern) 定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码
行为型模式 3 命令模式(Command Pattern) 是一种数据驱动的设计模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令
行为型模式 4 职责链模式(Chain of Responsibility Pattern) 为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦
行为型模式 5 状态模式(State Pattern) 对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为
行为型模式 6 观察者模式(Observer Pattern) 又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己
行为型模式 7 中介者模式(Mediator Pattern) 是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护
行为型模式 8 迭代器模式(Iterator Pattern) 是 Java 和 .Net 编程环境中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示
行为型模式 9 访问者模式(Visitor Pattern) 使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作
行为型模式 10 备忘录模式(Memento Pattern) 保存一个对象的某个状态,以便在适当的时候恢复对象
行为型模式 11 解释器模式(Interpreter Pattern) 提供了评估语言的语法或表达式的方式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等

以上模式的具体介绍后续会发布到本专栏系列中




2 UML图

2.1 概述

  统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言,是一种基于统一建模语言的图表,用于描述软件系统的结构、行为和交互。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。UML图从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图,使得开发人员可以更清晰地表达和沟通软件系统的设计思想。

为了更好的学习设计模式,需要了解类图

2.2 作用

  UML可以帮助开发人员更好地理解和设计软件系统。通过使用UML图,开发人员可以更清晰地表达和沟通软件系统的结构、行为和交互,从而更好地理解和管理复杂的软件系统。UML图还可以帮助开发人员更好地进行系统分析和设计,提高软件的可维护性、可复用性和可扩展性。

  • 提高沟通效率:UML提供了统一的标准化图形符号,可以简化软件开发团队成员之间的沟通,减少沟通成本和误解。
  • 促进软件开发流程的正规化:UML提供了一套完整的、标准的、面向对象的软件开发模型,可以帮助开发人员更好地组织开发过程,规范开发流程。
  • 改善软件开发的可维护性:UML提供了丰富的图形符号和语法规则,开发人员可以通过UML建立精确的软件模型,以提高软件的可维护性和可扩展性。
  • 提高代码的可读性:UML图形符号可以直观地展示软件结构和设计思路,对于阅读和理解代码都有很大的帮助。

  UML图和设计模式之间存在一定的关联性。在软件设计过程中,UML图可以用来表示和描述软件系统的结构,而设计模式则可以用来指导和优化软件系统的结构。例如,当我们使用UML类图来描述软件系统的类和它们之间的关系时,我们可以通过设计模式来选择合适的设计模式来实现这些类和关系。

2.3 UML类图

2.3.1 概念

  类图(Class diagram)显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。

2.3.2 作用
  • 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;
  • 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
2.3.3 类图中类的表示法

  在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个People类,它包含name,age和address这3个属性,以及work()方法。

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

  属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:

  • +:表示public
  • -:表示private
  • #:表示protected

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

注意:
​中括号中的内容表示是可选的
​也有将类型放在变量名前面,返回值类型放在方法名前面

例如:

Class
+method1() : void
-method2() : int
#method3(int var1,String var2) : String

上图Class类定义了三个方法:

  • method1()方法:修饰符为public,没有参数,没有返回值。
  • method2()方法:修饰符为private,没有参数,返回值类型为int。
  • method3()方法:修饰符为protected,接收两个参数,第一个参数类型为int,第二个参数类型为String,返回值类型是String。

2.4 类与类之间的表示方式

类和类之间存在着六种关系,由弱到强分别是 依赖<关联<聚合<组合<实现=继承

2.4.1 关联关系

  关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、博主和读者等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。
  关联又可以分为单向关联,双向关联,自关联。

  1. 单向关联
单向关联
Address
People
-address:Address

  在UML类图中单向关联用一个带箭头的实线表示。上图表示每个人都有一个地址,这通过让People类持有一个类型为Address的成员变量类实现。

  1. 双向关联
双向关联
Product
customer:Customer
Customer
-products:List< Product >

  从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。

  1. 自关联
自关联
Node
node:Node

  自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。

2.4.2 聚合关系

  聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:

聚合
University
teas:List< Teacher >
Teacher
-name:String
+teach() : void
2.4.3 组合关系

  组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:

组合
Head
-mouth:Mouth
Mouth
+eat() : void
2.4.4 依赖关系

  依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:

依赖
Car
+mover() : void
Driver
-name:String
+driver(Car car) : void
2.4.5 继承(泛化)关系

  继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。在 UML 类图中,继承关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示(注意一下markDown画出的继承关系是实心三角箭头):

类Student 继承自类Person
类Teacher 继承自类Person
Person
-name:String
-age:int
+speak() : void
Student
-studentNo:String
+study() : void
Teacher
-teacherNo:String
+teach() : void
2.4.6 实现关系

  实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如下图所示(注意一下markDown画出的实现关系是实心三角箭头):

实现
实现
«interface»
Vechicle
+move() : void
Car
+move() : void
Ship
+move() : void

类和类之间存在着六种关系,由弱到强分别是 依赖<关联<聚合<组合<实现=继承




3 设计模式6大法则

  在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本

3.1 开闭原则(Open Close Principle)

  开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

对扩展开放,对修改关闭

  【例】搜狗输入法的皮肤设计

  分析:搜狗输入法的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin和HeimaSpecificSkin)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。类图如下:

瑞_23种设计模式_概述(含代码)_第1张图片

/**
 * 第1步:定义抽象类
 *
 * @author LiaoYuXing-Ray
 * @version 1.0
 * @createDate 2023/11/18 16:18
 */
public abstract class AbstractSkin {

    /**
     * 显示的方法
     *
     * @author LiaoYuXing-Ray 2023/11/18 16:18
     **/
    public abstract void display();
}

/**
 * 第2步:默认皮肤类
 *
 * @author LiaoYuXing-Ray
 * @version 1.0
 * @createDate 2023/11/18 16:18
 */
public class DefaultSkin extends AbstractSkin {

    public void display() {
        System.out.println("默认皮肤");
    }
}

/**
 * 第3步:定义Ray皮肤类
 *
 * @author LiaoYuXing-Ray
 * @version 1.0
 * @createDate 2023/11/18 16:18
 */
public class RaySkin extends AbstractSkin {

    public void display() {
        System.out.println("Ray皮肤");
    }
}

/**
 * 第4步:聚合 搜狗输入法
 *
 * @author LiaoYuXing-Ray
 * @version 1.0
 * @createDate 2023/11/18 16:18
 */
public class SouGouInput {

    private AbstractSkin skin;

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

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

/**
 * 第5步:测试
 *
 * @author LiaoYuXing-Ray
 * @version 1.0
 * @createDate 2023/11/18 16:18
 */
public class RayTest {
    public static void main(String[] args) {
        // 1 创建搜狗输入法对象
        SouGouInput input = new SouGouInput();
        // 2 创建皮肤对象
        //DefaultSkin skin = new DefaultSkin();
        RaySkin skin = new RaySkin();
        // 3 将皮肤设置到输入法中
        input.setSkin(skin);

        // 4 显示皮肤
        input.display();
    }
}

3.2 里氏代换原则(Liskov Substitution Principle)

  里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

  下面看一个里氏替换原则中经典的一个例子

  【例】正方形不是长方形。

  在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。类图如下:

瑞_23种设计模式_概述(含代码)_第2张图片

代码如下:
长方形类(Rectangle):

public class Rectangle {
    private double length;
    private double width;

    public double getLength() {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    public double getWidth() {
        return width;
    }

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

长方形类(Rectangle):
由于正方形的长和宽相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同值。

public class Square extends Rectangle {
    
    public void setWidth(double width) {
        super.setLength(width);
        super.setWidth(width);
    }

    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }
}

RectangleDemo:是我们的软件系统中的一个组件,它有一个resize方法依赖基类Rectangle,resize方法是该类中的一个方法,用来实现宽度逐渐增长的效果。

public class RectangleDemo {
    
    public static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    //打印长方形的长和宽
    public static void printLengthAndWidth(Rectangle rectangle) {
        System.out.println(rectangle.getLength());
        System.out.println(rectangle.getWidth());
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setLength(20);
        rectangle.setWidth(10);
        resize(rectangle);
        printLengthAndWidth(rectangle);

        System.out.println("============");

        Rectangle rectangle1 = new Square();
        rectangle1.setLength(10);
        resize(rectangle1);
        printLengthAndWidth(rectangle1);
    }
}

  我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。

我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形

下面我们进行改进

  我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口,类图如下:
瑞_23种设计模式_概述(含代码)_第3张图片

  符合里氏代换原则设计代码如下:

/**
 * @Description: 四边形接口
 */
public interface Quadrilateral {

    // 获取长
    double getLength();

    // 获取宽
    double getWidth();
}

/**
 * @Description: 长方形类
 */
public class Rectangle implements Quadrilateral {

    private double length;
    private double width;

    public void setLength(double length) {
        this.length = length;
    }

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

    public double getLength() {
        return length;
    }

    public double getWidth() {
        return width;
    }
}

/**
 * @Description: 正方形
 */
public class Square implements Quadrilateral {

    private double side;

    public double getSide() {
        return side;
    }

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

    public double getLength() {
        return side;
    }

    public double getWidth() {
        return side;
    }
}

/**
 * @Description: 改进 只能传递长方形对象,因为不存在父子关系
 */
public class RectangleDemo {
    public static void main(String[] args) {
        // 创建长方形对象
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        // 调用方法进行扩宽操作
        resize(r);

        printLengthAndWidth(r);
    }

    // 扩宽的方法 - 改进 只能传递长方形对象,因为不存在父子关系
    public static void resize(Rectangle rectangle) {
        // 判断宽如果比长小,进行扩宽的操作
        while(rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    // 打印长和宽
    public static void printLengthAndWidth(Quadrilateral quadrilateral) {
        System.out.println(quadrilateral.getLength());
        System.out.println(quadrilateral.getWidth());
    }
}

3.3 依赖倒转原则(Dependence Inversion Principle)

  这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

是开闭原则具体的实现、面向接口编程。高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

  【例】组装电脑

  现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿等。类图如下:
瑞_23种设计模式_概述(含代码)_第4张图片

代码如下:

希捷硬盘类(XiJieHardDisk):

public class XiJieHardDisk implements HardDisk {

    public void save(String data) {
        System.out.println("使用希捷硬盘存储数据" + data);
    }

    public String get() {
        System.out.println("使用希捷希捷硬盘取数据");
        return "数据";
    }
}

Intel处理器(IntelCpu):

public class IntelCpu implements Cpu {

    public void run() {
        System.out.println("使用Intel处理器");
    }
}

金士顿内存条(KingstonMemory):

public class KingstonMemory implements Memory {

    public void save() {
        System.out.println("使用金士顿作为内存条");
    }
}

电脑(Computer):

public class Computer {

    private XiJieHardDisk hardDisk;
    private IntelCpu cpu;
    private KingstonMemory memory;

    public IntelCpu getCpu() {
        return cpu;
    }

    public void setCpu(IntelCpu cpu) {
        this.cpu = cpu;
    }

    public KingstonMemory getMemory() {
        return memory;
    }

    public void setMemory(KingstonMemory memory) {
        this.memory = memory;
    }

    public XiJieHardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(XiJieHardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public void run() {
        System.out.println("计算机工作");
        cpu.run();
        memory.save();
        String data = hardDisk.get();
        System.out.println("从硬盘中获取的数据为:" + data);
    }
}

测试类(TestComputer):测试类用来组装电脑。

public class TestComputer {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.setHardDisk(new XiJieHardDisk());
        computer.setCpu(new IntelCpu());
        computer.setMemory(new KingstonMemory());

        computer.run();
    }
}

  上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。

根据依赖倒转原则进行改进

  代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。类图如下:

瑞_23种设计模式_概述(含代码)_第5张图片
电脑(Computer):

public class Computer {

    private HardDisk hardDisk;
    private Cpu cpu;
    private Memory memory;

    public HardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public Cpu getCpu() {
        return cpu;
    }

    public void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }

    public Memory getMemory() {
        return memory;
    }

    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    public void run() {
        System.out.println("计算机工作");
    }
}

面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

3.4 接口隔离原则(Interface Segregation Principle)

  这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

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

  【例】安全门案例

  我们需要创建一个阿瑞品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:

«interface»
SafetyDoor
+antTheft() : void
+fireproof() : void
+waterproof() : void
RaySafetyDoor
+antTheft() : void
+fireproof() : void
+waterproof() : void

  上面的设计我们发现了它存在的问题,阿瑞品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个CSDN品牌的安全门,而该安全门只具有防盗、防水功能而没有防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:

RaySafetyDoor
+antTheft() : void
+fireproof() : void
+waterproof() : void
CSDNSafetyDoor
+antTheft() : void
+fireproof() : void
«interface»
AntTheft
+antTheft() : void
«interface»
Fireproof
+fireproof() : void
«interface»
Waterproof
+waterproof() : void

代码如下:
AntiTheft(接口):

public interface AntiTheft {
    void antiTheft();
}

Fireproof(接口):

public interface Fireproof {
    void fireproof();
}

Waterproof(接口):

public interface Waterproof {
    void waterproof();
}

RaySafetyDoor(类):

public class RaySafetyDoorimplements AntiTheft,Fireproof,Waterproof {
    public void antiTheft() {
        System.out.println("防盗");
    }

    public void fireproof() {
        System.out.println("防火");
    }


    public void waterproof() {
        System.out.println("防水");
    }
}

CSDNSafetyDoor(类):

public class CSDNSafetyDoor implements AntiTheft,Fireproof {
    public void antiTheft() {
        System.out.println("防盗");
    }

    public void fireproof() {
        System.out.println("防火");
    }
}

3.5 迪米特法则,又称最少知道原则(Demeter Principle)

  最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

降低类之间的耦合度,外观模式就是低迷特法则的典型应用。比如:你租房,你会去和中介谈,然后中介和房东沟通。因为平时房东很忙,这样降低了房东类和租房者类的耦合度。

  【例】明星与经纪人的关系实例

  明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。类图如下:

瑞_23种设计模式_概述(含代码)_第6张图片

代码如下:

明星类(Star)

public class Star {
    private String name;

    public Star(String name) {
        this.name=name;
    }

    public String getName() {
        return name;
    }
}

粉丝类(Fans)

public class Fans {
    private String name;

    public Fans(String name) {
        this.name=name;
    }

    public String getName() {
        return name;
    }
}

媒体公司类(Company)

public class Company {
    private String name;

    public Company(String name) {
        this.name=name;
    }

    public String getName() {
        return name;
    }
}

经纪人类(Agent)

public class Agent {
    private Star star;
    private Fans fans;
    private Company company;

    public void setStar(Star star) {
        this.star = star;
    }

    public void setFans(Fans fans) {
        this.fans = fans;
    }

    public void setCompany(Company company) {
        this.company = company;
    }

    public void meeting() {
        System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。");
    }

    public void business() {
        System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
    }
}

3.6 合成复用原则(Composite Reuse Principle)

  合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。通常类的复用分为继承复用和合成复用两种。继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

  采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  • 对象间的耦合度低。可以在类的成员位置声明抽象。
  • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

在阿里巴巴的Java开发手册也是推荐能组合聚合就组合聚合。除非万不得已,比如 “baseController” 或者 “钱有子类:人民币、美元” 等情况才使用继承

  【例】汽车分类管理程序

  汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。类图如下:

瑞_23种设计模式_概述(含代码)_第7张图片
  从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。下面将继承复用改为聚合复用,类图如下:

瑞_23种设计模式_概述(含代码)_第8张图片




本文是博主的粗浅理解,可能存在一些错误或不完善之处,如有遗漏或错误欢迎各位补充,谢谢

  如果觉得这篇文章对您有所帮助的话,请动动小手点波关注,你的点赞收藏⭐️转发评论都是对博主最好的支持~

你可能感兴趣的:(23种设计模式,设计模式,java,笔记)