一篇文章带你了解设计模式原理——UML图和软件设计原则

我们在学习过程中可能并不会关心设计模式,但一旦牵扯到项目和面试,设计模式就成了我们的短板

这篇文章并不会讲到二十三种设计模式,但是会讲解设计模式的设计原则以及设计依据和最明显的图形表示

或许我们只有先去了解设计模式的来源才能真正理解设计模式吧

我们该篇会提及到以下内容:

  • 设计模式概述
  • UML图
  • 软件设计原理

设计模式概述

既然我们要讲述设计模式原理,自然就需要先来了解设计模式了

软件设计模式产生背景

其实我们软件的很多概念往往来自于其他专业,设计模式就是其中一个:

  • "设计模式"最初并不是出现在软件设计中,而是被用于建筑领域的设计中。

我们的设计模式就是根据建筑领域中设计模式的概念而产生的:

  • 1977年Christopher Alexander提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。
  • 1990年软件工程界开始研讨设计模式的话题,最终在《设计模式:可复用面向对象软件的基础》一书中收录了23 个设计模式

软件设计模式基本概念

我们以官方角度来讲述设计模式:

  • 软件设计模式(Software Design Pattern),是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

简单来说,设计模式就是前辈们的经验之谈:

  • 它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。
  • 也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。

软件设计模式学习必要性

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

正确使用设计模式具有以下优点:

  • 可以提高程序员的思维能力、编程能力和设计能力
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强

软件设计模式简单分类

我们在未正式学习设计模式之前先去简单了解一下设计模式的主要三种分类:

  • 创建型模式

    用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。

    书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。

  • 结构型模式

    用于描述如何将类或对象按某种布局组成更大的结构

    书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。

  • 行为型模式

    用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。

    书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

UML图

统一建模语言(UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。

UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

类图概述

我们在设计模式中最常用的无非只有类图:

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

类图作用

类图主要具有以下两种作用:

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

类表示法

首先我们先来介绍UML中类的基本表示:

  • 在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性:

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

那么我们就可以给出对应的属性方法的完整表达方式:

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

我们给出一个简单的示例图:

我们可以对上述的类图进行一个简单的解析:

  • 类名:Employee
  • 属性:private String name,private int age,private String address
  • 方法:public void work()

类关系表示法

类关系表示法大致分为关联关系,继承关系,实现关系

关联关系

首先我们先来介绍一下关联关系:

  • 关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。
  • 关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系,组合关系和依赖关系。
单项关联

在UML类图中单向关联用一个带箭头的实线表示。

上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。

双向关联

在UML类图中,双向关联用一个不带箭头的直线表示。

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

自关联

自关联在UML类图中用一个带有箭头且指向自身的线表示。

上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。

聚合关系

在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。

聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。

聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。

组合关系

在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。

组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。

在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。

依赖关系

在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。

依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

继承关系

继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。

在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:

实现关系

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如图 9 所示。

软件设计原则

在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本,同时设计模式也是根据这些原则所产生的。

开闭原则

首先我们给出官方解释:

  • 对扩展开放,对修改关闭
  • 在程序需要进行拓展的时候,不能去修改原有的代码。简言之,是为了使程序的扩展性好,易于维护和升级。

那么从我们的视角来看是怎样的:

  • 利用接口和抽象类来实现上述原则
  • 我们使用抽象类完成众多子类都具有的方法以减少代码冗余,我们使用接口去规定子类的行为但不对其内部细节进行约束

我们来给出一个简单的示例:

我们对上图进行解释并讲解:

/* 案例介绍 */

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

分析:
`搜狗输入法` 的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。
用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。
这些皮肤有共同的特点,可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤是其子类。
用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。 /* 代码展示 */ /**
* @version v1.0
* @ClassName: Client
* @Description: 测试代码
*/
public class Client {
public static void main(String[] args) {
//1,创建搜狗输入法对象
SougouInput input = new SougouInput();
//2,创建皮肤对象
//DefaultSkin skin = new DefaultSkin();
HeimaSkin skin = new HeimaSkin();
//3,将皮肤设置到输入法中
input.setSkin(skin); //4,显示皮肤
input.display();
}
} /**
* @version v1.0
* @ClassName: SougouInput
* @Description: 搜狗输入法
*/
public class SougouInput { private AbstractSkin skin; public void setSkin(AbstractSkin skin) {
this.skin = skin;
} public void display() {
skin.display();
}
} /**
* @version v1.0
* @ClassName: AbstractSkin
* @Description: 抽象皮肤类
*/
public abstract class AbstractSkin { //显示的方法
public abstract void display();
} /**
* @version v1.0
* @ClassName: DefaultSkin
* @Description: 默认皮肤类
*/
public class DefaultSkin extends AbstractSkin { public void display() {
System.out.println("默认皮肤");
}
} /**
* @version v1.0
* @ClassName: HeimaSkin
* @Description: 黑马程序员皮肤
*/
public class HeimaSkin extends AbstractSkin { public void display() {
System.out.println("黑马皮肤");
}
}

里氏代换原则

首先我们给出官方解释:

  • 任何基类可以出现的地方,子类一定可以出现。

从我们的视角来解释:

  • 子类可以扩展父类的功能,但不能改变父类原有的功能
  • 子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法
  • 如果通过重写父类的方法来完成新的功能,整个继承体系的可复用性会比较差

我们给出一个对比案例来展示里氏代换原则:

  1. 正方形不是长方形

我们对问题和上图进行解释:

/* 问题展示 */

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

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。
所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。 /* 代码展示 */ // 但我们可以作用在长方形的方法,如果作用在正方形上却不可以,这就说明正方形不能继承长方形 /**
* @version v1.0
* @ClassName: RectangleDemo
* @Description: 测试方法,这里的resize方法只能作用在长方形,却不能作用于正方形,但正方形继承于长方形导致思维错误
*/
public class RectangleDemo { public static void main(String[] args) {
//创建长方形对象
Rectangle r = new Rectangle();
//设置长和宽
r.setLength(20);
r.setWidth(10);
//调用resize方法进行扩宽
resize(r);
printLengthAndWidth(r); System.out.println("==================");
//创建正方形对象
Square s = new Square();
//设置长和宽
s.setLength(10);
//调用resize方法进行扩宽
resize(s);
printLengthAndWidth(s);
} //扩宽方法
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());
}
} /**
* @version v1.0
* @ClassName: Rectangle
* @Description: 长方形类
*/
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;
}
} package com.itheima.principles.demo2.before; /**
* @version v1.0
* @ClassName: Square
* @Description: 正方形类
*/
public class Square extends Rectangle { @Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
} @Override
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
}
  1. 正方形,长方形都是四边形

