版本 |
说明 |
发布日期 |
1.0 |
发布文章第一版 |
2021-02-12 |
文章目录
- 前言
-
- 创建型模式
-
- 单例模式
-
- 工厂方法模式
-
- 普通工厂方法模式
- 多个工厂方法模式
- 静态工厂方法模式
- 抽象工厂方法模式
- 结构型模式
-
- 行为型模式
-
前言
- 这篇文章是我个人的学习笔记,可能无法做到面面俱到,也可能会有各种纰漏。如果任何疑惑的地方,欢迎一起讨论~
- 本篇文章仅仅是让小伙伴们对设计模式有一个概览上的理解,所以篇幅较小,无深入内容。
- 如果想完整阅读这个系列的文章,欢迎关注我的专栏《设计模式》~
- 哦对了!请不要吝啬->点赞、关注、收藏~
什么是设计模式
- 以我个人的理解,设计模式就是一种业界公认的,在某种场景下的,高效的、满足设计规范的代码编写套路。
设计模式分类
- 设计模式分为三个大类,本文章将从三个大类中抽取一两个典型的设计模式,进行简要介绍。
- 创建型模式:专注于优化对象创建方式的设计模式,通常会与多态进行配合实现。例如本文下面要讲的单例模式、工厂方法模式、抽象工厂模式。
- 结构型模式:用于优化类的上下级结构的设计模式。例如本文下面要讲的装饰器模式、代理模式。
- 行为型模式:用于优化类、方法之间的交互方式和功能职责。例如本文下面要讲的模版方法模式。
创建型模式
单例模式
- 单例模式的特点是整个系统中,该类的对象最多只会有一个。咱日常应用中有一个很好的例子——Windows系统的任务管理器。无论你怎么点,任务管理器始终都只会有一个。
- 单例模式适合用在对系统整体进行统一管理的类,有一种“静态”类的感觉。
单例模式的实现方法
- 单例模式分为两种实现方式:饿汉式和懒汉式。
- 饿汉式:在JVM加载类的时候,就生成该类的对象。该方式不会产生线程同步问题,但是会造成一定程度的资源浪费。
- 懒汉式:在第一次调用该类的时候,才生成该类的对象。该方式需要处理线程同步问题,但是不会造成资源浪费。
饿汉式
- 话不多说,直接上代码。比如我们模仿一个“任务管理器”。
- 好吧,多说一句,饿汉式实现方式就3个步骤,我将在写在下面的代码备注中,以便更好理解。
public class HungryMan {
private final HashMap<String, String> tasks;
private int taskId;
private static final HungryMan hungryMan = new HungryMan();
private HungryMan() {
System.out.println("饿汉来啦!");
tasks = new HashMap<>();
taskId = 0;
}
public static HungryMan getInstance() {
return hungryMan;
}
public String addTask(String taskName) {
String id = Integer.toString(taskId++);
tasks.put(id, taskName);
System.out.println("添加了任务:" + taskName);
return id;
}
public String getTaskName(String taskId) {
return tasks.get(taskId);
}
}
public class Starter {
public static void main(String[] args) {
testHungry();
}
private static void testHungry(){
System.out.println("我开始运行饿汉式单例模式啦!");
HungryMan hungryMan1 = HungryMan.getInstance();
HungryMan hungryMan2 = HungryMan.getInstance();
String id = hungryMan1.addTask("4399小游戏");
System.out.println(hungryMan2.getTaskName(id));
}
}
- 运行结果如下。可以看到,hungryMan1和hungryMan2两个变量指向的是同一个对象。
我开始运行饿汉式单例模式啦!
饿汉来啦!
添加了任务:4399小游戏
4399小游戏
- 不过我目前有一点比较疑惑,理论上在还没有调用HungryMan的时候,JVM就应该已经初始化了所有静态资源才对。但事实上,从方法调用栈可以看出,即使是饿汉式,其对象也是在调用该类时才会创建。不知道这个是不是JVM对静态资源的一种自动优化,希望有明白的大佬可以解读一下。
懒汉式
- 对比着饿汉式来,还是用同样的例子。实现步骤依然放在代码注释中讲解。
public class LazyMan {
private final HashMap<String, String> tasks;
private int taskId;
private static LazyMan lazyMan;
private LazyMan() {
System.out.println("懒汉来啦!");
tasks = new HashMap<>();
taskId = 0;
}
public static LazyMan getInstance() {
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public String addTask(String taskName) {
String id = Integer.toString(taskId++);
tasks.put(id, taskName);
System.out.println("添加了任务:" + taskName);
return id;
}
public String getTaskName(String taskId) {
return tasks.get(taskId);
}
}
public class Starter {
public static void main(String[] args) {
testLazy();
}
private static void testHungry(){
System.out.println("我开始运行饿汉式单例模式啦!");
HungryMan hungryMan1 = HungryMan.getInstance();
HungryMan hungryMan2 = HungryMan.getInstance();
String id = hungryMan1.addTask("4399小游戏");
System.out.println(hungryMan2.getTaskName(id));
}
private static void testLazy(){
System.out.println("我开始运行懒汉式单例模式啦!");
LazyMan lazyMan1 = LazyMan.getInstance();
LazyMan lazyMan2 = LazyMan.getInstance();
String id = lazyMan1.addTask("7k7k小游戏");
System.out.println(lazyMan2.getTaskName(id));
}
}
- 运行结果如下。饿汉式和懒汉式的区别很细微,他们的实现步骤几乎一样,只是具体的实现逻辑有些许区别。懒汉式就是实实在在的,在初次调用getInstance方法的时候才会创建对象。
我开始运行懒汉式单例模式啦!
懒汉来啦!
添加了任务:7k7k小游戏
7k7k小游戏
解决线程同步问题
- 上面也说到了,懒汉式会存在线程同步的问题。为什么呢?听我娓娓道来~
- 问题就出在getInstance方法上。我们知道创建对象是需要一段时间的,而对象在被创建完成之前,lazyMan这个成员变量始终会等于null。
- 这个时候,来了两个线程(他们叫小线和大线),同时调用了getInstance方法。小线跑得快一点,于是先通过了if判断,于是开始对lazyMan进行实例化。
- 但是在小线实例化完成之前,大线也来进行if判断了,此时大线理所当然能够通过判断,于是也开始了对lazyMan的实例化。
- 此时,两个线程调用getInstance方法,就有可能会获取到两个不同的对象(当然运气好的话也可能是一个,总之这是一个不正确的状态)。
- 正是因为getInstance方法访问了临界区资源,所以我们应当对getInstance方法加锁,并且是整个系统使用同一把锁。
- 此时聪明的小伙伴唰唰唰就吧改好的方法贴出来了:
public synchronized static LazyMan getInstance() {
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
- 但是这样真的好么?当然,这样做确实解决了线程同步问题,但是却引入了一个新问题——同时只能有一个线程能够获取这个类的对象,导致效率较低。小学二年级的同学都知道,加锁的粒度要尽可能地小。
- 所以我们需要把方法锁改为代码块锁,而且粒度要尽可能小。这个地方就稍微有一点点难度了。
- 我们知道,其实临界区资源就是new对象那一行,那我们要防止线程同步问题,就应该同一时间只让一个线程创建对象。所以我决定把锁包在if外面。
- 但是仅仅用一个包住if判断的锁就解决问题了么?当然没有,小小分析一下就能发现,同一时间还是只能有一个线程获取对象。于是我们再加一个if判断,问题就解决了。最终代码如下。
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
工厂方法模式
- 工厂方法模式用于强化对象创建。其主要优势在于可以在对象创建的前后封装一些额外的操作;并且当系统中存在大量该类的对象创建代码时,可以提高系统的可维护性。
- 工厂方法模式也有几个分支——普通工厂方法模式、多个工厂方法模式、静态工厂方法模式、抽象工厂方法模式。
普通工厂方法模式
- 普通工厂方法模式有一点点反射机制内味儿,但是它可以比反射机制创建对象更灵活。
- 优点:
- 可以实现动态编程,即创建的对象可以通过字符串传递。
- 可以在创建对象前后额外编写操作。
- 缺点:
- 可能存在字符串传递错误的情况,所以需要有对应的异常处理。
- 每次使用需要额外创建一个工厂对象。
- 若需求发生变更,需要调整工厂类代码,因此不满足开闭原则。
- 其实我感觉普通工厂方法模式就很实用,因为动态编程有时候真的很爽~
- 来个简单的例子:
public class Person {
public Person() {
System.out.println("a person created");
}
}
public class Man extends Person {
public Man() {
System.out.println("a man created");
}
}
public class Woman extends Person {
public Woman() {
System.out.println("a woman created");
}
}
public class PersonFactory {
public Person create(String type) throws Exception {
System.out.println("ready to crate " + type);
switch (type) {
case "Man":
return new Man();
case "Woman":
return new Woman();
default:
throw new Exception("创建失败:该类不存在");
}
}
}
public class Starter {
public static void main(String[] args) throws Exception {
testNormal();
}
private static void testNormal() throws Exception {
PersonFactory factory = new PersonFactory();
Person man = factory.create("Man");
Person incorrect = factory.create("autoMan");
}
}
- 执行结果如下。可以看到,普通工厂方法模式可以实现动态编程,当我们把创建对象的字符串以变量的方式传递,则可以通过变量的值,来灵活创建对象。此外,还可以看到,普通工厂方法模式的对象创建是可能出错的。
ready to crate Man
a person created
a man created
ready to crate autoMan
Exception in thread "main" java.lang.Exception: 创建失败:该类不存在
at com.DesignPattern.Creational.Factory.NormalFactory.PersonFactory.create(PersonFactory.java:17)
at com.DesignPattern.Creational.Factory.Starter.testNormal(Starter.java:14)
at com.DesignPattern.Creational.Factory.Starter.main(Starter.java:8)
多个工厂方法模式
- 对普通工厂方法模式进行改造,对每一种类都单独提供一个实例化方法。改造完就没有反射机制内味儿了。
- 优点:
- 不会存在字符串传递错误的情况。
- 可以在创建对象前后额外编写操作。
- 缺点:
- 失去了动态编程的优点。
- 每次使用需要额外创建一个工厂对象。
- 若需求发生变更,需要调整工厂类代码,因此不满足开闭原则。
- 这个模式的例子就和静态工厂方法模式放在一起吧。
静态工厂方法模式
- 静态工厂方法模式可以套用在普通工厂方法模式和多个工厂方法模式上面。通过将创建对象的方法静态化,从而解决了每次使用需要额外创建一个工厂对象的缺点。
- 来个栗子。被生产的类还是一样(Person家族),所以就省略了,区别主要在于工厂类。
public class MultiPersonFactory {
public static Person createMan() {
System.out.println("ready to crate Man");
return new Man();
}
public static Person createWoman() {
System.out.println("ready to crate Woman");
return new Woman();
}
}
public class Starter {
public static void main(String[] args) throws Exception {
testMulti();
}
private static void testNormal() throws Exception {
PersonFactory factory = new PersonFactory();
Person man = factory.create("Man");
Person incorrect = factory.create("autoMan");
}
private static void testMulti() {
Person man = MultiPersonFactory.createMan();
}
}
- 执行结果如下。静态工厂方法可以省去每一次都去实例化工厂对象的过程;多个工厂方法模式虽然失去了动态编程能力,但是避免了对象创建错误的情况。
ready to crate Man
a person created
a man created
抽象工厂方法模式
- 该模式是对多个工厂方法模式的改良,通过将工厂类进行抽象和继承,来让其满足开闭原则。
- 直接举个栗子就明白了。这里我就不放启动类了,直接给你们看工厂类吧。
public class AbstractManFactory extends Person {
public Person create(){
System.out.println("ready to create Man");
return new Man();
}
}
public abstract class AbstractPersonFactory {
public abstract Person create();
}
public class AbstractWomanFactory extends Person {
public Person create(){
System.out.println("ready to create Woman");
return new Woman();
}
}
- 一目了然,当需要生产的类有多少种,工厂类就会对应添加多少种子工厂,从而避免了需求变更时,需要对原有代码进行修改。这就满足了“对扩展开放,对修改关闭”原则,也就是开闭原则。
结构型模式
装饰器模式
- 装饰器模式可以在不改变原来类的功能的基础上,附加上额外的逻辑处理。
- 优点:
- 可以灵活的选择使用功能扩展(使用装饰类或使用原始类)。
- 只要原始类实现的是同一个接口,那么这个装饰器可以对多个原始类进行修饰。
- 符合开闭原则的基础上,能对原始类进行优化。
- 缺点:
- 原始类和装饰类都需要额外实现装饰接口,增加了代码复杂性。
- 需要声明额外的相似的对象,降低了代码的可读性。
- 文字有点苍白,直接上栗子!
public interface Decorateable {
void say();
}
public class Native implements Decorateable {
@Override
public void say(){
System.out.println("我就是我,最单纯的我~");
}
}
public class Decorater implements Decorateable {
private final Decorateable decorateable;
public Decorater(Decorateable decorateable) {
this.decorateable = decorateable;
}
@Override
public void say() {
decorateable.say();
System.out.println("但就是要给你加点颜色不一样的花火!");
}
}
public class Starter {
public static void main(String[] args) {
Decorateable na = new Native();
na.say();
System.out.println("======================================");
Decorateable decorater = new Decorater(na);
decorater.say();
}
}
- 执行结果如下。怎么说呢,可能只能用《九品芝麻官》里的一个桥段来形容了:我进来啦!我又出去啦!我又进来啦!!这种来去自如的感觉真不戳~
我就是我,最单纯的我~
======================================
我就是我,最单纯的我~
但就是要给你加点颜色不一样的花火!
代理模式
- 代理模式和装饰器模式很像,但是作用不一样。代理模式的作用是以中介的身份,替代一个类与其他类交互的过程。
- 打个很恰当的比方:假如我要租房子,我就是被代理的类,中介就是代理类,而房东就是其他类。在找房子的时候,并不需要我亲自出面来与房东交互。
- 代理模式的作用主要在于控制被代理类的访问权限以及动态添加方法的额外逻辑处理。
- 举个栗子如下,因为和装饰器模式很像,所以这里就只放出代理类,其余代码同装饰器模式的例子。
public class Proxy implements Decorateable {
private final Decorateable decorateable = new Native();
@Override
public void say() {
decorateable.say();
System.out.println("代理类在给你加点颜色不一样的花火!");
}
}
- 从代码就能看出来,代理模式和装饰器模式存在以下区别:
- 装饰器模式通常将原始类对象作为参数来构造装饰类对象;而代理模式直接创建被代理的类的对象。
- 装饰器模式关注于在一个对象上动态的添加方法;代理模式关注于控制对象的访问。
行为型模式
模版方法模式
- 模板方法模式可以说是一个特别简单,但是又特别实用的设计模式了。
- 其通过在一个抽象类中提取公共的方法作为普通方法、将不同处理逻辑的同名方法设置为抽象方法,由子类根据自身需求来具体重写抽象方法,从而让固定的流程产生不同的结果。
- 这样做的优点在于提高代码可读性、结构性,从而降低后期维护成本。而模板方法模式目前没有发现明显的缺点。
- 听起来好高端,其实很简单,比如还是拿Person类那一系举个栗子:
public abstract class Person {
public void walk(){
System.out.println("用两条腿走路");
}
public abstract void speak();
}
public class Man extends Person {
@Override
public void speak() {
System.out.println("男性说话音调会低一些");
}
}
public class Woman extends Person {
@Override
public void speak() {
System.out.println("女性说话音调会高一些");
}
}
public class Starter {
public static void main(String[] args) {
testFactory();
}
public static void testFactory(){
Person man = new Man();
Person woman = new Woman();
man.speak();
woman.speak();
man.walk();
woman.walk();
}
}
- 运行结果如下。Person类这一族中,walk的实现逻辑是一模一样的,所以我们将其提取出来放在模板类中;而speak方法,虽然是实现相同的目的,但是不同子类之间的实现逻辑会有差异,所以需要声明为抽象方法,强制要求子类重写逻辑。
男性说话音调会低一些
女性说话音调会高一些
用两条腿走路
用两条腿走路