Java18 设计模式

第十八节:设计模式

1.设计模式概述

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

​ "设计模式"最初并不是出现在软件设计中,而是被用于建筑领域的设计中。1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。

​ 1990年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)著称。

1.2 软件设计模式的概念

软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。

1.3 学习设计模式的必要性

​ 设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点。
● 可以提高程序员的思维能力、编程能力和设计能力。
● 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
● 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
1.4 设计模式分类
● 创建型模式
​ 用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
● 结构型模式
​ 用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
● 行为型模式
​ 用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

2.UML图

​ 统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。UML 本身是一套符号的规定,就像数学符号和化学符号一样,这些符号用于描述软件模型中的各个元素和他们之间的关系,比如类、接口、实现、泛化、依赖、组合、聚合等。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

2.1 类图概述

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

2.2 类图的作用

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

2.3 类图表示法

2.3.1 类的表示方式

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

image.png

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
● +:表示public
● -:表示private
● #:表示protected
属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
注意:
1,中括号中的内容表示是可选的
2,也有将类型放在变量名前面,返回值类型放在方法名前面

例如:

image.png

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

​ 关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。
关联又可以分为单向关联,双向关联,自关联。

1,单向关联

image.png

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

2,双向关联

image.png

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

3,自关联

image.png

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

2.3.2.2 聚合关系

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

image.png

​ 举例2:一台电脑由键盘(keyboard)、显示器(monitor),鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来的,使用带空心菱形的实线来表示:
image.png

2.3.2.3 组合关系

​ 组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。

举例1:头和嘴的关系,没有了头,嘴也就不存在了。在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
image.png

举例2:如果我们认为 Mouse、Monitor 和 Computer 是不可分离的,则升级为组合关系

2.3.2.4 依赖关系

​ 依赖关系通常指的是类之间的调用关系,即一个类通过局部变量、方法参数或静态方法调用另一个类,只要是在类中用到了对方,那么他们之间就存在依赖关系。如果没有对方,连编译都通过不了。依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

举例1:
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:

image.png

举例2:

image.png

public class PersonServiceBean {// 类的成员属性private PersonDao personDao;// 方法接收的参数类型public void save(Person person) {}// 方法的返回类型public IDCard getIDCard(Integer personid) {return null;}// 方法中使用到public void modify() {Department department = new Department();}}

小结
● 1)类中用到了对方
● 2)类的成员属性
● 3)方法的返回类型
● 4)方法接收的参数类型
● 5)方法中使用到

2.3.2.5 继承关系

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

image.png

小结
● 1)继承关系实也被称为泛化关系
● 2)如果 A 类继承了 B 类,我们就说 A 和 B 存在泛化关系

2.3.2.6 实现关系

​ 实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。它是依赖关系的特例。
举例1:
​ 在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如图所示。

image.png

3.设计模式七大原则

3.1 设计模式目的

​ 编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好的
● 1)可维护性:在不破坏原有代码设计、不引入新的bug的情况下,能够快速的修改或者添加代码
​ 举例:比如iphone在维修手机摄像头的时候,如果手抖,可能导致喇叭或者麦克风的损坏,从而影响通讯或者音配的功能,因为他们都在一个继承电路板上,但是,单反镜头维修时,就不存在这种情况。
● 2)可扩展性 在不修改或者少量修改原有代码的情况下,可以通过扩展的方式添加新的功能代码。(即:当需要增加新的功能时,非常的方便,也叫做可维护性) 举例:比如用手机拍月亮的时候,怎么拍效果都不好,这个时候隔壁老王把单方架在三脚架上,换上了长焦镜头。这个时候单反可以根据不同的拍照场景,扩展不同的镜头
● 3)可复用性(即:相同功能的代码,不用多次编写,尽量减少重复代码的编写,直接复用已有的代码)
● 4)可读性(即:编程规范性,便于其他程序员的阅读和理解)
● 5)使程序呈现高内聚,低耦合的特性(模块内部元素的紧密程度,内聚性越好,模块独立性越好,可维护性越高,复用性越高。耦合性是模块和模块之间的关联关系,耦合性越高,模块之间的关联关系越复杂,可维护性复用性越差)