我们对问题和上图进行解释:

/* 问题解释 */

抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口

/* 代码展示 */

/**
* @version v1.0
* @ClassName: RectangleDemo
* @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());
}
} /**
* @version v1.0
* @ClassName: Quadrilateral
* @Description: 四边形接口
*/
public interface Quadrilateral { //获取长
double getLength(); //获取宽
double getWidth();
} /**
* @version v1.0
* @ClassName: Rectangle
* @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;
}
} /**
* @version v1.0
* @ClassName: Square
* @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;
}
}

依赖倒转原则

我们首先用官方话语解释:

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

从我们的视角解释就是:

  • 对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

下面看一个例子来理解依赖倒转原则:

  1. 假设我们直接依赖于实现类

我们对问题和上图进行解释:

/* 问题解释 */

【例】组装电脑

现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。

/* 代码展示 */

// 如果我们直接在Computer中配置对应的cpu,硬盘,内存条的实现类也就是特定的型号,那么我们这台电脑就只能使用这些型号

/**
* @version v1.0
* @ClassName: ComputerDemo
* @Description: 测试类
*/
public class ComputerDemo {
public static void main(String[] args) {
//创建组件对象
XiJieHardDisk hardDisk = new XiJieHardDisk();
IntelCpu cpu = new IntelCpu();
KingstonMemory memory = new KingstonMemory(); //创建计算机对象
Computer c = new Computer();
//组装计算机
c.setCpu(cpu);
c.setHardDisk(hardDisk);
c.setMemory(memory); //运行计算机
c.run();
}
} /**
* @version v1.0
* @ClassName: Computer
* @Description: 电脑
*/
public class Computer { // 注意:这里的属性直接使用了实现类,也就是我们无法更换硬件型号
private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory; public XiJieHardDisk getHardDisk() {
return hardDisk;
} public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
} 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 void run() {
System.out.println("运行计算机");
String data = hardDisk.get();
System.out.println("从硬盘上获取的数据是:" + data);
cpu.run();
memory.save();
}
} /**
* @version v1.0
* @ClassName: XiJieHardDisk
* @Description: 希捷硬盘
*/
public class XiJieHardDisk { //存储数据的方法
public void save(String data) {
System.out.println("使用希捷硬盘存储数据为:" + data);
} //获取数据的方法
public String get() {
System.out.println("使用希捷希捷硬盘取数据");
return "数据";
}
} /**
* @version v1.0
* @ClassName: IntelCpu
* @Description: Intel cpu
*/
public class IntelCpu { public void run() {
System.out.println("使用Intel处理器");
}
} /**
* @version v1.0
* @ClassName: KingstonMemory
* @Description: 金士顿内存条类
*/
public class KingstonMemory { public void save() {
System.out.println("使用金士顿内存条");
}
}
  1. 假设我们依赖于接口,装配用实现类

