Java 23 种设计模式学习
Posted 皮豪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 23 种设计模式学习相关的知识,希望对你有一定的参考价值。
Java设计模式
解决普遍存在的问题,反复出现的各种问题,所提出的解决方案。
设计模式七大原则
- 设计模式七大原则:
- 单一职责
- 接口隔离
- 依赖倒转
- 里氏替换
- 开闭原则
- 迪米特法则
- 合成复用原则
面向对象 => 功能模块[设计模式+算法] => 框架[调用多种模式] => 架构[服务器集群]
单一职责原则
- 降低类的复杂度,一个类只负责一项职责
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 通常情况下,应当遵守单一职责原则,保有逻辑足够简单,才可以在代码级违法单一职责原则,只有类中方法数量足够少,可以在方法级别保持单一职责原则
接口隔离原则
将一个接口的功能进行拆分,让其成模块化的。这里的例子就是,一个接口有五个方法,如果两个类继承了,此接口,则需要实现十次。有些方法是用不到的。所以需要将折口拆分成几个接口。
这样的使用的和维护的时候,更有针对性,而且耦合度更低。当然,其缺点就是可能会造成模块粒度不可控。
依赖倒转原则
将一个类型抽象为接口,这类型都是相同的属性,具有不同的细节特性。
如果动物 下 有 狗和猫,其都有走路和吃食的 属性(方法),但吃的不一样,所以需要不同的实现。
但是整体业务是一样的,只需要更换不同的实现。面向接口编程就如同面向规范编程,不同产商同一标准,生产相同的零件,可能是颜色不一样,Logo不一样。
下面只是 依赖关系 传递的一种操作。
- 接口传递
- 构造方法传递
- setter传递
总结:
- 底层最好是抽象类或接口,程序稳定性更好
- 变量的声明类型尽量是抽象类和接口,这样在使用时,就是可以利用多态的特性,而不是直接把接口写死,利于后期代码的更新和维护。
- 继承时遵循晨氏替换原则
里氏替换原则
这个我的总结就是不应该破坏原有的封装。在添加新的功能时,应当考虑原有代码的这个封装和可能存在的误导性。新的类或者方法需要与之前的模块进行一定的区分和隔离。
这个隔离,就是把原有的继承关系,变成依赖关系。即可以重写方法,又可以使用相关性非常高的原有的继承关系。
开闭原则
这原则的核心。
很像是依整倒转原则,不过基类变成了抽象类。
主要是提供使用接口,让其不需要为每个使用到的类而重写一个功能方法,直接使用抽象类的方法就可以了。
迪米特法则
又称最少知道原则。
简单理解为,如果不依赖,就不要在本类创建其任何相关的局部变量。
不依赖的意思就是不是直接朋友,不是直接朋友就是不依赖。
老师内,有直接依赖学生,所以老师可以在局部创建学生的变量,而学生并不依赖老师,所以在使用的时候,不能直接创建老师的局部变量。
所以这种操作,可以转为 其他的直接朋友进行操作,就是一个主抽象类,即依赖了老师,又依赖了学生,那么这种局部的操作,可以转给此类进行操作。
- 迪米特法则的核心是降低类之间的耦合
- 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间耦合关系,并不是要求公开赛没有依赖关系。
合成复用原则
原则是尽量使用全成/聚合的方式,而不是使用继承。
就是说,新手可能在使用一个类的时候会直接使用继承,这样显然是主动耦合了,在维护上会出现很大的关联性的问题。一般在使用一个类的时候,一般使用 依赖,合成 和 聚合。
总结
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象之间的松耦合设计而努力。
设计模式
类型
- 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
- 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式(责任链模式)
创建型模式
单例模式
懒汉式(线程安全,同步方法)
- 解决了线程不安全问题,但效率太低了,也不推荐,但我记录下。
双重检查
- DoubleCheck概念是多线程使用的
- 再次判断,线程安全,延迟加载,效率较高
实际开发中,非常推荐
静态内部类
使用了JVM的装载机制,静态内部类在父类装载时并不被实例化,而是在需要实例化时,调用 GetInstance方法,才会装载SIngletonInstance类。
优点:使用JVM的机制实现了懒加载,并且是线程安全的。
推荐使用
单例枚举
能避免多线程同步的总理 ,还能防止反序列化重新创建新的对象
《effective Java》作者推荐
推荐使用
总结
单例模式注意事项和细节说明:
-
单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
-
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
-
单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多,但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)。
Runtime 使用的是 饿汉式 单列模式的编写方法。
每次使用的时候都需要考虑使用场景。
工厂模式
通过一个工厂类,获取不通的解决方案。
工厂可以是类也可以是方法。
最好的例子,就是造包子,不同的馅料。
工厂方法模式:定义了一个创建对象的抽象方法,由子类决定要实例化的类,工厂方法模式将对象的实例化推迟到子类。
抽象工厂方法
虽然这个抽象方法也就是一个接口,但其是用了几个工厂类的实现。
就是进一步的将工厂进行抽象了,先是基于工厂为基础进行开发。而抽象工厂,则是直接将这个抽象工厂作为一个中心。(这个图中的UML是错的)
静态工厂
在一个里有一个静态方法用来将输入的数字yyyyMMdd转成LocalDate
抽象工厂
这里的抽象工厂的作用就是将使用的工厂和接口都抽象出来,然后每种类型都需要不同的实现。
测试 方法
工厂类接口和工厂实现类
Document接口和实现类
工厂模式小结
- 工厂模式的意义
将实例化的对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦,从而提高项目的扩展和维护性 - 三种工厂模式(简单工厂模式、工厂方法模式、抽象工厂模式)
- 设计模式的依赖抽象原则
设计模式的依赖抽象原则:
- 创建对象实例时,不要直接new类,而是把这个new类的动作放在一个工厂的方法中,并返回。有的书推荐,变量不要直接持有具体的引用。
- 不要让类继承具体类,而是继承抽象类或者是实现interface。
- 不要覆盖蕨类中已经实现的方法
原型模式-基本介绍
基本介绍
- 原型模式(Prototype)是指:用原型实例指定创建对象的各类,并且通过挂账这些原型,创建新的对象。
- 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
- 工作原理是通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过 请求原型对象拷贝它们自己来实施创建,即 对象.clone()
- 需要实现Cloneable接口,不然会报错
Spring的原型的应用
Spring配置Bean时可以选择使用原型还是单例。如果选择原型,则每次创建的对象都不同。
- 分析Spring的bean创建的代码
深拷贝
浅拷贝时,对象内的成员属性如果是引用类型,则直接复制其引用的地址,而不是重新拷贝一份。这样的问题就是所有浅拷贝的对象,都是同一个引用成员属性。
clone方法里手动拷贝
- 引用对象越多越不方便
使用序列化来实现拷贝(推荐)
原理: 将对象序列化成字节数组,然后将字节数组反序列化。这样原来的值都需要创建新的内存空间,所以引用会发生改变。以达到深度拷贝的效果。
原型模式总结
默认是使用浅拷贝,如果需要深度拷贝,则需要自定义方法,会破坏OCP原则。
原型的话,就是在Bean的 创建的时候使用,我想,如果创建一个容器,或者获取一些临时的Bean的时候,都需要用到。
建造者模式
好理解,易操作。
设计的程序结构,过于简单,没有设计缓存层对象,程序的扩展和维护不好,也就是说,这种设计方案,把产品和创建产品的过程封装在一起,耦合性增加了。
解决方案:将产品和产品建造过程解耦 =》 建造者模式。
-
尚硅谷的版本
-
ChatGPT写的版本
生成器模式
聚合一些成员属性,然后使用这些成员属性建构一个新的对象。
下面的例子就是使用 使用一些字符串,建构一个URL链接。
接口适配器
我觉得在适配器里把聚合的思想应用 的非常好。就是把一些不能直接使用的资源,用适配器过滤一下就能用了。并且耦合性不高,扩展性很好。
(视频确实讲的很全)
然后就是有几种实现方式:
总结
就是依赖倒转,为每个可能实现一个Adapter类,然后需要实现对应的操作。
类适配器:在Adapter里,将src当做类,继承
对象适配器:在Adapter里,将src作为一个对象,持有
接口适配器:以Adapter里,将SRC作为一个接口实现
下图就是接口适配器。
接口模式
- 桥接模式(Bridge模式)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
- 是一种结构型设计模式
- Bridge模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来。从而可以保持各部分的独立性以及应对他们的功能扩展。
具体的操作:就是抽象一个接口,然后聚合这个接口,但是其使用的依赖倒转,其本质的实现是不相同的。
装饰者设计模式
装饰者就相当于快递打包,一层一层的。线性的角度来看有点像链表,其实就是一个层层加码的过程。你需要什么,你就需要套着什么。例子就是咖啡加一些小料。
我的感觉就是通过引用一层套着一层,在功能上是可以无限扩充的。就是基于一个主体,无限 扩充。
一个哲学的概念就是:“学习我,成为我”。
- 装饰者模式:动态的将新功能附加到对象上,在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则。
下面是执行结果
这个例子是很容易的理解的,非常类似于递归。需要注意的事,在返回price时,需要返回前几个Price总的值,而不是返回当前的price。可以说是对象嵌着对象的递归操作。
组合模式
这是把系统中所有模块都有统一的特性,然后这些模块都是有相互的关系,上下级。然后组成一套系统。
所以说每个模块都实现了这个Component类,然后又是递进的关系进行依赖。
(我越来越感觉这是不是一种抽象级别的数据结构)
- 简化客户端操作。客户端只需要面对一致的对象,而不用考虑整体部分或者节点叶子的问题。
- 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何发动。
- 方便创建出复杂的层次结构,客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构。
- 需要遍历组织机构,或者处理对象具有树形结构时,非常适合使用组合模式
- 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式。
总结
跟意思差不多,不过是组合关系。就像人没有心脏是不能活的这种强依赖关系,而形成的组成模式。
外观模式
也是组合的操作。将很多类组合,然后这些类的操作都是统一有条件的操作,是有流程的。
这里的例子非常好理解就是电影院,把各个模块的类,放到电影院,然后只需要操作电影院这个类暴露出来的接口就行了。
让我写个简单的例子。
总结
外观模式的注意事项和细节:
- 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统的复杂性
- 外观模式对客户端与子系统的耦合关系,让子系统的内部的模块更易维护和扩展
- 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
- 当系统需求进行分层设计时,可以考虑使用Facade模式
- 在维护一个遗留大型系统时,此是可以考虑将难以为扩展的类用一个Facade来实现,让新系统与Facade类交互,提高复用性
- 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好,要让系统更有层次,利于维护为目的。
享元模式
就是大家一起共享一些资源,这些资源通常是经常使用,而且模块独立性高。
执行结果如下:
总结
享元模式有点 像线程池的概念,“享“表示共享,“元”表示对象。
系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式。
用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable存储。
享元模式提高了系统的复杂度,需要分享出内部的状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是需要注意的地方
使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
享元模式经典的应用场景是需要缓冲池的场景,比如String常量池,数据库连接池。
代理模式
不直接使用一个对象,而是间接的使用一个对接,在这个间接的过程中可以添加很多的操作。
静态代理
这里的具体操作就是,将一个具有普遍性的方法,用代理来扩展,如果不具有普遍性,则依然是用其本身的功能。
动态代理
这个呢,很像Mybatis的操作,就是用动态代理实例化一个接口。
当然这个作用还可以是给一个类加上其他功能,下面的代码中就是给已经的实现的teach再加了一些代码。这类操作可以用来做一个方法的事务管理。如果在动态代理中执行失误了,就回撤数据,如果没有问题就放行。
在尚硅谷这里的学习收获非常大。
Cglib代理
- 静态代理 和 JDK代理模式都要求目标对象是实现一个接口,但有时候对象只是一个单独的对象,并没有实现任何的接口,这个蚨可以使用目标对象子类来实现代理 - 这就是Cglib代理。
- Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展,有些书将Cglib代理归到动态代理
- Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展Java与实现Java接口,它广泛的被许多AOP的框架使用,SpringAOP,实现方法拦截
- 在AOP编程中,如何选择代理模式:
- 目标对象需要实现接口,用JDK代理
- 目标对象不需要实现接口,Cglib代理
- Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类
下面的代码跑起来需要cblig和asm的库,建议用Maven项目跑,(是这样写的,但我没跑起来,不打算试了。)后面用到的时候肯定能解决问题的。
总结
其他代理方法
除了静态代理、动态代理、Cglib代理外,还有几种变体
防火墙代理:内网通过代理穿透防火墙,实现对公网的访问。
缓存代理:当请求图片文件等资源时,先到缓存代理取,有则返回,没有则到公网或数据库里取,然后缓存(到时候写缓存的时候可以用)
远程代理:远程对象的本地代表,通过它可以把远程对象当本地对象来调用,远程代理通过网络和真正的远程对象沟通信息。(RPC?Feign?)
同步代理,主要使用在多线程中,完成多线程间同步工作。
行为型
模板方法
用我的话来说就是一些特定的模块,可能需要自定义一些算法,这些自定义的算法需要你根据你的使用场景进行自定义。而且这些实现模块的类,仅是一些小差别,如果差异太大,可能不适合这个模块方法。
AbstractClass 抽象类,类中实现了模板方法(template),定义了算法的骨架,具体子类需要去实现,其它的抽象方法
ConcreteClass 实现抽象方法,以完成算法中特点子类的步骤
总结
这是尚硅谷的总结
模板方法的注意事项和细节:
- 基本思想是:算法只存在于一个地方,也就是父类中,容易修改,需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
- 实现了最大化代码复用,父类的模板方法和已实现的某些步骤会被子类继承而直接使用
- 即统一了算法,也提供了很大的灵活性,父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现
- 该模式的不足之处,每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。
- 一般模板方法都加上final关键字,防止子类重写模板方法
- 模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤,这一系统的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理