3.2 设计模式七大原则

​ 在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据7条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础(即:设计模式为什么这样设计的依据)设计模式
常用的七大原则有:
● 1)单一职责原则
● 2)接口隔离原则
● 3)依赖倒转原则
● 4)里氏替换原则
● 5)开闭原则
● 6)迪米特法则
● 7)合成复用原则

3.2.1单一职责原则(Single Responsibility Principle)

基本介绍
对类来说的,即一个类或者一个模块应该只负责一项职责(或者功能)。通俗的讲,如果一个类包含了两个或者多个业务不相干的功能,那么这个类的职责就不够单一,应当将其拆分成多个功能更加单一,颗粒度更细的类。单一职责原则是实现高内聚低耦合的指导方针,它是最简单但又是最难运用的原则,需要设计人员发现类的不同职责并将其分离。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2

应用实例1:
1)以交通工具案例讲解,讲解一个模块负责一项职责
2)看代码演示
3)方案1[分析说明]

package single;public class SingleDemo01 {public static void main(String[] args) {Vehicle vehicle = new Vehicle();vehicle.run("汽车");vehicle.run("轮船");vehicle.run("飞机");}
}/*** 案例1方式1的分析* 1.在方式1的run方法中,违反了单一职责原则* 2.解决的方案非常的简单,根据交通工具运行方法不同,分解成不同类即可*/
class Vehicle{public void run(String type){if ("汽车".equals(type)) {System.out.println(type + "在公路上运行...");} else if ("轮船".equals(type)) {System.out.println(type + "在水面上运行...");} else if ("飞机".equals(type)) {System.out.println(type + "在天空上运行...");}}
}

4)方案2[分析说明]

package single;public class SingleDemo02 {public static void main(String[] args) {RoadVehicle roadVehicle = new RoadVehicle();roadVehicle.run("汽车");WaterVehicle waterVehicle = new WaterVehicle();waterVehicle.run("轮船");AirVehicle airVehicle = new AirVehicle();airVehicle.run("飞机");}
}/*** 方案2的分析* 1.遵守单一职责原则* 2.但是这样做的改动很大,即将类分解,同时修改客户端* 3.改进:直接修改Vehicle类,改动的代码会比较少=>方案3*/
class RoadVehicle{public void run(String type){System.out.println(type + "在公路上运行...");}
}
class WaterVehicle{public void run(String type){System.out.println(type + "在水面上运行...");}
}
class AirVehicle{public void run(String type){System.out.println(type + "在天空上运行...");}
}

5)方案3[分析说明]

package single;public class SingleDemo03 {public static void main(String[] args) {Vehicle2 vehicle = new Vehicle2();vehicle.run("汽车");vehicle.runWater("轮船");vehicle.runAir("飞机");}
}/*** 方式3的分析* 1.这种修改方法没有对原来的类做大的修改,只是增加方法* 2.这里虽然没有在类这个级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责*/
class Vehicle2{public void run(String type){System.out.println(type + "在公路上运行...");}public void runWater(String type){System.out.println(type + "在水面上运行...");}public void runAir(String type){System.out.println(type + "在天空上运行...");}
}

应用实例2:讲解一个类负责一项职责
未使用单一原则情况下UserInfo即包含了用户相关信息,还单独包含了地址相关的信息

image.png

未使用单一原则前:

package single;public class SingleDemo04 {}
/*** 案例2方式1分析* 1.在本类中的主要是和用户相关的信息,如果有地址相关的信息,*      尽可能定义好单独的地址类和设置地址的方法* @author APESOURCE**/
class UserInfo {long userID;String userName;String phone;String provice;String region;String detailAdress;public void save() {}public void saveAdress() {}}

使用单一职责时:

package single;public class SingleDemo05 {}class UserInfo1 {long userID;String userName;String phone;Address addressesList;public void save() {}}class Address {String provice;String region;String detailAdress;public void saveAdress() {}
}

注意事项和细节
● 1)降低类的复杂度,一个类只负责一项职责
● 2)提高类的可读性,可维护性
● 3)降低变更引起的风险
● 4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则

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