我们对问题和上图进行解释:

/* 问题解释 */

目前我们的电脑属性连接的是接口,而该接口可以去实现多型号的硬件,说明我们的电脑可以采用多种配置方式

/* 代码展示 */

/**
* @version v1.0
* @ClassName: ComputerDemo
* @Description: 测试类,为电脑装配不同的配置
*/
public class ComputerDemo {
public static void main(String[] args) { //创建计算机的组件对象
HardDisk hardDisk = new XiJieHardDisk();
Cpu cpu = new IntelCpu();
Memory memory = new KingstonMemory(); //创建计算机对象
Computer c = new Computer();
//组装计算机
c.setCpu(cpu);
c.setHardDisk(hardDisk);
c.setMemory(memory); //运行计算机
c.run();
}
} /**
* @version v2.0
* @ClassName: Computer
* @Description: 电脑
*/
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("计算机工作");
}
} // 后续就是硬件的接口和实现类,这里就不赘述了

接口隔离原则

首先我们给出官方解释:

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

从我们的角度来解释:

  • 一个类所使用多少功能,在它的父类或者接口上就应该只具有这些功能
  • 将多功能分散在多接口中,只为该类继承它所需要使用的接口

我们同样给出案例:

  1. 接口多功能继承

我们针对上图和问题进行解释:

/* 问题解释 */

【例】安全门案例

我们需要创建一个`黑马`品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。

现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能,很显然如果实现SafetyDoor接口就违背了接口隔离原则

/* 代码展示 */

/**
* @version v1.0
* @ClassName: Client
* @Description: 测试
*/
public class Client {
public static void main(String[] args) {
HeimaSafetyDoor door = new HeimaSafetyDoor();
door.antiTheft();
door.fireProof();
door.waterProof();
}
} /**
* @version v1.0
* @ClassName: SafetyDoor
* @Description: 多功能接口
*/
public interface SafetyDoor { //防盗
void antiTheft(); //防火
void fireProof(); //防水
void waterProof();
} /**
* @version v1.0
* @ClassName: HeimaSafetyDoor
* @Description: 黑马品牌的安全门
*/
public class HeimaSafetyDoor implements SafetyDoor {
public void antiTheft() {
System.out.println("防盗");
} public void fireProof() {
System.out.println("防火");
} public void waterProof() {
System.out.println("防水");
}
}
  1. 接口单功能继承

我们针对上图和问题进行解释:

/* 问题解释 */

现在我们将接口的功能拆分为多接口,我们的类想要使用单个接口就可以直接继承单个接口

/* 代码展示 */

/**
* @version v1.0
* @ClassName: Client
* @Description: TODO(一句话描述该类的功能)
*/
public class Client {
public static void main(String[] args) {
//创建黑马安全门对象
HeimaSafetyDoor door = new HeimaSafetyDoor();
//调用功能
door.antiTheft();
door.fireProof();
door.waterProof(); System.out.println("============");
//创建传智安全门对象
ItcastSafetyDoor door1 = new ItcastSafetyDoor();
//调用功能
door1.antiTheft();
door1.fireproof();
}
} /**
* @version v1.0
* @ClassName: HeiMaSafetyDoor
* @Description: TODO(一句话描述该类的功能)
*/
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
public void antiTheft() {
System.out.println("防盗");
} public void fireproof() {
System.out.println("防火");
} public void waterproof() {
System.out.println("防水");
}
} /**
* @version v1.0
* @ClassName: ItcastSafetyDoor
* @Description: 传智安全门
*/
public class ItcastSafetyDoor implements AntiTheft,Fireproof {
public void antiTheft() {
System.out.println("防盗");
} public void fireproof() {
System.out.println("防火");
}
} public interface AntiTheft {
void antiTheft();
} public interface Fireproof {
void fireproof();
} public interface Waterproof {
void waterproof();
}

迪米特法则

首先我们给出官方解释:

  • 如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。
  • 其目的是降低类之间的耦合度,提高模块的相对独立性。

