前言
写在前面,设计模式的核心要义,面向接口编程,组合优于继承
,要搞清楚我们学习设计模式并不是为了设计模式而学习设计模式,发心是为了让代码更加优雅、美观、健壮。让后来接手的人看到我们构建的“宏伟大厦”,忍不住的说一句:我草,牛B!这里借鉴很久之前看见的一句话,我等采石之人当怀大教堂之理想!(虽然我们是最底层采集石头的人,但是我们还是要有修建教堂的信念;众所周知,中世纪的大教堂,走进去让人深受震撼、宏伟壮观)。
学习设计模式之前要先学习六大设计原则,搞懂了这些设计原则,设计模式自然水到渠成。
一、六大设计原则
1、单一职责(Single Responsibitity Principle)
一个类应该只有一个职责
如何判断一个类的额职责是否单一?
1.类中的代码行数、函数,或者属性过多
2、类依赖的其他的类过多
3、私有方法过多
4、类中的大量的方法总是操作类中的几个属性
2、开发封闭(Open Close Principle)
对扩展开放,对修改关闭(抽象意识,封装意识,扩展意识)
3、里氏替换(Liskov Substitution Principle)
1、什么是替换?只有支持多态特性的语言才具有替换性,简单来说就是当我的一个方法的参数是一个接口类型时,这个方法跨域接收所有实现过这个接口的实现类;(你像Java语言天生的多态语言)
2、什么是与期望行为一致的替换?
在不了解派生类的情况下,仅通过接口或基类的方法,即可清楚的知道方法的行为,而不管哪种派生类的实现,都与接口或者基类方法的期望行为一致。说明:比如List接口添加元素【add(E e)】或删除元素【remove(Object o)】,其实现类是基于数组ArrayList呢?还是基于链表LinkedList,其实都不关心,只要能实现添加元素和删除元素的行为就行
4、接口隔离(Interface Segregation Principle)
一个类对另一个类的依赖应该建立在最小的接口上
5、依赖倒置(Dependence Inversion Principle)
在代码设计架构时,高层模块不应该依赖于底层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象;在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。
6、迪米特法则(Least Knowledge Principle)
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
1、优先考虑将一个类设置成不变类。
2、尽量降低一个类的访问权限。
3、谨慎使用Serializable。
4、尽量降低成员的访问权限。
还有一个合成复用原则,简单来说就是以上六大设计原则的组合技。了解了七大设计原则,在此基础上再针对性的学习,运用设计模式将得心应手。
二、创建型模式:
提供创建对象的机制,能够提升已有代码的灵活性和可复用性
创建型模式有5种,分别是:单例、工厂方法、抽象工厂、建造者、原型
;我一般用单例,工厂居多
1、单例模式
单例一般有2种实现方式,懒汉式和饿汉式2种,懒汉式是在真正用到实例的时候才去创建(DCL),饿汉式是在类加载的时候就创建好实例,jvm保证线程安全,只有一个实例。
1、懒汉式
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2、饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
如何破坏单例?
破坏单例模式的方法主要有以下几种:
- 反射破坏:通过反射机制,可以访问私有构造函数并创建多个实例。可以构造函数中添加逻辑来防止反射破坏单例模式。
- 序列化与反序列化破坏:当一个单例类被序列化后,再次反序列化会创建一个新的实例。可以通过在单例类中添加
readResolve()
方法,返回已有的实例,从而避免破坏。 - 多线程环境下的破坏:在多线程环境下,如果没有合适的同步机制,可能会创建多个实例。可以使用双重检查锁定或者静态内部类等线程安全的方式来防止破坏。
- 克隆破坏:如果单例类实现了
Cloneable
接口并重写了clone()
方法,可以通过克隆对象创建多个实例。可以在clone()
方法中抛出CloneNotSupportedException
异常,阻止克隆破坏。
破坏单例模式需要在特殊情况下有意识地使用上述方法,一般情况下,单例模式是为了保证全局唯一性和访问性,不建议破坏。
2、工厂方法
3、抽象工厂
三、行为型模式
负责对象间的高效沟通和职责传递委派
行为型模式有7种,分别是:代理、适配器、装饰(包装)、桥接、外观、组合、享元;我一般用代理,适配器居多
1、代理模式
1.1静态代理
基于接口的代理,就是java的多态,实际上执行的类,就是接口的一个子类,这个子类可能还会调用目标类。
public class Client{
/**
* 优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展
* 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类
* 一旦接口增加方法,目标对象与代理对象都要维护
*/
public static void main(String[] args) {
// 创建目标对象
StudentImpl student = new StudentImpl();
// 创建代理对象
StudentProxyImpl studentProxy = new StudentProxyImpl(student);
studentProxy.study(); // 实际上调用的是代理对象的study方法,然后代理对象根据业务条件判断是否调用源对象的方法
}
}
有个学生类,学生的核心任务是学习,建一个代理类,通过接口去将目标对象组合进来,然后,在学习方法的前面/后面增加相应的代理逻辑——这就是静态代理。
public interface Student {
/**
* 核心方法是学习
*/
void study();
}
public class StudentImpl implements Student {
@Override
public void study() {
System.out.println("我是学生,我的主线任务是学习知识,充实自己");
}
}
public class StudentProxyImpl implements Student{
// 通过接口,将目标对象组合进来
private Student student;
public StudentProxyImpl(Student student) {
this.student = student;
}
@Override
public void study() {
System.out.println("代理开始:增强业务逻辑");
// 执行目标代码逻辑
student.study(); // 具体是那个study,哪个student,由代理决定
System.out.println("代理结束:增强业务逻辑");
}
}
1.2 动态代理
1.1.1 JDK动态代理
jdk的动态代理也是基于接口的,代理类需要继承java.lang.reflect.InvocationHandler接口重写invoke方法,用Object obj将目标类组合进来
。
1.1.2 Cglib动态代理
用Cglib的动态代理,是因为目标类没有接口;且目标类不能是final修饰的。需要把坐标引入进来,如下:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
代理类需要继承net.sf.cglib.proxy.MethodInterceptor接口,重写intercept方法,用Object obj将目标类组合进来
。
2、适配器
这就是适配器,我只接收B,但是目前只有C,那就造一个A,将BC适配起来。
public class Client {
public static void main(String[] args) {
// 我现在只有Task,callable,但是Thread类只接受Runnable,所以用RunnableAdaptor去将callable适配进来
Task callable = new Task(1234500L);
Thread thread = new Thread(new RunnableAdaptor(callable)); // compile error! 因为Thread接受Runnable接口,但不接受Callable接口,怎么办?
/**
* 方法一:改写Task类,把实现的Callable改为Runnable接口,但这样做不好,因为Task很可能在其他地方作为Callable被引用,改写Task的接口,会导致其他正常工作的代码无法编译。
* 方法二:另外一个办法,而是用一个Adaptor,把这个Callable接口“变成”Runnable接口,这样,就可以正常编译
*/
thread.start();
}
/**
* 编写一个Adapter的步骤如下:
* 实现目标接口,这里是Runnable;
* 内部持有一个待转换接口的引用,这里是通过字段持有Callable接口;
* 在目标接口的实现方法内部,调用待转换接口的方法。
*/
}
public class RunnableAdaptor implements Runnable {
// 继承 or 组合 ? 引用待转换接口
private Callable<?> callable;
public RunnableAdaptor(Callable<?> callable) {
this.callable = callable;
}
// 实现指定接口
@Override
public void run() {
// 将指定接口调用委托给转换接口调用
try {
Object call = callable.call();
System.out.println("call = " + call);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Task implements Callable<Long> {
private long num;
public Task(long num) {
this.num = num;
}
@Override
public Long call() throws Exception {
long r = 0;
for (int n = 1; n < this.num; n++) {
r = r + n;
}
System.out.println("result = " + r);
return r;
}
}
四、结构型模式
如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效
结构型模式有11种,分别是:模板方法、策略、观察者、责任链、迭代器、状态、访问者、备忘录、命令、解释器、中介;我一般用模板方法、策略、观察者居多