接口隔离原则(ISP):Interface Segregation Principle,接口最小粒度设计。客户端不应该被迫依赖于它不使用的方法,即一个类对另一个类的依赖应该建立在最小的接口上。一个类实现一个接口,就必须实现这个接口的所有抽象方法,如果接口设计的过于庞大的话,实现类就被迫实现不需要的抽象方法。

案例-需求1-开发一套手机接口功能PhoneFunction,包含通话call,短信message,摄像camera功能,基于这套接口开发了最新的苹果手机ApplePhone

image.png

public class JSPDemo01 {}interface PhoneFunction{void call();void message();void camera();
}class ApplePhone implements PhoneFunction{@Overridepublic void call() {System.out.println("苹果手机实现打电话");}@Overridepublic void message() {System.out.println("苹果手机实现发信息");}@Overridepublic void camera() {System.out.println("苹果手机实现照相");}}

需求2:根据市场需求,需要开发一款老年机,只具有电话、短信功能。

image.png

class OldPhone implements PhoneFunction{@Overridepublic void call() {System.out.println("老年机实现打电话");}@Overridepublic void message() {System.out.println("老年机实现发信息");}@Overridepublic void camera() {System.out.println("老年机被迫实现照相");}}

接口隔离原则

image.png

public class JSPDemo02 {}interface CallFunction{void call();
}
interface MessageFunction{void message();
}interface CameraFunction{void camera();
}class ApplePhone1 implements CallFunction,MessageFunction,CameraFunction{@Overridepublic void call() {System.out.println("苹果手机实现打电话");}@Overridepublic void message() {System.out.println("苹果手机实现发信息");}@Overridepublic void camera() {System.out.println("苹果手机实现照相");}}class OldPhone1 implements CallFunction,MessageFunction{@Overridepublic void call() {System.out.println("老年机实现打电话");}@Overridepublic void message() {System.out.println("老年机实现发信息");}}
3.2.3依赖倒转原则(Dependence Inversion Principle)

依赖倒转原则(DIP): Dependency Inversion Principle,模块之间要依赖抽象,不依赖实现,要面向接口编程,不要面向实现编程。高层模块不应该直接依赖低层模块(底层模块则是指那些不可分割的、基本的逻辑单元),这样就降低了客户端与实现模块间的耦合.
基本介绍
● 1)高层模块不应该依赖低层模块,二者都应该依赖其抽象
● 2)抽象不应该依赖细节,细节应该依赖抽象
● 3)依赖倒转(倒置)的中心思想是面向接口编程
● 4)依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
● 5)使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
应用实例:
开发一套电脑组装系统Computer,给电脑配备IntelCup及IntelMemory.

image.png

public class DIPDemo01 {public static void main(String[] args) {IntelCpu intelCpu = new IntelCpu();IntelMemory intelMemory = new IntelMemory();Computer computer = new Computer(intelCpu, intelMemory);computer.startRun();}}class Computer {private IntelCpu intelCpu;private IntelMemory intelMemory;public void startRun() {intelCpu.calculate();intelMemory.storage();}public Computer(IntelCpu intelCpu, IntelMemory intelMemory) {this.intelCpu = intelCpu;this.intelMemory = intelMemory;}
}class IntelCpu {public void calculate() {System.out.println("intel cpu运行中");}
}class IntelMemory {public void storage() {System.out.println("intel 内存存储中");}
}

根据市场需求,为了降低成本需要给电脑配备AmdCpu

image.png

public class DIPDemo02 {public static void main(String[] args) {ICpu cpu = new AmdCpu();IMemory memory = new IntelMemory1();Computer1 computer = new Computer1(cpu, memory);computer.startRun();}
}class Computer1 {private ICpu cpu;private IMemory memory;public void startRun() {cpu.calculate();memory.storage();}public Computer1(ICpu cpu, IMemory memory) {this.cpu = cpu;this.memory = memory;}
}interface ICpu{void calculate();
}interface IMemory{void storage();
}class IntelCpu1 implements ICpu{public void calculate() {System.out.println("intel cpu运行中");}
}class AmdCpu implements ICpu{public void calculate() {System.out.println("amd cpu运行中");}
}class IntelMemory1 implements IMemory{public void storage() {System.out.println("intel 内存存储中");}
}

