设计模式
设计原则
-
开闭原则:对修改关闭,对拓展开放。
-
单一职责原则:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。术业有专攻。
-
里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能。
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。 子类可以增加自己特有的方法。 子类的方法重载父类的方法时,方法的前置条件(方法的输入/入参)要比父类方法的输入参数更宽松。 子类的方法实现父类的方法时(重写/重载/实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更严格或与父类一样。
java// 程序员 public abstract class Coder { // 写代码 public void coding() { } // Java程序员 class JavaCoder extends Coder{ // 打游戏 public void game(){ } } }
JavaCoder 虽然继承自 Coder,但是并没有对父类方法进行重写,并是在父类的基础上进行额外扩展,符合里氏替换原则。
java// 程序员 public abstract class Coder { // 写代码 public void coding() { } // Java程序员 class JavaCoder extends Coder{ // 打游戏 public void game(){ } // 写代码 @Override public void coding() { } } }
这里对父类的方法进行了重写,父类的行为就被子类覆盖了,这个子类已经不具备父类的原本的行为,违背了里氏替换原则。
对于这种情况,我们不需要再继承自 Coder 了,可以提升一下,将此行为定义到 People 中:
java// 人类 public abstract class People { // 写代码。这个行为还是定义出来,但是不实现 public abstract void coding(); // 程序员 class Coder extends People{ // 写代码 @Override public void coding() { } } // Java程序员 class JavaCoder extends People{ // 打游戏 public void game(){ } // 写代码 @Override public void coding() { } } }
-
依赖倒转原则
一直在使用的,最明显的就是 Spring 框架了。
高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。
在使用 Spring 框架之前的情况:
javapublic class UserController { UserService service = new UserService(); // 调用服务 static class UserService { UserMapper mapper = new UserMapper(); // 业务代码...... } static class UserMapper { // CRUD...... } }
突然有一天,公司业务需求变化,现在用户相关的业务操作需要使用新的实现:
javapublic class UserController { UserServiceNew service = new UserServiceNew(); // 调用服务 // 服务发生变化,新的方法在新的服务类中 static class UserServiceNew { UserMapper mapper = new UserMapper(); // 业务代码...... } static class UserMapper { // CRUD...... } }
各个模块之间是强关联的,一个模块是直接指定依赖于另一个模块。虽然这样结构清晰,但是底层模块的变动,会直接影响到其他依赖于它的高层模块。如果项目很庞大,这样的修改将是一场灾难。
而有了 Spring 框架之后,我们的开发模式就发生了变化:
javapublic class Main { public static void main(String[] args) { UserController controller = new UserController(); } interface UserMapper { // 接口中只做 CRUD 方法定义 } static class UserMapperImpl implements UserMapper { // 实现类完成 CRUD 具体实现 } interface UserService { // 业务接口定义...... } static class UserServiceImpl implements UserService { // 现在由Spring来为我们选择一个指定的实现类,然后注入,而不是由我们在类中硬编码进行指定 @Resource UserMapper mapper; // 业务代码实现...... } static class UserController { // 直接使用接口,就算你改实现,我也不需要再修改代码了 @Resource UserService service; // 业务代码...... } }
通过使用接口,将原有的强关联给弱化,只需要知道接口中定义了什么方法然后去使用即可。而具体的操作由接口的实现类来完成,并由 Spring 来为我们注入,而不是我们通过硬编码的方式去指定。
-
接口隔离原则:在定义接口的时候,一定要注意控制接口的粒度
-
合成复用原则:优先使用对象组合,而不是通过继承来达到复用的目的。
-
迪米特法则:每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
创建者模式
简单工厂模式
- 在工厂模式中,创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
- 根据迪米特法则,应该尽可能地少与其他类进行交互,所以可以将那些需要频繁出现的对象创建,封装到一个工厂类中。当需要对象时,直接调用工厂类中的工厂方法来生成对象,这样,就算类出现了变动,只需要修改工厂中的代码即可,而不是大面积地进行修改。同时,可能某些对象的创建并不只是一个 new 就可以搞定,可能还需要更多的步骤来准备构造方法需要的参数。
-
定义水果接口
javapublic interface Fruit { void color(); }
-
定义实现类
javapublic class Apple implements Fruit{ @Override public void color() { System.out.println("苹果:红色"); } }
javapublic class Orange implements Fruit{ @Override public void color() { System.out.println("橘子:黄色"); } }
-
定义工厂
直接使用一个静态方法根据指定类型进行创建
javapublic class FruitFactory { public static Fruit getFruit(String type){ if(type.equals("apple")) return new Apple(); else if(type.equals("orange")) return new Orange(); else return null; }
-
调用
javapublic static void main(String[] args) { Apple apple = (Apple)FruitFactory.getFruit("apple"); Orange orange = (Orange)FruitFactory.getFruit("orange"); } }
存在问题:前面提到了开闭原则,一个软件实体,比如类、模块和函数应该对扩展开放,对修改关闭。
此时如果需要新增一种水果,比如桃子,那么就得去修改工厂提供的工厂方法了,这样是不太符合开闭原则的。因为工厂实际上是针对于调用方提供的,所以应该尽可能对修改关闭。
工厂方法模式
-
定义工厂接口(也可以是抽象类)
javapublic interface FruitFactory { Fruit getFruit(); }
-
定义水果工厂类
javapublic class AppleFactory implements FruitFactory{ @Override public Apple getFruit() { return new Apple(); } }
javapublic class OrangeFactory implements FruitFactory{ @Override public Orange getFruit() { return new Orange(); } }
-
调用
javapublic class Main { public static void main(String[] args) { // 使用对应工厂创建对应的对象 Apple apple = new AppleFactory().getFruit(); Orange orange = new OrangeFactory().getFruit(); } }
这样就可以使用不同类型的工厂来生产不同类型的水果了,如果新增了水果类型,直接创建一个新的工厂类就行,不需要修改之前已经编写好的内容。
优点 :
(1)一个调用者想创建一个对象,只要知道其名称就可以了。
(2)扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
(3)屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
使用场景 :
(1)日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
(2)数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
(3)设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。
抽象工厂模式
当涉及到产品族的时候,就需要引入抽象工厂模式了。
实际上这些产品都是成族出现的,比如小米的产品线上有小米手机、小米平板等,华为的产品线上也有华为手机、华为平板。如果按照之前的工厂模式来进行设计,那就需要单独设计 9 个工厂来生产上面这些产品,显然这样就比较浪费时间的。
这时可以使用抽象工厂模式,将多个产品都放在一个工厂中进行生成,按不同的产品族进行划分。
比如小米,那么就创建一个小米工厂,这个工厂里面可以生产整条产品线上的内容,包括小米手机、小米平板等。
-
创建产品接口
javapublic interface Pad { void video(); } public interface Phone { void call(); }
-
创建产品类
javapublic class XiaoMiPad implements Pad{ @Override public void video() { System.out.println("小米平板看视频"); } } public class XiaoMiPhone implements Phone{ @Override public void call() { System.out.println("小米手机打电话"); } } public class HuaWeiPad implements Pad{ @Override public void video() { System.out.println("华为平板看视频"); } } public class HuaWeiPhone implements Phone{ @Override public void call() { System.out.println("华为手机打电话"); } }
-
创建抽象工厂
java/** * 抽象工厂 */ public abstract class AbstractFactory { /** * 获取手机 * @return 手机对象 */ public abstract Phone getPhone(); /** * 获取平板 * @return 平板对象 */ public abstract Pad getPad(); }
-
创建产品族工厂类
java/** * 小米工厂 */ public class XiaoMiFactory extends AbstractFactory{ @Override public Phone getPhone() { return new XiaoMiPhone(); } @Override public Pad getPad() { return new XiaoMiPad(); } } /** * 华为工厂 */ public class HuaWeiFactory extends AbstractFactory { @Override public Phone getPhone() { return new HuaWeiPhone(); } @Override public Pad getPad() { return new HuaWeiPad(); } }
-
生产使用
java// 小米手机 Phone xiaomiPhone = new XiaoMiFactory().getPhone(); // 华为手机 Phone huaweiPhone = new HuaWeiFactory().getPhone(); // 小米平板 Pad xiaoMiPad = new XiaoMiFactory().getPad(); // 华为平板 Pad huaweiPad = new HuaWeiFactory().getPad(); // 输出测试 xiaomiPhone.call(); huaweiPhone.call(); xiaoMiPad.video(); huaweiPad.video();
优点:如此即实现了各工厂生产不同产品族的产品并实现对应的功能。一个工厂可以生产同一个产品族的所有产品,这样按族进行分类,显然比之前的工厂模式更好。
缺点:如果产品族新增了产品,那么我就不得不去为每一个产品族的工厂都去添加新产品的生产方法,违背了开闭原则。且这种模式需要使用的类远多于工厂模式,除非是类似这种产品族的业务场景,否则还是更推荐工厂模式实现。
使用场景 :
(1)QQ 换皮肤,一整套一起换。
(2)生成不同操作系统的程序。
单例模式
单例就是只有一个实例对象,即在整个程序中,同一个类始终只有一个对象进行操作。这样可以极大的减少内存开支和系统的性能开销,因此应用十分广泛。比如数据库连接类,实际上只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。
- 为保证只能由自己创建对象,单例类必须构造方法私有化。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
-
饿汉式
在类加载时就创建对象,保证了线程的安全。这种方式比较常用,但容易产生垃圾对象,对空间的消耗较大。
javapublic class Singleton { /** * 单例模式的核心,构造方法私有化 */ private Singleton() { } /** * 用于全局引用的唯一单例对象,在一开始就创建好 */ private final static Singleton INSTANCE = new Singleton(); /** * 获取全局唯一的单例对象 * @return 实例对象 */ public static Singleton getInstance() { return INSTANCE; } }
这种方式由于不存在线程安全问题, 因此不用加锁,效率较高,以空间换时间。
-
懒汉式(双重检测锁)
javapublic class Singleton { private Singleton() { } private volatile static Singleton INSTANCE; public static Singleton getInstance(){ // 外层检测规避多线程情况 if(INSTANCE == null) { // 只对赋值这一步进行加锁,提升效率 synchronized (Singleton.class) { // 内层检测以实现单例 if(INSTANCE == null) { INSTANCE = new Singleton(); } } } return INSTANCE; } }
volatile 关键字来避免指令重排
synchronized 避免线程安全问题
-
静态内部类
双重校验锁模式虽然严谨,但
终究需要加锁
,效率始终会受到影响。那么,有没有一种更好的,不用加锁的方式也能实现延迟加载的写法呢?答案是有的,可以使用静态内部类。
静态内部类实现依赖于 Java 在类加载时不会加载其内部类,只有实例化内部类时才会加载的特性。
其他语言不具备这一特性,无法使用该方式实现,因此这种方式不具备通用性。javapublic class Singleton { private Singleton() { } /** * 由静态内部类持有单例对象 * <p>根据类加载特性,仅使用外部类时,不会对静态内部类进行初始化 */ private static class Holder { private final static Singleton INSTANCE = new Singleton(); } /** * 只有真正使用内部类时,才会进行类初始化 * @return 单例对象 */ public static Singleton getInstance(){ // 直接获取内部类中的对象 return Holder.INSTANCE; } }
这种方式显然是最完美的懒汉式解决方案:没有进行任何的加锁操作,也能保证线程安全,还能实现懒加载。
不过要实现这种写法,跟语言本身也有一定的关联,并不是所有的语言都支持这种写法的。而且它也不是绝对安全的,因为 Java 还有一个十分霸道的东西:反射。
-
反射破坏单例
反射会无视 private 修饰的构造方法,可以直接在外面 newInstance,破坏单例。
javaSingleton singleton1 = Singleton.getInstance(); // 获取无参构造器 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 取消无参构造器私有属性 declaredConstructor.setAccessible(true); // 使用反射获取的无参构造器实例化对象 Singleton singleton2 = declaredConstructor.newInstance(); // 比较实例化的两个对象 System.out.println(singleton1 == singleton2);
输出结果为 false,同一个类实例化的两个对象不相等,单例被破坏了。那么,怎么解决这种问题呢?
可以在构造方法中加上对象的非空判断:
javaprivate Singleton() { synchronized (Singleton.class) { if (INSTANCE != null) { throw new RuntimeException("不要试图用反射破坏单例"); } } }
但是这种写法还是有问题:
前面是先正常的调用了 getInstance 方法,创建了 Singleton 对象,然后第 2 次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。
但如果先用反射创建对象,判断就不生效了:java// 获取无参构造器 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 取消无参构造器私有属性 declaredConstructor.setAccessible(true); // 使用反射获取的无参构造器实例化对象 Singleton singleton1 = declaredConstructor.newInstance(); Singleton singleton2 = declaredConstructor.newInstance(); // 比较实例化的两个对象 System.out.println(singleton1 == singleton2);
输出结果为 false,同一个类实例化的两个对象不相等,单例又被破坏了!还有什么办法防止这种反射破坏呢?
可以使用标志位来避免重复创建:
javaprivate static boolean flag = false; private Singleton() { synchronized (Singleton.class) { if (flag == false) { // 第一次创建后,标志位设为 true flag = true; } else { // 后续再使用构造方法创建则直接报错 throw new RuntimeException("不要试图用反射破坏单例模式"); } } }
但是外界如果知道了flag变量,可以通过反射修改flag值来破坏单例
java// 获取无参构造器 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 取消无参构造器私有属性 declaredConstructor.setAccessible(true); // 使用反射获取的无参构造器实例化对象 Singleton singleton1 = declaredConstructor.newInstance(); // 获取 flag 属性 Field field = Singleton.class.getDeclaredField("flag"); // 取消 flag 属性的私有属性 field.setAccessible(true); // 通过反射,修改属性的值 field.set(singleton1, false); // 使用反射获取的无参构造器实例化对象 Singleton singleton2 = declaredConstructor.newInstance(); // 比较实例化的两个对象 System.out.println(singleton1 == singleton2);
-
枚举实现
javapublic enum Singleton { /** * 单例对象 */ INSTANCE }
带有属性值的单例
javapublic enum Singleton { INSTANCE("KOMOREBI"); private String name; Singleton(String name){ this.name = name; } public void setName(String name) { this.name = name; } public static void main(String[] args) { System.out.println(Singleton.INSTANCE.name); } }
使用反射无法破坏单例模式,反射中 Constructor 的 newInstance() 源码:
java@CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
可以看到,如果类为反射类,就会直接报错:
javaif ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
这就从底层就避免了反射对枚举实现单例的破坏,是最直接也是最有效的手段。所以,枚举实现是最完美的单例模式。
它极致简洁,支持懒加载,自动支持序列化机制,绝对防止多次实例化。它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,避免多次实例化。最让人感动的是,它不会被反射破坏!
-
反序列化破坏单例
从ObjectInputStream.readObject()这个方法为入口
找到readObject0方法中的switch片段,判断反序列化对象类型,此时对象类型是Object
返回值会调用readOrdinaryObject方法,readOrdinaryObject方法中的三目允许算符判断了对象是不是可实例化的,如果是可实例化的会通过newInstance()方法反射实例化一个新的对象,
所以序列化前的对象和反序列化后得到的对象不同!
解决方案及解析
解决方案是在单例类中加一个readResolve方法
javapublic class LazySingleTon implements Serializable { private LazySingleTon(){ } public static LazySingleTon getInstance(){ return InnerClass.lazySingleTon; } private static class InnerClass{ private static LazySingleTon lazySingleTon = new LazySingleTon(); } /** * 解决序列化、反序列化破坏单例 * @return */ public Object readResolve(){ return getInstance(); } }
那为什么加一个readResolve方法就能阻止单例被破坏呢?
在刚才分析的readOrdinaryObject方法有调用hasReadResolveMethod的判断,这个方法是验证目标类是否包含一个方法名为readResolve的方法,如果有就执行desc.invokeReadResolve,通过反射调用单例类的LazySingleTon的readResolve方法,即我们刚才加的readResolve方法,并将获得的对象返回,所以序列化前后对象相同!阻止了单例被破坏
建造者模式
建造者模式主要包含以下四个组成部分:
- 产品(Product):表示最终要构建的复杂对象。
- 抽象建造者(Builder):定义用于构建产品的抽象类,包含创建产品各个部分的方法。
- 具体建造者(Concrete Builder):实现Builder接口,完成产品各个部分的具体构建工作。
- 指挥者(Director):负责调用合适的建造者方法,按照特定顺序组装产品。
普通实现(无指挥者)
模拟点餐场景
-
产品
java/** * 快餐食品(产品) */ public class Product { /** * 快餐 A 默认为汉堡 */ private String BuildA = "汉堡"; private String BuildB = "可乐"; private String BuildC = "薯条"; private String BuildD = "甜点"; public String getBuildA() { return BuildA; } public void setBuildA(String buildA) { BuildA = buildA; } public String getBuildB() { return BuildB; } public void setBuildB(String buildB) { BuildB = buildB; } public String getBuildC() { return BuildC; } public void setBuildC(String buildC) { BuildC = buildC; } public String getBuildD() { return BuildD; } public void setBuildD(String buildD) { BuildD = buildD; } @Override public String toString() { return "Product{" + "BuildA='" + BuildA + '\'' + ", BuildB='" + BuildB + '\'' + ", BuildC='" + BuildC + '\'' + ", BuildD='" + BuildD + '\'' + '}'; } }
-
建造者(接口-厨房)
java/** * 厨房(建造者) */ public abstract class Kitchen { /** * 制作快餐 A * @param msg 快餐名称 * @return 快餐 */ abstract Kitchen builderA(String msg); abstract Kitchen builderB(String msg); abstract Kitchen builderC(String msg); abstract Kitchen builderD(String msg); /** * 获取产品 * @return 产品 */ abstract Product getProduct(); }
-
具体建造者(服务员)
java/** * 服务员(传递者) */ public class Waiter extends Kitchen { private Product product; public Waiter(){ product = new Product(); } @Override Kitchen builderA(String msg) { product.setBuildA(msg); return this; } @Override Kitchen builderB(String msg) { product.setBuildB(msg); return this; } @Override Kitchen builderC(String msg) { product.setBuildC(msg); return this; } @Override Kitchen builderD(String msg) { product.setBuildD(msg); return this; } @Override Product getProduct() { return product; } }
建造的的核心在于每次builder后都会返回this,所以可以使用链式调用法则。
-
测试(点餐)
java// 叫服务员 Waiter waiter = new Waiter(); // 可以选择套餐,省事,直接告诉服务员要套餐即可 Product product1 = waiter.getProduct(); System.out.println(product1); // 也可以自己点餐,点了哪些上哪些 Product product2 = waiter .builderA("炸鸡") .builderB("雪碧") .builderC(null) .builderD(null) .getProduct(); System.out.println(product2);
指挥者实现
有时对于建造的顺序也是有要求的,例如:在工地建筑时,除了建造本身,建造的顺序也非常重要,因此工地上一般都会有一个指挥者来决定建造的顺序。
-
定义产品(楼)
java/** * 一栋楼(产品) */ public class Product { /** * 地基 */ private String productA; /** * 主体 */ private String productB; /** * 粉刷 */ private String productC; /** * 绿化 */ private String productD; public String getProductA() { return productA; } public void setProductA(String productA) { this.productA = productA; } public String getProductB() { return productB; } public void setProductB(String productB) { this.productB = productB; } public String getProductC() { return productC; } public void setProductC(String productC) { this.productC = productC; } public String getProductD() { return productD; } public void setProductD(String productD) { this.productD = productD; } @Override public String toString() { return "Product{" + "productA='" + productA + '\'' + ", productB='" + productB + '\'' + ", productC='" + productC + '\'' + ", productD='" + productD + '\'' + '}'; } }
-
抽象建造者
java/** * 包工头(抽象建造者) * @author yifan */ public abstract class Builder { /** * 打地基 */ abstract void buildA(); /** * 建主体 */ abstract void buildB(); /** * 去粉刷 */ abstract void buildC(); /** * 搞绿化 */ abstract void buildD(); /** * 建一栋楼 * @return 一栋楼 */ abstract Product getProduct(); }
-
具体建造者
java/** * 工人(实际建造者) */ public class Worker extends Builder{ private Product product; public Worker() { // 指定要建设的楼 product = new Product(); } @Override void buildA() { product.setProductA("地基"); System.out.println("地基"); } @Override void buildB() { product.setProductB("主体"); System.out.println("主体"); } @Override void buildC() { product.setProductC("粉刷"); System.out.println("粉刷"); } @Override void buildD() { product.setProductD("绿化"); System.out.println("绿化"); } @Override Product getProduct() { return product; } }
-
指挥者定义(施工调度员)
java/** * 施工调度员(指挥者) */ public class Director { /** * 指挥包工头按照顺序建楼 * @param builder 包工头 * @return 楼 */ public Product build(Builder builder){ builder.buildA(); builder.buildB(); builder.buildC(); builder.buildD(); return builder.getProduct(); } }
-
测试
java// 施工调度员指挥包工头,包工头找到具体的工人按照施工调度员指定的顺序建造 new Director().build(new Worker());
这样就用代码实现了工地上各岗位的协作,如果工程需要调整建造顺序,只需要更改指挥者的 build 方法即可,非常方便。
设计模式的思想起源于建筑行业,从建造者模式这里就能体现得淋漓尽致。
优点
1、建造者独立,易扩展。
2、便于控制细节风险。
缺点
1、产品必须有共同点,范围有限制。
2、如内部变化复杂,会有很多的建造类。