从我们的视角来解释:

  • 只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)
  • 迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等
  • 这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

我们给出一个简单的案例进行解释:

我们对上述问题和图进行解释:

/* 问题展示 */

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

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

/* 代码展示 */

/**
* @version v1.0
* @ClassName: Client
* @Description: 测试类
*/
public class Client {
public static void main(String[] args) {
//创建经纪人类
Agent agent = new Agent();
//创建明星对象
Star star = new Star("林青霞");
agent.setStar(star);
//创建粉丝对象
Fans fans = new Fans("李四");
agent.setFans(fans);
//创建媒体公司对象
Company company = new Company("黑马媒体公司");
agent.setCompany(company); agent.meeting();//和粉丝见面
agent.business();//和媒体公司洽谈业务
}
} /**
* @version v1.0
* @ClassName: Agent
* @Description: 经纪人类
*/
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(star.getName() + "和粉丝" + fans.getName() + "见面");
} //和媒体公司洽谈的方法
public void business() {
System.out.println(star.getName() + "和" + company.getName() + "洽谈");
}
} /**
* @version v1.0
* @ClassName: Company
* @Description: 媒体公司类
*/
public class Company {
private String name; public String getName() {
return name;
} public Company(String name) {
this.name = name;
}
} /**
* @version v1.0
* @ClassName: Fans
* @Description: 粉丝类
*/
public class Fans { private String name; public String getName() {
return name;
} public Fans(String name) {
this.name = name;
}
} /**
* @version v1.0
* @ClassName: Star
* @Description: 明星类
*/
public class Star {
private String name; public Star(String name) {
this.name = name;
} public String getName() {
return name;
}
}

合成复用原则

我们给出官方解释:

  • 尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

通常类的复用分为继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。

  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。

  • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

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

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。

  • 对象间的耦合度低。可以在类的成员位置声明抽象。

  • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

最后我们给出两张图来介绍为什么组合优于继承:

  1. 继承图

  1. 组合图

我们进行简单的解释:

/* 问题解释 */

【例】汽车分类管理程序

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

/* 图形介绍 */

继承图:我们会发现我们每多一种属性,就需要创建多个子类,类的创建是非常耗费资源的,上述多类的创建也是资源耗费的一种

组合图:我们将部分属性以属性的形式介入,并采用接口存放,后续我们只需要更新其接口就可以更新多种实体类,节省资源的使用

结束语

关于设计模式原理我们就介绍到这里,后面我会继续更新二十三种设计模式,希望能给你带来帮助~

附录

该文章属于学习内容,具体参考B站黑马程序员的Java设计模式详解

这里附上视频链接:黑马程序员Java设计模式详解, 23种Java设计模式(图解+框架源码分析+实战)_哔哩哔哩_bilibili

一篇文章带你了解设计模式原理——UML图和软件设计原则的更多相关文章

  1. 一篇文章带你吃透 Docker 原理

    容器的实现原理 从本质上,容器其实就是一种沙盒技术.就好像把应用隔离在一个盒子内,使其运行.因为有了盒子边界的存在,应用于应用之间不会相互干扰.并且像集装箱一样,拿来就走,随处运行.其实这就是 Paa ...

  2. MYSQL(进阶篇)——一篇文章带你深入掌握MYSQL

    MYSQL(进阶篇)--一篇文章带你深入掌握MYSQL 我们在上篇文章中已经学习了MYSQL的基本语法和概念 在这篇文章中我们将讲解底层结构和一些新的语法帮助你更好的运用MYSQL 温馨提醒:该文章大 ...

  3. 一篇文章带你掌握主流服务层框架——SpringMVC

    一篇文章带你掌握主流服务层框架--SpringMVC 在之前的文章中我们已经学习了Spring的基本内容,SpringMVC隶属于Spring的一部分内容 但由于SpringMVC完全针对于服务层使用 ...

  4. MYSQL(基本篇)——一篇文章带你走进MYSQL的奇妙世界

    MYSQL(基本篇)--一篇文章带你走进MYSQL的奇妙世界 MYSQL算是我们程序员必不可少的一份求职工具了 无论在什么岗位,我们都可以看到应聘要求上所书写的"精通MYSQL等数据库及优化 ...

  5. 一篇文章带你掌握主流数据库框架——MyBatis

    一篇文章带你掌握主流数据库框架--MyBatis MyBatis 是一款优秀的持久层框架,它支持自定义 SQL.存储过程以及高级映射. 在之前的文章中我们学习了MYSQL和JDBC,但是这些东西远远不 ...

  6. 一篇文章带你掌握主流基础框架——Spring

    一篇文章带你掌握主流基础框架--Spring 这篇文章中我们将会介绍Spring的框架以及本体内容,包括核心容器,注解开发,AOP以及事务等内容 那么简单说明一下Spring的必要性: Spring技 ...

  7. 一篇文章带你掌握主流办公框架——SpringBoot

    一篇文章带你掌握主流办公框架--SpringBoot 在之前的文章中我们已经学习了SSM的全部内容以及相关整合 SSM是Spring的产品,主要用来简化开发,但我们现在所介绍的这款框架--Spring ...

  8. 一篇文章带你掌握MyBatis简化框架——MyBatisPlus

    一篇文章带你掌握MyBatis简化框架--MyBatisPlus 我们在前面的文章中已经学习了目前开发所需的主流框架 类似于我们所学习的SpringBoot框架用于简化Spring开发,我们的国人大大 ...

  9. 一篇文章带你了解网页框架——Vue简单入门

    一篇文章带你了解网页框架--Vue简单入门 这篇文章将会介绍我们前端入门级别的框架--Vue的简单使用 如果你以后想从事后端程序员,又想要稍微了解前端框架知识,那么这篇文章或许可以给你带来帮助 温馨提 ...

  10. 一篇文章带你了解热门版本控制系统——Git

    一篇文章带你了解热门版本控制系统--Git 这篇文章会介绍到关于版本控制的相关知识以及版本控制神器Git 我们可能在生活中经常会使用GitHub网页去查询一些开源的资源或者项目,GitHub就是基于G ...