注意事项和细节

● 1)低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
● 2)变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
● 3)继承时遵循里氏替换原则

3.2.4里氏替换原则(Liskov Substitution Principle)

里氏替换原则(LSP): Liskov Substitution Principle。子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,如果重写父类方法,程序运行会发生出错概率。如果一定要用多态,那么父类可以设计成抽象父类或者接口。

OO 中继承性的思考和说明

  • 1)继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏
  • 2)继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障

案例1:

一个程序引出的问题和思考

img

先看个程序,思考下问题和解决思路

public class LSPDemo01 {public static void main(String[] args) {// 遵守里氏替换Bird parrotBird = new Parrot();parrotBird.setFlySpeed(150.0);// 设置飞行速度System.out.println("飞行距离为300公里");System.out.println("将要飞行" + parrotBird.calcuFlyTime(300.0) + "小时");// 破坏里氏替换Bird duckBird = new Duck();parrotBird.setFlySpeed(50.0);// 设置飞行速度System.out.println("飞行距离为300公里");System.out.println("将要飞行" + duckBird.calcuFlyTime(300.0) + "小时");}}//添加鹦鹉类
class Parrot extends Bird {public void studyspeak() {System.out.println("英语学习人类说话");}}//添加鸭子类
class Duck extends Bird {// 鸭子会跑倒是不会飞,所以飞的速度设为0public void setFlySpeed(double flySpeed) {super.flySpeed = 0.0;}
}class Bird {// 走的速度protected double runSpeed;// 飞的速度protected double flySpeed;public void setRunSpeed(double runSpeed) {this.runSpeed = runSpeed;}public void setFlySpeed(double flySpeed) {this.flySpeed = flySpeed;}public double calcuFlyTime(double distance) {return distance / flySpeed;}public double calcuRunTime(double distance) {return distance / runSpeed;}}

原因就是类DUCK无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候

解决办法

public class LSPDemo02 {public static void main(String[] args) {// 遵守里氏替换Bird1 parrotBird = new Parrot1();parrotBird.setFlySpeed(150.0);// 设置飞行速度System.out.println("飞行距离为300公里");System.out.println("将要飞行" + parrotBird.calcuFlyTime(300.0) + "小时");// 破坏里氏替换Animal duckBird = new Animal();parrotBird.setFlySpeed(50.0);// 设置飞行速度System.out.println("飞行距离为300公里");
//		System.out.println("将要飞行" + duckBird.calcuFlyTime(300.0) + "小时");}}//添加鹦鹉类
class Parrot1 extends Bird1 {public void studyspeak() {System.out.println("英语学习人类说话");}}//添加鸭子类
class Duck1 extends Animal {
}class Animal {// 走的速度protected double runSpeed;public void setRunSpeed(double runSpeed) {this.runSpeed = runSpeed;}public double calcuRunTime(double distance) {return distance / runSpeed;}}class Bird1  extends Animal{// 飞的速度protected double flySpeed;public void setFlySpeed(double flySpeed) {this.flySpeed = flySpeed;}public double calcuFlyTime(double distance) {return distance / flySpeed;}}
3.2.5开闭原则(Open Closed Principle)

​ 开闭原则(OCP):open Closed Principle,对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
基本介绍
● 1)开闭原则是编程中最基础、最重要的设计原则
● 2)一个软件实体如类、模块和函数应该对扩展开放(对提供者而言),对修改关闭(对使用者而言)。用抽象构建框架,用实现扩展细节
● 3)当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
● 4)编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则

看一段代码
image.png

定义了一个类用来产生随机数,进行加法操作

import java.util.Random;public class OCPDemo01 {public static void main(String[] args) {Equation equation = new Equation().generateEquation();System.out.println(equation);}}//算式类
class Equation {protected int leftNum;protected int rightNum;protected int result;protected String op;@Overridepublic String toString() {return this.leftNum + op + this.rightNum + "=" + result;}// 产生指定区间随机数!// 案例:0-100或者50-100之间public int generateRantom(int min, int max) {Random random = new Random();return random.nextInt(max-min+1)+min;}public Equation generateEquation() {leftNum = generateRantom(0, 100);rightNum = generateRantom(0, 100);result = leftNum + rightNum;this.op = "+";return this;}}

此时,如果需求发生变化,要求再提供一个减法的呢?

import java.util.Random;public class OCPDemo01 {public static void main(String[] args) {Equation equation = new Equation().generateEquation("-");System.out.println(equation);}}//算式类
class Equation {protected int leftNum;protected int rightNum;protected int result;protected String op;@Overridepublic String toString() {return this.leftNum + op + this.rightNum + "=" + result;}// 产生指定区间随机数!// 案例:0-100或者50-100之间public int generateRantom(int min, int max) {Random random = new Random();return random.nextInt(max-min+1)+min;}public Equation generateEquation(String op) {leftNum = generateRantom(0, 100);rightNum = generateRantom(0, 100);if("+".equals(op)) {result = leftNum + rightNum;}else if("-".equals(op)) {result = leftNum - rightNum;}this.op = op;return this;}}

​ 如果要求再增加一个乘法和除法,是不是也同样需要进行逻辑的修改。但是大家要知道每次修改会导致这个程序,如果在一个项目开发过程中,知道可能会导致其他类的改变,我们需要重新测试一遍的所有相关的功能。所以在此处我们要对修改关闭,但是又要完成乘法,可以扩展开发~

import java.util.Random;public class OCPDemo02 {public static void main(String[] args) {Equation1 equation = new AddEquation().generateEquation();System.out.println(equation);}}//算式类
abstract class Equation1 {protected int leftNum;protected int rightNum;protected int result;protected String op;@Overridepublic String toString() {return this.leftNum + op + this.rightNum + "=" + result;}// 产生指定区间随机数!// 案例:0-100或者50-100之间public int generateRantom(int min, int max) {Random random = new Random();return random.nextInt(max-min+1)+min;}//此处只知道此处要产生一个算式,但是不知道要产生什么算式,可以给个抽象的方法public abstract Equation1 generateEquation() ;}class AddEquation extends Equation1{@Overridepublic Equation1 generateEquation() {leftNum = generateRantom(0, 100);rightNum = generateRantom(0, 100);result = leftNum + rightNum;this.op = "+";return this;}
}
3.2.6 迪米特法则(Demeter Principle)

​ 迪米特法则(LOD): Law of Demeter,迪米特法则来自于1987年美国东北大学的一个名为Demeter的一个研究项目只跟朋友联系,不跟“陌生人”说话。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
典型案例1:明星与经纪人的关系实例
​ 明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。

image.png

基本介绍

● 1)一个对象应该对其他对象保持最少的了解
● 2)类与类关系越密切,耦合度越大
● 3)迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息
● 4)迪米特法则还有个更简单的定义:只与直接的朋友通信
● 5)直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多:依赖、关联、组合、聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
应用实例
1)有一个学校,下属有各个学院和总部,现要求打印出学校总部员工 ID 和学院员工的 id
2)编程实现上面的功能,看代码演示

