很多时候在工作中其实我们也用到了很多软件的思想和设计模式,可能觉得没有必要去专门学习设计原则和模式,我刚做开发前几年也是这么想的。确实不学这些也能写出很好的代码,但是就像盖一栋房子,不用任何工具和学习也可以盖,但是这样盖出来的房子能住多久?而且当你再次想盖房子的是还是走以前的老路,花的时间很长而且效果还不是很好。就算不用工具起码得总结出一些步骤对吧,起码知道下次盖房子需要准备什么。扯了这么多其实就想说明一下,设计原则其实就是一种思想,一种方法论,而设计模式就是我们盖房子的工具,是solid的原则的具体体现。这么一说就很清晰了,你可以不学设计模式,但是设计原则绝对会帮到你很多,设计模式最大的好处就是当你需要使用工具时会帮你省下很多的时间,更轻松的解决各种业务场景。以下内容仅是个人学习的一点总结分享,第一次写技术博客分享,不喜勿喷。
目录
solid原则
设计模式
工厂模式
单例模式:
克隆模式
建造者模式
代理模式
适配器模式
桥接模式
享元模式
组合模式
委派模式
模板模式
门面模式
装饰器模式
策略模式
责任链模式
迭代器模式
命令模式
状态模式
备忘录模式
中介者模式
解释器模式
观察者模式
访问者模式
学习设计原则之前请记住,这是思想,要记住思想不要去背,它是前辈们准备的心血和礼物。我们在实际去使用的时候要记住尽可能的去符合这些原则,但是,但是,但是,重要的事说三遍,不要做杠精,要学会举一反三,看具体情况而定,就像一粒沙子也可能是一个世界。
开闭原则:很简单也很好记,对扩展开放,对修改关闭。
依赖倒置原则:这个可能不太好记,但是你只要记住它的思想,高层不要依赖低层,依赖抽象不要依赖细节,如果实在不理解,你就记住一句很熟悉的话就行,面向接口不要面向过程。
单一职责原则:这个很好理解,术业有专攻嘛,各自做自己擅长的才不会乱对吧。
接口隔离原则:这个其实跟单一职责很像,但是它强调的是接口,你可以认为是接口的单一职责,为什么要强调接口呢,因为接口编程我们用的多啊,如果不隔离就会很乱出现各种问题。
迪米特法则:最少知道原则,这个也很好记,不要和单一职责搞混了,这里的最少知道可以理解为,对我有用的我才需要知道,为什么这么做呢,这就是工匠精神。
里氏替换原则:这个很多人可能不好理解,还是记住一句话,思想,思想,思想,这个原则表面的意思就是子类重写父类的方法时不能改变原来的作用,还是很难理解?其实这个原则最大的作用就是同一个父类的子类可以相互替换,父类也可以替换子类?这不就是多态吗?明白了吗,其实就是为了程序的灵活性。
合成复用原则:这个就比较好理解了,就是尽量使用组合不要使用继承。为什么呢?因为继承的耦合性太强,万一业务变动是不是改代码改到想吐?其实就是尽量降低代码的耦合性。
工欲先其事必先利其器,其实掌握了设计原则设计模式不也无所谓,但是谁会拒绝一把称手的工具呢,事半功倍毕竟是最好的。感兴趣的小伙伴可以去了解一下GoF,这里就不多说了。以下内容是自己学习时的笔记,不一定适合所有人,但是可以帮助理解。
推荐一本书《设计模式就该这样学》
创建型模式,用来创建对象。
简单工厂: 解耦系统功能和逻辑部分,高内聚低耦合。
工厂方法:在简单共产的基础上增加单一职责,优点是客户端只关心工厂不用关心产品,缺点是产品多的时候,工厂也多。
抽象工厂:产品多维度时,抽象出稳定的产品维度,工厂的维度负责拓展。根据具体业务选择。
创建型模式
目的是自始至终只创建一个实例,否则就不是单例
饿汉式:执行效率高,性能高,没有任何的锁,某些情况会造成内存浪费。
懒汉式(使用静态方法):被外部类调用时才创建,节省内存,使用synchronized加载静态方法上可以解决线程安全问题,但是会造成性能问题。
懒汉DoubleCheckLock(DCL):解决性能问题,但是不够优雅。
instance要加上volatile防止指令重排序问题和线程内可见问题
volatile static LazyDoubleCheckSingleton instance ;
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
syncronize(LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
懒汉式内部类写法:使用静态内部类写法,利用java的语法特点,静态内部类使用时才进行初始化。性能高,避免内存浪费。不能够被反射破坏,不够优雅。
public class TestStaticInnerClass {
private TestStaticInnerClass() {
if (null != Inner.Instance) {
throw new RuntimeException("不允许非法访问");
}
}
public TestStaticInnerClass getInstance() {
return Inner.Instance;
}
private static class Inner {
private static final TestStaticInnerClass Instance = new TestStaticInnerClass();
}
}
枚举式:写法优雅和安全但是可能会造成内存浪费,同饿汉式单例一样是加载时就已经创建好对象。这也是官方推荐的。
public enum EnumSingleton {
INSTANCE;
private Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
//这段代码其实可以不用,因为枚举类的实例是public static final修饰,所以可以直接类名调用
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
注册式:内存不浪费,但是存在线程安全问题(可以在if代码块上加锁)
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map ioc = new ConcurrentHashMap();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
}
return ioc.get(className);
}
}
序列化/反序列化:使用重写readResolve方法可以防止反序列化导致的单例被破坏的情况。
ThreadLocal单例:当前线程是单例
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}
private static final ThreadLocal theadLocalInstance=new ThreadLocal()
{
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return theadLocalInstance.get();
}
}
单例总结:枚举类单例可以防止反射和反序列化并且线程安全(反射底层对枚举类做了限制)。防止反序列化的破坏可以重写readResolve方法(这个可以反序列化但是保证单例)或者readObject()抛出异常(这个方式会禁止序列化的使用)。如果不用没聚类想要防止反射就要重写构造方法。
枚举类不能反射是底层判断了如果是枚举类不允许使用反射。
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
枚举类反序列化的时候会调用valueOf方法,最后会从类中的下面的Map去取保存的对象
private volatile transient Map enumConstantDirectory = null;
创建型模式
克隆模式会破坏单例,所以单例不要实现cloneable接口,或者重写clone方法返回单例对象。克隆和单例是对立的2种模式。
浅克隆:实现cloneable接口,Object底层实现。(存在问题:引用类型的变量克隆只克隆指针)
深克隆(二进制拷贝):使用序列化的方式去克隆。readObject()方法;也可以利用引用类型的变量自身的克隆去重写赋值,如ArrayList、HashMap等本身实现了cloneable接口。
三种方式实现深克隆:递归、序列化、Json流
xx deepclone(){
ByteArrayOutputStream bos=new ByteArrayOutputStream ();
ObjecOutputStream oos=new ObjecOutputStream (bos);
oos.write(this);
ByteArrayIntputStream bis=new ByteArrayIntputStream (bos.toByteArray());
ObjectIntputStream ois=new ObjectIntputStream(bis);
return (xx)ois.readObject();
}
创建型模式
链式编程,创建和使用分离,提前写好构造的方法,让用户在使用时自己去根据情况进行组装(例子:StringBuilder)。
优点:封装性好,创建和使用分离;扩展性好,建造类之间独立,一定程度上解耦。
缺点:产生多余的builder对象,内部发生变化,建造者都要修改,成本较大。
结构型模式
本质:用代码去写代码,通过生成字节码,重组一个新的类。
作用:实际应用中,让程序员开发可以只关注业务,架构上的通用的逻辑由代理去实现,如数据源的切换等。
代码增强,房屋中介,婚介所等
静态代理(编译时就已经知道代理):张三他爸帮他找媳妇。张三爸负责找,张三谈恋爱,张三爸是代理。
动态代理(每次代码执行时动态生成代理):有jdk实现(兄弟关系)和cglib实现(父子关系)。
JDK实现:
实现接口的方式,必须要求代理的目标对象一定要实现一个接口,依赖性更强,调用也更复杂,生成逻辑较为简单;执行效率低,每次都要用到反射。
public class JdkMeipo implements InvocationHandler {
private IPerson target;
public IPerson getInstance(IPerson target){
this.target = target;
Class> clazz = target.getClass();
return (IPerson) Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(this.target,args);
after();
return result;
}
private void after() {
System.out.println("双方同意,开始交往");
}
private void before() {
System.out.println("我是媒婆,已经收集到你的需求,开始物色");
}
}
cglib实现:
继承方式,覆盖父类的方法,对目标类没有任何要求,使用FastClass,性能也更高,底层没有用到反射
有个坑,目标代理类不能有final方法,忽略final修饰的方法
public class CGlibMeipo implements MethodInterceptor {
public Object getInstance(Class> clazz) throws Exception{
//相当于Proxy,代理的工具类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object obj = methodProxy.invokeSuper(o,objects);
after();
return obj;
}
private void before(){
System.out.println("我是媒婆,我要给你找对象,现在已经确认你的需求");
System.out.println("开始物色");
}
private void after(){
System.out.println("OK的话,准备办事");
}
}
结构型模式
本质是兼容。
适配器模式也叫变压器模式,属于对现有产品的补充式开发,现有的产品满足不了新的客户需求,要实现新的接口就要同时继承原来的类产品在进行转换。
1.类适配器模式:违背最少知道原则,因为继承了父类又实现了新接口。
2.对象适配器模式:遵守最少知道原则,采用聚合,java是单继承,聚合使得对目标类不一定要求是接口了,更灵活。
场景:
1.220v电压转为5v,20v,7v。
2.系统可以使用qq、微信、手机等多种第三方登录方式。
结构型模式
精髓:连接。约定优于配置,不允许用继承。
本质是管理不同维度,但可以不做具体实现。
抽象类使用组合和聚合的方式加上实现接口,把相关2个维度的类关联在一起。这样可以避免多继承造成的代码混乱,不利于扩展。
场景:
1.发生消息的方式:邮件、短信
2.消息的类型:普通、紧急
3.用抽象的组合和继承可以实现2个维度的关联,避免了多继承的混乱。
总结:属于结构型模式。有内部状态(不变),外部状态(可变)。
优点:池化技术,减少对象的创建,降低内存中的对象数量,降低系统内存,提高效率。减少内存之外的其他资源占用(新对象可能会更多占用端口等资源)。
缺点:关注内外部状态和线程安全。使系统、程序逻辑变的复杂。
享元模式相关的模式:
享元模式和代理模式的区别:代理是功能增强,享元模式是为了合理利用资源。
享元模式和单例模式的区别:享元模式和工厂模式、单例模式配合使用。但是享元模式缓存的实例是多个。
精髓:很多节点保持类型一致,方便处理。
也叫整体-部分模式,作用是使客户端对单个对象和组合对象保持一直的处理方式。宗旨是将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口表示,解决了无限嵌套的问题 。
透明式写法:根节点是很多空实现方法,树枝和叶子节点根据需要去实现。层次差异小,结构容易变。
场景:课程目录,缺点是课程也能调用目录的添加方法,违背最少知道原则。
安全式写法:父类只定义公共的方法,其他的方法子类自己去实现,符合最少知道原则。层次差异比较大,结构比较稳定。
优点:清楚定义分层次的复杂对象,表示对象的全部层次或部分层次。让客户端忽略了层次的差异,方便对整个层次结构进行控制。简化客户端代码,符合开闭原则。
缺点:限制类型比较复杂(需要特殊处理),使设计变的更加抽象。
行为型模式
只对结果负责。ClassLoader.
委派对象不知道自己怎么处理请求和任务,交给其他对象处理,双亲委派。
场景:BOSS->Leader->EmployeeA……->EmployeeX
优点;细化任务。
缺点:多重委派造成紊乱。
委派模式和代理模式区别:委派模式行为型模式,代理模式是结构型模式。委派模式注重的是任务派遣注重的是结果,代理模式注重的是代码增强,注重过程。委派模式是一种特殊的静态代理模式,相当于全权处理。
行为型模式:
骨架,逻辑复用。
场景:一次性实现一个算法不变的部分,将可变的行为留给子类去实现。各个子类中公共的部分被提取出来并集中到一个公共的父类中,从而避免代码重复。适合步骤稳定的场景去用的模式。
结构型模式
又叫外观模式,提供一个统一的接口来访问子系统中的一群接口。
特征:门面模式定义了一个高层接口,让子系统更容易使用。
适用场景:子系统越来越复杂,增加门面模式提供简单接口。构建多层系统结构,利用门面对象作为每层的入口,简化层间调用。门面类只负责调用,客户端使用门面,客户端变的简单。
1.门面模式和代理模式的区别:
门面模式就是特殊的静态代理模式:门面模式重点在封装,代理是增强。不做增强的代理就是门面模式。
2.门面模式和单例模式的关系:
门面模式做成单例,工具包。
行为型模式
精髓:组件和附加功能分离,附加功能可以扩展、增加和移除。装饰器也是组件父类的子类。
结构型模式,也叫包装模式。不改变原有对象来附加功能,比继承更好的方案。动态透明的去扩展功能,也可以动态去撤销。装饰器父类一定是抽象类,因为都要继承组件接口所以要和组件区分,还有就是要统一管理不同的装饰器。
行为型模式
例如选择支付方式。
精髓:用户+策略工厂+策略,策略相互替换。
要理解策略模式的策略就是不同的行为,用策略模式可以解决硬编码的问题。
作用:大量ifelse语句,扩展也更方便。封装使得客户端调用简单(工厂负责初始化创建对象)。当策略多的时候可以使用装饰器+策略模式。
场景(如优惠券):
1.假如系统中有很多类,而他们的区别仅仅在于他们的行为不同。
2.一个系统需要动态的在几种算法中选择一种。
3.需要屏蔽算法规则。
行为型模式
精髓:每个节点只负责自己的任务和维护下一个节点是谁。各扫门前雪,踢皮球。
本质就是把处理请求的操作变成节点,在把节点链接起来。
将链中的每个节点看作是一个对象,每个节点处理不同的请求。且内部自动维护一个下一个节点对象。当一个请求从链式首端发出时,会沿着链的路径一次传递给每一个节点对象,直至有对象处理这个请求为止。
场景:审批流程,过五关斩六将。
作用:动态处理请求。请求和处理解耦。客户端维护链式的节点需要哪些。
行为型模式
也叫游标模式,理解这个模式的关键是要理解集合(具体的对象集合,由用户自己创建的容器)和数据结构(MAP、LIST)的不同,当然JDK内部也有自己实现的迭代器。供一种顺序访问集合/容器对象元素的方法,而又无须暴露集合内部表示。
本质:抽离集合对象迭代行为到迭代器中,提供一致访问接口。
可以有很多迭代器的实现,但是通过同一个接口访问就不需要关心具体的对象的存储方式和结构。获取数据时只要获取迭代器就可以。
行为型模式
命令(action)、命令启动模块(controller)、接收方(reciver)。
可以实现命令宏。
源码应用:Thread线程
优点:
1.通过引用中间件(抽象接口),解耦了命令请求和实现。
2.扩展性良好,可以很容易地增加新的命令。
3.支持组合命令,支持命令队列。
4.可以在现有的命令的基础上,增加额外新功能(比如日志记录……结合装饰器模式更酸爽)。
缺点:
1.具体的命令类过多。
2.增加程序的复杂度,理解更加困难。
行为型模式
精髓:状态自动切换-->行为改变。
也叫状态机模式,允许对象在内部状态发生改变时改变他的行为,对象看起来好像修改了他的类。切换状态需要通过上下文。行为和状态进行了绑定。上下文负责初始化所有状态。
场景:springmachine包实现了状态机。
状态模式和策略模式的区别,状态模式的状态改变是系统根据不同情况自动进行切换,具体的状态由上下文进行创建和切换。策略模式的策略则是交给用户自己去选择,用户一旦选择某种策略则策略执行期间不能进行切换,也就是策略在用户侧决定之后不能改变,除非用户再次主动发起。状态模式的行为和状态之间有动态的联系,而策略模式的策略和行为是提前就已经定死的。
行为型模式
也叫快照模式或令牌模式。是指在不破坏对象封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后可以恢复到原先保存的状态。特征:后悔药。
举例:编辑器保存副本到草稿箱,撤销时可以把草稿箱的内容恢复到编辑器,这里的草稿箱是个栈结构。
本质:就是用栈(也可以是其他数据结构)保存内部状态,必要时进行恢复。
行为型模式
精髓:利用中间对象去组合使得复杂变简单。
也叫调节者模式或调停者模式。用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其解耦,而且可以独立地改变他们之间的交互。
核心:通过中介者解耦系统各层次对象的直接耦合,层次对象的对外依赖通信统统交由中介者转发。
适用场景:
1.系统对象之间存在复杂的引用关系,产生相互依赖的关系结构混乱且难以理解。
2.交互的公共行为,如果需要改变行为则可以增加新的中介者类。
理解中介者和代理模式的区别:代理模式可以做到具体角色做不到的事,代理就是提供更强大的功能。中介者只是帮助建立联系,不办事。
理解中介者和桥接模式的区别:桥接模式是解决2个维度之间的问题,中介者则是解决更复杂的系统集群的问题。两者都是建立联系。
举例:RPC
行为型模式
思想:常用的简单的表达式进行抽离。
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释预言者中的句子。特征为了解释一种语言,而为语言创建的解释器。
适用场景:重复出现的问题用一种简单的语言表达,一个简单语法需要解释的场景。语法复杂的不适合用解释器模式。
优缺点:过程编码变成对象解析,扩展性强,但是类膨胀、效率低。
行为型模式
精髓:被观察者+事件(触发之后缓存也可以是注册)、观察者+事件(获取缓存事件)+回调。
也叫发布-订阅、模型-视图、源-监听模式、从属者模式。定义一种一对多的关系,一个主题对象被多个观察者对象同时监听,当主题发生变化时,所有依赖它的对象都会得到通知并且被自动更新。
JDK实现:观察者(Observer),被观察者(Observable);
第三方实现:guava。
被观察者通知观察者,被观察者要知道有哪些观察者(注册)。
优点:符合依赖倒置原则,被观察者和观察者松耦合(抽象耦合)。分离了表示层(观察者)和数据逻辑层(被观察者),建立了一套触发机制,使得数据的变化可以响应到多个表示层上。实现了一套一对多的通讯机制,支持事件注册机制,支持兴趣分发机制,当被观察者触发事件时,只有感兴趣的观察者可以接收到通知。
缺点:观察者数量过多,则事件通知会耗时较长;事件通知呈线性关系,观察者处理事件卡壳会影响后续观察者接收事件。如果观察者和被观察者之间存在循环依赖,则可能造成两者之间的循环调用,导致系统崩溃。
行为型模式
提供原始数据,数据组织交给用户自己去做,每个用户对同样的数据有不同的观察维度。
是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作。特征是可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
适用场景:数据结构稳定,作用于数据结构的操作经常变化的场景;需要于数据操作分离的场景;需要对不同数据类型进行操作,而不使用分支判断具体类型的场景。
使用到技术:方法的静态(编译时分派)和动态分派(运行时分派),java支持单分派。访问者模式用到了伪动态的双分派。
源码中的应用:FileVistior.
优点:
1.解耦了数据结构与数据操作,使得操作集合可以独立变化。
2.扩展性好,可以扩展访问者角色,实现对数据的不团操作。
3.元素具体的类型并非单一,访问者均可操作。
4.各角色职责分离,符合单一职责原则。
缺点:
1.数据类型结构不稳定的话就会违背开闭原则。
2.具体元素变更困难,元素和访问者依赖太强。
3.违背依赖倒置原则,访问者依赖的是具体元素而不是抽象。
本来想总结一下,但是我觉得自己总结更加有意义,设计模式不是立马就能精通的,博大精深,只有自己去体会才能知道好处。