随机推荐

  1. P7114 [NOIP2020] 字符串匹配 (字符串hash+树状数组)

    好多题解用的扩展KMP(没学过,所以不用这种方法). 我们按照题目要求记F(s)表示s串的权值,可以预处理出前缀权值(用于A)和后缀权值(用于C),枚举AB的长度i=2~n-1,不需要分开枚举,我们只 ...

  2. C#中Math.Round()实现中国式四舍五入问题

    C#中的Math.Round()并不是使用的"四舍五入"法.实际上是四舍六入五取偶. 对于这个问题我们常见的解决方式是使用MidpointRounding.AwayFromZero ...

  3. Redis 常见问题-缓存穿透

    问题描述: * 针对 DB 中不存在的数据源,每次请求缓存和数据库都不存在 造成后果: * 应用服务器压力变大 * Redis 命中率大幅度降低 * `数据库压力巨增甚至 down 掉`* 该现象对于 ...

  4. 2.签名&初始化&提交

      Git设置签名 签名的作用是区分不同操作者的身份,用户的签名信息在每一个版本的提交信息中能够看到, 以此确认本次提交是谁做的,git首次安装必须设置用户签名,否则无法提交代码 这里设置的用户签名和 ...

  5. 前端JS获取路由地址里的参数QueryString取值

    参数的获取 声明一个函数 //参数name是路由参数 engNo function getQueryString(name) { var reg = new RegExp("(^|& ...

  6. Java安全之Tomcat6 Filter内存马

    Java安全之Tomcat6 Filter内存马 回顾Tomcat8打法 先回顾下之前Tomcat789的打法 这里先抛开 7 8之间的区别, 在8中,最后add到filterchain的都是一个fi ...

  7. 词云(WordCloud)

    WordCloud的参数: font_path:可用于指定字体路径 width:词云的宽度,默认为 400: height:词云的⾼度,默认为 200: mask:蒙版,可⽤于定制词云的形状: min ...

  8. PyCharm配置远程Docker环境

    1. docker 配置 使用-p参数暴露一个端口用于ssh连接. docker run -itd --name wangchao_paddle --gpus all -p 8899:8888 -p ...

  9. 2022,一个Java程序猿的外设配置

    工欲善其事,必先利其器. 是的没错,我就是个器材党,哈哈.正赶上搬家布置了新桌面,经过我的精心挑选和安装,也是凑齐了我新一套的桌面外设.写下来记录一下. 键盘 套件:腹灵MK870 轴体:佳达隆G白P ...

  10. 【Azure 环境】Azure 云环境对于OpenSSL 3.x 的严重漏洞(CVE-2022-3602 和 CVE-2022-3786)的处理公告

    问题描述 引用报告:(OpenSSL3.x曝出严重漏洞 : https://www.ctocio.com/ccnews/37529.html ) 最近OpenSSL 3.x 爆出了严重安全漏洞,分别是 ...