public class Demeter01 {public static void main(String[] args) {// 创建了一个SchoolManager对象SchoolManager schoolManager = new SchoolManager();// 输出学院的的员工ID和总部的员工IDschoolManager.printAllEmployee(new CollegeManager());}
}/*** 学院员工类*/
class Employee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}/*** 学院员工类*/
class CollegeEmployee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}/*** 管理学院员工的管理类*/
class CollegeManager {public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee> list = new ArrayList<>();for (int i = 0; i < 5; i++) {CollegeEmployee employee = new CollegeEmployee();employee.setId("学院员工的id = " + i);list.add(employee);}return list;}
}class SchoolManager {public List<Employee> getAllEmployee() {List<Employee> list = new ArrayList<>();for (int i = 0; i < 3; i++) {Employee employee = new Employee();employee.setId("学校总部员工的id = " + i);list.add(employee);}return list;}/*** 该方法完成输出学校总部和学院员工信息* @param collegeManager*/void printAllEmployee(CollegeManager collegeManager) {// 获取到学院员工List<CollegeEmployee> list1 = collegeManager.getAllEmployee();System.out.println("------学院员工-------");for (CollegeEmployee employee : list1) {System.out.println(employee.getId());}// 获取到总部员工List<Employee> list2 = this.getAllEmployee();System.out.println("-----总部员工-------");for (Employee employee : list2) {System.out.println(employee.getId());}}
}

应用实例改进
分析SchoolManager类的直接朋友有哪些?
1 Employee类是getAllEmployee()方法的返回值,所以是直接朋友;
2 CollegeManager类是printAllEmployee()方法的参数,所以是直接朋友。
3 CollegeEmployee是printAllEmployee方法的局部变量,不是通过方法参数传递进来的,所以不是直接朋友,是一个陌生类,违反了迪米特法则。
改进思路:
1、前面设计的问题在于SchoolManager中,CollegeEmployee类并不是SchoolManager类的直接朋友
2、按照迪米特法则,应该避免类中出现这样的非直接朋友关系的耦合。

public class Demeter02 {public static void main(String[] args) {// 创建了一个SchoolManager对象SchoolManager02 schoolManager = new SchoolManager02();// 输出学院的的员工ID和总部的员工IDschoolManager.printAllEmployee(new CollegeManager02());}
}/*** 学院员工类*/
class Employee02 {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}/*** 学院员工类*/
class CollegeEmployee02 {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}/*** 管理学院员工的管理类*/
class CollegeManager02 {public List<CollegeEmployee02> getAllEmployee() {List<CollegeEmployee02> list = new ArrayList<>();for (int i = 0; i < 5; i++) {CollegeEmployee02 employee = new CollegeEmployee02();employee.setId("学院员工的id = " + i);list.add(employee);}return list;}/*** 输出学院员工的信息*/public void printEmployee() {List<CollegeEmployee02> list = this.getAllEmployee();System.out.println("------学院员工-------");for (CollegeEmployee02 employee : list) {System.out.println(employee.getId());}}
}class SchoolManager02 {public List<Employee02> getAllEmployee() {List<Employee02> list = new ArrayList<>();for (int i = 0; i < 3; i++) {Employee02 employee = new Employee02();employee.setId("学校总部员工的id = " + i);list.add(employee);}return list;}/*** 该方法完成输出学校总部信息*/void printAllEmployee(CollegeManager02 collegeManager) {// 输出学院的员工方法 封装到CollegeManager02collegeManager.printEmployee();// 获取到总部员工List<Employee02> list1 = this.getAllEmployee();System.out.println("-----总部员工-------");for (Employee02 employee : list1) {System.out.println(employee.getId());}}
}

注意事项和细节
● 1)迪米特法则的核心是降低类之间的耦合
● 2)但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系

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

​ 合成复用原则(CRP); Composite Reuse Principle,合成复用原则是指: 尽量先使用组合或者聚合等关联关系来实现其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复出呈然有简单和易实现的优点,但是存在以下缺点:
​ 1.继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用.
​ 2.子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
1.它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
2.对象间的耦合度低。可以在类的成员位置声明(抽象类或者接口)。

image.png

public class CRPDemo01 {public static void main(String[] args) {B b  = new B();b.name = "张三";//父类的细节在这里b.methodA();//A类中发生变化他也会发生变化}
}class A{protected String name;public void methodA() {System.out.println("A类中的methodA方法调用");}
}
class B extends A{public void methodB() {System.out.println("B类中的methodB方法调用");}
}

修改后

public class CRPDemo01 {public static void main(String[] args) {B b = new B();// b.name = "张三";//父类的细节在这里b.methodA();// A类中发生变化他也会发生变化}
}class A {protected String name;public void methodA() {System.out.println("A类中的methodA方法调用");}
}class B {private A a = new A();public void methodA() {a.methodA();}public void methodB() {System.out.println("B类中的methodB方法调用");}
}
3.2.8 设计原则核心思想

● 1)找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
● 2)针对接口编程,而不是针对实现编程
● 3)为了交互对象之间的松耦合设计而努力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/414817.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安卓开发环境搭建1

