第十八节:设计模式
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()方法。
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
● +:表示public
● -:表示private
● #:表示protected
属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
注意:
1,中括号中的内容表示是可选的
2,也有将类型放在变量名前面,返回值类型放在方法名前面
例如:
2.3.2 类与类之间关系的表示方式
2.3.2.1 关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。
关联又可以分为单向关联,双向关联,自关联。
1,单向关联
在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。
2,双向关联
从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。
在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。
3,自关联
自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。
2.3.2.2 聚合关系
聚合关系表示的是整体和部分的关系,整体与部分可以分开。聚合关系是关联关系的特例,是强关联关系,是整体和部分之间的关系。聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。
举例1:学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:
举例2:一台电脑由键盘(keyboard)、显示器(monitor),鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来的,使用带空心菱形的实线来表示:
2.3.2.3 组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。
举例1:头和嘴的关系,没有了头,嘴也就不存在了。在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
举例2:如果我们认为 Mouse、Monitor 和 Computer 是不可分离的,则升级为组合关系
2.3.2.4 依赖关系
依赖关系通常指的是类之间的调用关系,即一个类通过局部变量、方法参数或静态方法调用另一个类,只要是在类中用到了对方,那么他们之间就存在依赖关系。如果没有对方,连编译都通过不了。依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
举例1:
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:
举例2:
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 类的子类,其类图如下图所示:
小结
● 1)继承关系实也被称为泛化关系
● 2)如果 A 类继承了 B 类,我们就说 A 和 B 存在泛化关系
2.3.2.6 实现关系
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。它是依赖关系的特例。
举例1:
在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如图所示。
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即包含了用户相关信息,还单独包含了地址相关的信息
未使用单一原则前:
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
public class JSPDemo01 {
}
interface PhoneFunction{
void call();
void message();
void camera();
}
class ApplePhone implements PhoneFunction{
@Override
public void call() {
System.out.println("苹果手机实现打电话");
}
@Override
public void message() {
System.out.println("苹果手机实现发信息");
}
@Override
public void camera() {
System.out.println("苹果手机实现照相");
}
}
需求2:根据市场需求,需要开发一款老年机,只具有电话、短信功能。
class OldPhone implements PhoneFunction{
@Override
public void call() {
System.out.println("老年机实现打电话");
}
@Override
public void message() {
System.out.println("老年机实现发信息");
}
@Override
public void camera() {
System.out.println("老年机被迫实现照相");
}
}
接口隔离原则
public class JSPDemo02 {
}
interface CallFunction{
void call();
}
interface MessageFunction{
void message();
}
interface CameraFunction{
void camera();
}
class ApplePhone1 implements CallFunction,MessageFunction,CameraFunction{
@Override
public void call() {
System.out.println("苹果手机实现打电话");
}
@Override
public void message() {
System.out.println("苹果手机实现发信息");
}
@Override
public void camera() {
System.out.println("苹果手机实现照相");
}
}
class OldPhone1 implements CallFunction,MessageFunction{
@Override
public void call() {
System.out.println("老年机实现打电话");
}
@Override
public void message() {
System.out.println("老年机实现发信息");
}
}
3.2.3依赖倒转原则(Dependence Inversion Principle)
依赖倒转原则(DIP): Dependency Inversion Principle,模块之间要依赖抽象,不依赖实现,要面向接口编程,不要面向实现编程。高层模块不应该直接依赖低层模块(底层模块则是指那些不可分割的、基本的逻辑单元),这样就降低了客户端与实现模块间的耦合.
基本介绍
● 1)高层模块不应该依赖低层模块,二者都应该依赖其抽象
● 2)抽象不应该依赖细节,细节应该依赖抽象
● 3)依赖倒转(倒置)的中心思想是面向接口编程
● 4)依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
● 5)使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
应用实例:
开发一套电脑组装系统Computer,给电脑配备IntelCup及IntelMemory.
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
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:
一个程序引出的问题和思考
先看个程序,思考下问题和解决思路
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 {
// 鸭子会跑倒是不会飞,所以飞的速度设为0
public 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)编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则
看一段代码
定义了一个类用来产生随机数,进行加法操作
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;
@Override
public 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;
@Override
public 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;
@Override
public 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{
@Override
public 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:明星与经纪人的关系实例
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
基本介绍
● 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和总部的员工ID
schoolManager.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和总部的员工ID
schoolManager.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) {
// 输出学院的员工方法 封装到CollegeManager02
collegeManager.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.对象间的耦合度低。可以在类的成员位置声明(抽象类或者接口)。
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)为了交互对象之间的松耦合设计而努力