1 什么是设计模式
1.1 理解
- 设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是
语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关
系和组合关系的充分理解。 - 当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择:
- 对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易;
- 但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。
1.2 重点
- 单例模式
2 类的设计原则
2.1 单一职责原则
一个类应该有且只有一个变化的原因。单一职责原则将不同的职责分离到单独的类,每一个职责都是一
个变化的中心。需求变化时,将通过更改职责相关的类来体现。如果一个类拥有多于一个的职责,则多
个职责耦合在一起,会有多于一个原因来导致这个类发生变化。一个职责的变化可能会影响到其他的职
责,另外,把多个职责耦合在一起,影响复用性。
2.2 里氏代换原则
这就是要求继承是严格的is-a关系。所有引用基类的地方必须能透明地使用其子类的对象。在软件中将一
个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实
体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢
狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也
是动物。
2.3 依赖倒置原则
依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。低层模块
尽量都要有抽象类或接口,或者两者都有。变量的声明类型尽量是抽象类或接口。根据标准写实现
2.4 接口隔离原则
客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上,通俗的讲就是需
要什么就提供什么,不需要的就不要提供。
2.5 迪米特法则
迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),一个类
对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌
生人说话。
也就是说,一个类应该对自己需要耦合或调用的类知道的最少,类与类之间的关系越密切,耦合度越
大,那么类的变化对其耦合的类的影响也会越大,这也是我们面向设计的核心原则:低耦合,高内聚。
2.6 开闭原则
对修改关闭,对扩展开放。在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进
行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码
经过重新测试。解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过
修改已有的代码来实现。不过这要求,我们要对需求的变更有前瞻性和预见性。其实只要遵循前面5中设
计模式,设计出来的软件就是符合开闭原则的。
3 类和类之间的关系
- 在设计模式中类与类之间的关系主要有6种:依赖、关联、聚合、组合、继承,实现, 它们之间的耦合
度依次增加。
3.1 继承关系
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加
它自己的新功能的能力。在Java中继承关系通过关键字extends明确标识,在设计时一般没有争议性。在
UML类图设计中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。
is a
3.2 实现关系
实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。
在Java中此类关系通过关键字implements明确标识,在设计时一般没有争议性。在UML类图设计中,实
现用一条带空心三角箭头的虚线表示,从类指向实现的接口。
3.3 依赖关系
简单的理解,依赖就是一个类A使用到了2,而这种使用关系是具有偶然性的、临时性的、非常弱的,但
是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现
在代码层面,为类B作为参数被类A在某个method方法中使用。在UML类图设计中,依赖关系用由类A
指向类B的带箭头虚线表示。
3.4 关联关系
关联体现的是两个类之间语义级别的一种强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存
在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可
以是单向、双向的。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类
A引用了一个类型为被关联类B的全局变量。在UML类图设计中,关联关系用由关联类A指向被关联类B
的带箭头实线表示,在关联的两端可以标注关联双方的角色和多重性标记。
3.5 聚合关系
聚合是关联关系的一种特例,它体现的是整体与部分的关系,即has-a的关系。此时整体与部分之间是可
分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比
如计算机与CPU、公司与员工的关系等,比如一个航母编队包括海空母舰、驱护舰艇、舰载飞机及核动
力攻击潜艇等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,
聚合关系以空心菱形加实线箭头表示。
3.6 组合关系
组合也是关联关系的一种特例,它体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚
合。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着
部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来
区分。在UML类图设计中,组合关系以实心菱形加实线箭头表示。
4 模式分类
根据不同设计模式的特点和作用,我们可以对所有的设计模式进行大体的分类。
4.1 创建型
- 创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。这样可以降
低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。就像我们去商
场购买商品时,不需要知道商品是怎么生产出来一样,因为它们由专门的厂商生产。 - 创建型模式分为以下几种。
- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实
例,其拓展是有限多例模式。 - 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新
实例。 - 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
- 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关
的产品。 - 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创
建它们,最后构建成该复杂对象。
- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实
4.2 结构型
- 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,
前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 - 结构型模式分为以下 7 种:
- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访
问该对象,从而限制、增强或修改该对象的一些特性。 - 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不
兼容而不能一起工作的那些类能一起工作。 - 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来
实现的,从而降低了抽象和实现这两个可变维度的耦合度。 - 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
- 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
- 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的
访问性。
- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访
4.3 行为型模式
- 行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单
个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚
合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象
行为模式比类行为模式具有更大的灵活性。 - 行为型模式是 GoF 设计模式中最为庞大的一类,它包含以下 11 种模式。
- 模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类
中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤。 - 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算
法的改变不会影响使用算法的客户。 - 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割
开。职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被 - 响应为止。通过这种方式去除对象之间的耦合。
- 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
- 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知
给其他多个对象,从而影响其他对象的行为。 - 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间
的耦合度,使原有对象之间不必相互了解。 - 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象
的内部表示。 - 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方
式,即每个元素有多个访问者对象访问。 - 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以
后恢复它。 - 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
- 模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类
5 设计模式详解
5.1 单例模式
- 在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
5.1.1 定义
- 指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
5.1.2 特点
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
5.1.3 优缺点
- 单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
- 单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背
开闭原则。 - 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能
模拟生成一个新的对象。 - 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背
5.1.4 使用场景
对于 Java 来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几
个方面。
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
- 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
频繁访问数据库或文件的对象。 - 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,
则系统会完全乱套。 - 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快
对象访问速度。如 Web 中的配置对象、数据库的连接池等。
5.1.5 分类
- 懒汉式 : 在调用功能时才创建对象 ->线程不安全|不同步的
- 饿汉式 : 在类加载完成之后就创建对象->线程安全的|同步的
5.1.6 实现步骤
- 构造器私有化
- 私有的静态的该类的引用
- 公共的静态的访问方式
5.1.7 饿汉式
public class Class001_Single {
//2.私有的静态的该类的引用
private static Class001_Single single = new Class001_Single();
//1.构造器私有化
private Class001_Single() {
}
//3.公共的静态的访问方式
public static Class001_Single newInstance(){
return single;
}
}
5.1.8 懒汉式
- 线程不安全的|不同步的
- 控制线程安全: 方法在添加synchronized关键字进行修改
- synchronized : 被修改的内容多个线程之间排队执行
public class Class002_SingleTon {
//2.私有的静态的该类的引用
private static Class002_SingleTon single = null;
//1.构造器私有化
private Class002_SingleTon(){
}
//A B C
//3.公共的静态的访问方式
public static synchronized Class002_SingleTon newInstance(){
if(single==null){
single = new Class002_SingleTon();
}
return single;
}
}
5.1.9 测试
public class Test {
public static void main(String[] args) {
//测试饿汉式
System.out.println(Class001_Single.newInstance());
System.out.println(Class001_Single.newInstance());
System.out.println(Class001_Single.newInstance());
//测试懒汉式
System.out.println(Class002_SingleTon.newInstance());
System.out.println(Class002_SingleTon.newInstance());
System.out.println(Class002_SingleTon.newInstance());
System.out.println(Class002_SingleTon.newInstance());
}
}
5.2 静态代理
5.2.1 分类
- 静态代理
- 动态代理
5.2.2 静态代理
- 代理角色与真实角色实现相同的接口
- 代理角色持有真实角色的引用
- 代理行为:代理角色真实帮助你做的事情
5.2.3 实现
public class Class003_StaticProxy {
public static void main(String[] args) {
//项目经理
Manager manager = new Manager();
//HR
HR hr = new HR(manager);
//代理行为
hr.addUser();
}
}
//录用人
interface AddUser{
void addUser();
}
//真实角色 : 项目经理
class Manager implements AddUser{
@Override
public void addUser() {
System.out.println("面试..录用了");
}
}
//代理角色 : HR
class HR implements AddUser{
//真实角色的引用
Manager manager = null;
public HR() {
}
public HR(Manager manager) {
this.manager = manager;
}
@Override
public void addUser() {
System.out.println("发布招聘信息");
System.out.println("筛选简历");
System.out.println("预约面试");
//联系项目经理面试
manager.addUser();
System.out.println("谈薪资。。");
}
}
5.3 简单工厂模式
5.3.1 定义
- 定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建
型模式中所要求的“创建与使用相分离”的特点。简单来说,简单工厂模式有一个具体的工厂类,可以生成多个不同的产品,属于创建型设计模式。简单工厂模式不在 GoF 23 种设计模式之列。
5.3.2 优缺点
- 优点
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创
建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。 - 客户端无需知道所创建具体产品的类名,只需知道参数即可。
- 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创
- 缺点
- 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且
工厂类代码会非常臃肿,违背高聚合原则。 - 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
- 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
- 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。
- 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且
5.3.3 结构
- 抽象产品角色 : 具体产品角色实现的接口|继承的父类
- 具体产品角色 : 实现类|子类,是简单工厂模式的创建目标。
- 工厂角色:是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂
类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
5.3.4 注意
- 字符串比较是否相等需要使用equals方法进行比较字符串的内容,建议常量放在.的前面,避免空指针异常的出现
5.3.4 实现
public class Class004_Factory {
public static void main(String[] args) {
//多态
Car car = factory("Bmw");
car.run();
}
//工厂角色
public static Car factory(String type){
if("Benz".equals(type)){
return new Benz();
}else if("Bmw".equals(type)){
return new Bmw();
}
return null;
}
}
//抽象产品角色
interface Car{
void run();
}
//具体产品角色
class Benz implements Car{
@Override
public void run() {
System.out.println("坐在引擎盖笑...");
}
}
//具体产品角色
class Bmw implements Car{
@Override
public void run() {
System.out.println("坐在副驾驶哭...");
}
}
5.3.5 Pizza实例
编写程序实现比萨制作。需求说明编写程序,接收用户输入的信息,选择需要制作的比萨。可供选择的比萨有:培根比萨和海鲜比萨。
实现思路及关键代码
1) 分析培根比萨和海鲜比萨
2) 定义比萨类
3) 属性:名称、价格、大小
4) 方法:展示
5) 定义培根比萨和海鲜比萨继承自比萨类
6) 定义比萨工厂类,根据输入信息产生具体的比萨对象
public class Class005_PizzaTest {
public static void main(String[] args) {
Pizza pizza = factory();
pizza.show();
}
//工厂
public static Pizza factory(){
Scanner sc = new Scanner(System.in);
System.out.println("请选择想要制作的Pizza \n1)培根披萨\n2)海鲜披萨");
int num = sc.nextInt();
System.out.println("请输入披萨大小");
int size = sc.nextInt();
System.out.println("请输入披萨价格");
double price = sc.nextDouble();
Pizza pizza = null;
if(num==1){
}else if(num==2){
System.out.println("请输入海鲜披萨的配料信息");
String info = sc.next();
pizza = new SeaPizza("海鲜披萨",price,size,info);
}
return pizza;
}
}
//父类->披萨
abstract class Pizza{
//属性:名称、价格、大小
private String name;
private double price;
private int size;
public Pizza() {
}
public Pizza(String name, double price, int size) {
this.name = name;
this.price = price;
this.size = size;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
//方法:展示
public void show(){
System.out.println(name);
System.out.println(price);
System.out.println(size);
}
}
//子类->海鲜披萨
class SeaPizza extends Pizza{
//配料信息
private String info;
public SeaPizza() {
}
public SeaPizza(String name, double price, int size, String info) {
super(name, price, size);
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public void show() {
super.show();
System.out.println(info);
}
}
//子类->培根披萨