1、参考地址&#xff1a;Android开发环境搭建_kiba518的博客-CSDN博客 安装jdk 安装Android.Studio 2、就这样&#xff0c;next&#xff0c;搞定安卓开发环境&#xff0c;耗时20分钟 3、参考视频&#xff1a; 三&#xff1a;Android Studio安装_哔哩哔哩_bilibili 1.手机设…

XSS漏洞

本质 变量在接收数据时&#xff0c;数据被写成js脚本&#xff0c;然后进行回显操作&#xff0c;就被浏览器执行&#xff1b;js可以干什么&#xff0c;这个漏洞就可以干什么 产生层面 前端 函数类 echo… 漏洞操作对应层 危害影响 js代码决定 浏览器内核版本 版本是否支持执…

Ubuntu Linux Server安装Kubernetes

本文主要描述在Ubuntu Linux Server操作系统中安装Kubernetes云原生对应的microk8s组件。 sudo snap install microk8s --classic 如上所示&#xff0c;在Ubuntu服务器中安装microk8s组件完成&#xff0c;对应的版本是microk8s v1.30版本 microk8s enable dashboard 如上所…

ISIS路由渗透

/ 实验介绍: / 原理概述 在IS-IS网络中&#xff0c;所有的Level-2和Level-1-2路由器构成了一个连续的骨干区域。Level-1区域必须且只能与骨干区域相连&#xff0c;不同的Level-1区域之间不能直接相连。Level-1区域内的路由信息会通过Level-1-2路由器通报给Level-2区域&#x…

语音测试(二)音频标注

一、标注工具&#xff08;praat&#xff09; Praat的主要功能是对自然语言的语音信号进行采集、分析和标注&#xff0c;并执行包括变换和滤波等在内的多种处理任务。 二、标注教程 音频标注工具&#xff08;praat&#xff09; a.打开praat软件在Praat Objects界面上选择Open选…

【全志H616】【开源】 ARM-Linux 智能分拣项目:阿里云、网络编程、图像识别

【全志H616】【开源】 ARM-Linux 智能分拣项目&#xff1a;阿里云、网络编程、图像识 文章目录 【全志H616】【开源】 ARM-Linux 智能分拣项目&#xff1a;阿里云、网络编程、图像识1、实现功能2、软件及所需环境3、逻辑流程图及简述3.1 完整逻辑流程图3.2 硬件接线3.3 功能简述…

我的sql我做主!Mysql 的集群架构详解之组从复制、半同步模式、MGR、Mysql路由和MHA管理集群组

目录 Mysql 集群技术一、Mysql 在服务器中的部署方法1.1 在Linux下部署mysql1.1.1 安装依赖性&#xff1a;1.1.2 下载并解压源码包1.1.3 源码编译安装mysql1.1.4 部署mysql 二、Mysql的组从复制2.1 配置mastesr2.2 配置salve2.3 当有数据时添加slave22.4 延迟复制2.5 慢查询日志…

wlanapi.dll丢失怎么办?有没有什么靠谱的修复wlanapi.dll方法

在遇到各种系统文件错误当中&#xff0c;其中之一就是“wlanapi.dll文件丢失”的问题。这种问题通常发生在Windows操作系统上&#xff0c;特别是当系统试图执行与无线网络相关的任务时。wlanapi.dll是一个重要的系统文件&#xff0c;它负责处理Windows无线网络服务的许多功能。…

【ESP32 】VScode -window环境配置(adruino开发)(点亮LED)

创建工程 新建工程 、 进行vs code的下载&#xff0c;等待一段时间 工程代码 #include <Arduino.h>// put function declarations here: int myFunction(int, int);void setup() {// put your setup code here, to run once:int result myFunction(2, 3);pinMode(2…

一分钟创建自己的分班查询系统,家长扫码即可进群

开学后&#xff0c;老师们的忙碌也达到了顶峰。整理教材、准备课程计划、布置教室&#xff0c;这些工作已经让人应接不暇&#xff0c;更别提还要处理分班事宜。以往&#xff0c;老师们需要一个个通知家长分班结果&#xff0c;这不仅耗时耗力&#xff0c;还容易出错。家长们也常…

【Qt】tcp服务器、tcp多线程服务器、心跳保持、服务端组包

文章目录 背景&#xff1a;代码实现&#xff08;服务端&#xff09;&#xff1a;总结改进方案&#xff1a;多线程tcp服务器代码实现&#xff08;服务端&#xff09;心跳保持&#xff1a;大文件收发 背景&#xff1a; 局域网内&#xff0c;客户端会进行udp广播&#xff0c;服务…

书法图片自动扣字的批处理

本程序会根据原文字图片&#xff0c;自动扣字并生成黑字、红字2个透明的png图片&#xff0c;原图片黑字或白字均可。运行的话需要先安装好 ImageMagick-7.1.1-37 用法与生成效果举例&#xff1a; a.jpg 白字 转 黑、红扣字png: b.jpg 黑字 转 黑、红扣字png: 分享脚本如下: …

Spring MVC 八股文

目录 重点 SpringMVC的工作原理 Spring MVC 拦截器 Spring MVC 的拦截器和 Filter 过滤器有什么差别&#xff1f; 基础 什么是SpringMVC SpringMVC的优点 Spring MVC的核心组件 Spring MVC的常用注解由有哪些 Controller 注解有什么用 重点 SpringMVC的工作原理 1、客…

OLED显示屏详解(IIC协议0.96寸 STM32)

目录 一、介绍 二、模块原理 1.原理图 2.工作原理&#xff1a;SSD1306显存与命令 三、程序设计 main.c文件 oled.h文件 oled.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 OLED是有机发光二极管&#xff0c;又称为有机电激光显示&#xff08;Organic Electrol…

U-Mail垃圾邮件网关:一站式邮件安全防护方案

在当今的数字化时代&#xff0c;电子邮件已成为企业日常运营中不可或缺的通讯工具。然而&#xff0c;随着电子邮件的广泛应用&#xff0c;垃圾邮件也日益成为困扰企业的一大难题。如何有效防止垃圾邮件入侵&#xff0c;确保企业邮件系统的安全稳定运行&#xff0c;已成为众多企…

Python进阶08-爬虫

零、文章目录 Python进阶08-爬虫 1、爬虫介绍 &#xff08;1&#xff09;爬虫是什么 **网络爬虫:**又被称为网页蜘蛛&#xff0c;网络机器人&#xff0c;是一种按照一定的规则&#xff0c;自动地抓取网络信息的程序或者脚本&#xff0c;另外一些不常使用的名字还有蚂蚁、自…

挂载磁盘时有多个文件系统

mount: /opt/storage/data1/: more filesystems detected on /dev/md5; use -t or wipefs(8). 1、解决方法一 mount -t ext4 /dev/md5 /opt/data2、解决方法二 #返回磁盘有那些文件系统和格式 wipefs /dev/md5 #清除文件系统和元数据 wipefs -a -f /dev/md5 #再次查看将没有任…

算法导论 总结索引 | 第五部分 第二十三章:最小生成树

需要将多个组件的针脚 连接在一起。要连接n个针脚&#xff0c;可以使用 n-1 根连线&#xff0c;每根连线连接两个针脚。很显然&#xff0c;希望所使用的连线长度最短 用一个连通无向图 G (V, E) 来予以表示&#xff0c;这里的 V 是针脚的集合&#xff0c;E 是针脚之间的可能连…

华为网络工程师证书等级有哪些?怎么备考?

华为网络工程师是由华为技术厂商推出的一系列网络工程师认证&#xff0c;其主要目的就是为了培养了验证网络工程师在华为技术以及解决方案方面的拥有一定的专业知识及技能&#xff0c;该证书分为多个等级&#xff0c;涵盖了不同网络领域及技术&#xff0c;也为众多的网络工程师…

spring security 自定义图形验证码(web/前后端分离)

一、准备工作 1.1 导入pom 所需依赖 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><!-- <version>2.7.18</version>-->&l…