一、前言:设计模式可以帮助大家在工作中设计住高层次抽象的代码结构,根据用途可以分为创建型模式和结构型模式和和行为型模式。
二、原则:设计模式也有自己的设计原则,基于这个原则可以帮助我们去写出我们需要的修改可闭和扩展可开的代码结构;
1:面向接口编程,不是面向实现编程,帮助我们设计出优雅的可扩展的代码结构;
2:职责单一原则,每个类都应该有单一的功能,并且这功能被这个类封装起来;
3:对修改可关闭,对扩展可开放,修改可关闭:两种含义可能比较矛盾,一种就是在修改的时候不影响其他的类,一种就是不能随意的修改;扩展可开放:指的是可以扩展新的功能;
三、先介绍创建型设计模式中的比较常见的工厂模式和单例模式
工厂模式
1、简单工厂
public class FoodFactory { public static Food makeFood(String name){ if(name.equals("noodle")){ return new LanZhouNoodle(); }else if (name.equals("huangMenChicken")){ return new HuangMenChicken(); }else{ return null; } } }
LanZhouNoodle和HuangMenChicken分别是不同的食物,都继承Food.
简单工厂很简单,一个XXXFactory,一个静态方法,根据传入参数创建不同的bean实例,简单工厂模式遵循了职责单一原则,一个类只封装了一个功能,比如FoodFactory只是用来生产食物。
2、工厂模式
如果当我们需要多个工厂的时候,我们需要引入工厂模式
public interface FoodFactory { public Food makeFood(String name); } public class AmericanFoodFactory implements FoodFactory { public Food makeFood(String name){ if(name.equals("americanFoodA")){ return new AmericanFoodA(); }else if (name.equals("americanFoodB")){ return new AmericanFoodB(); }else{ return null; } } } public class ChineseFoodFactory implements FoodFactory { public Food makeFood(String name){ if(name.equals("chineseFoodA")){ return new ChineseFoodA(); }else if (name.equals("ChineseFoodB")){ return new ChineseFoodB(); }else{ return null; } } }
其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。
工厂模式核心在于,第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。
3、抽象工厂模式
当涉及到产品族的时候,就需要引入抽象工厂模式了。
// 得到 Intel 的 CPU CPUFactory cpuFactory = new IntelCPUFactory(); CPU cpu = intelCPUFactory.makeCPU(); // 得到 AMD 的主板 MainBoardFactory mainBoardFactory = new AmdMainBoardFactory(); MainBoard mainBoard = mainBoardFactory.make(); // 组装 CPU 和主板 Computer computer = new Computer(cpu, mainBoard);
单独看cpu工厂和主板工厂,满足上面讲到的工厂模式,也容易扩展,如果要给电脑加硬盘,只需要增加一个硬盘工厂和实现即可,不需要修改现有的工厂。
但是这样设计就会出现问题,比如cpu和主板是不同厂家生产,就可能会出现兼容性问题,而且客户端生产代码时也不知道他们是否兼容。
下面就要从产品族的角度考虑了,它代表了某个产品的一系列的零件的组合,不会出现不兼容的情况。
当从产品族设计代码的时候就需要引入抽象工厂的概念了,也就是从电脑工厂开始创建,每个电脑工厂负责生产所有的设备,这样就不会出现兼容性问题了。
public static void main(String[] args) { // 第一步就要选定一个“大厂” ComputerFactory cf = new AmdFactory(); // 从这个大厂造 CPU CPU cpu = cf.makeCPU(); // 从这个大厂造主板 MainBoard board = cf.makeMainBoard(); // 从这个大厂造硬盘 HardDisk hardDisk = cf.makeHardDisk(); // 将同一个厂子出来的 CPU、主板、硬盘组装在一起 Computer result = new Computer(cpu, board, hardDisk); }
当然单从工厂模式考虑,抽象工厂模式对修改关闭,对扩展开放的原则,比如我们需要给intel工厂增加一个显示器方法,那么amd工厂也要增加。不过也是有解决办法的,我们可以引入代理模式,或者装饰者模式,创建代理类或者装饰类来增加新的功能,这样就不用修改其它工厂了。
单例模式
1、饿汉模式
public class Singleton { //将new instance堵死,避免在其它地方new实例 private Singleton(){}; //创建私有静态实例,意味着这个类第一次调用的时候就创建 private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...), // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了 public static Date getDate(String mode) {return new Date();} }
很多人觉得饿汉模式在类初始化时就会创建实例,会占用内存,其实在实际生产过程中,我们不会随便定义一个不会用到的类,
更不会在一个不会用到的类中增加一些额外的方法。
2、懒汉模式
public class Singleton2 { //构造方法设置私有,将new Singleton()堵死 private Singleton2(){}; //和饿汉模式相比,这里不需要提前new 实例,需要主要volatile关键字 private static volatile Singleton2 instance = null; public static Singleton2 getInstance(){ if (instance == null){ //加锁 synchronized(Singleton2.class){ //这一次判断可以避免并发问题 if (instance == null){ instance = new Singleton2(); } } } return instance; } }
双重检查,指的是两次检查 instance 是否为 null。
volatile 关键字保证变量在多个线程之间是可见的。
加锁最简单的是直接就在 getInstance() 方法签名上加上 synchronized,但是性能太差。
3、嵌套类+饿汉模式实现单例
有的人嵌套类称嵌套类为静态内部类,但是不能和内部类混淆,因为它们访问外部类的变量和方法的权限是不一样的。
public class Singleton3 { //堵死new Singleton3 private Singleton3(){}; //主要利用了嵌套类可以访问外部类的属性或者方法的特性 private static class Holder { private static Singleton3 instance = new Singleton3(); } public static Singleton3 getInstance(){ return Holder.instance; } }
4、枚举类实现单例在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。不建议使用
四、介绍结构型模式中的代理模式,适配器模式和装饰模式
代理模式
用一个代理类来隐藏具体实现类的实现细节,通常还用于在真实实现的前后增加逻辑。
既然说是代理,就是对客户端隐藏实现细节,由代理来处理客户端的所有请求,代理只是代理,不会完成实际的业务逻辑,只是一层包装而已,但是要表现的就是客户端需要的真实实现。
/** * 真实的实现 */ public class FoodServiceImpl implements FoodService { public Food makeNoodle() { return null; } public Food makeChicken() { return null; } }
//代理对于客户端而言就要表现的是真实的实现类,需要实现service public class FoodServiceProxy implements FoodService { //注入真实的实现类 private FoodService foodService = new FoodServiceImpl(); //一层皮将真实实现包装起来 public Food makeNoodle() { //来调用真实的实现逻辑 return foodService.makeNoodle(); } public Food makeChicken() { return foodService.makeChicken(); }
}
FoodService foodService = new FoodServiceProxy();
foodService.makeChicken();
可见代理模式就是方法包装和方法增强,在spring 的Aop面向切面编程中使用的就是代理模式,我们不用自己定义代理类,spring会帮我们动态生成代理类,在代理类的前后会增加@Before,@After,@Around等逻辑。
spring实现动态代理有两种,一种是又接口和实现,采用的是jdk(java.lang.reflect.Proxy)的动态代理,一种是仅有接口spring会采用GGLB动态生成代理,GGLB是一个jar包,性能还是不错的。
适配器模式
代理模式和适配器模式类似,适配器就是我们需要一个接口的实现,但是现有的对象(实现并不满足),我们需要定义一个适配来适配。
适配器模式有默认适配器模式,对象适配器模式和类适配器模式。
1、默认适配器模式
Apache Commons-Io的FileAlterationListener接口中有很多抽象方法,用于监听文件的创建和修改,一旦事件触发则触发相应的事件。
public interface FileAlterationListener { void onStart(final FileAlterationObserver observer); void onDirectoryCreate(final File directory); void onDirectoryChange(final File directory); void onDirectoryDelete(final File directory); void onFileCreate(final File file); void onFileChange(final File file); void onFileDelete(final File file); void onStop(final FileAlterationObserver observer); }
这个接口抽象方法过多,如果我们只需要实现里面的部分方法,那么需要做一层适配。
public class FileAlterationListenerAdaptor implements FileAlterationListener { public void onStart(final FileAlterationObserver observer) { } public void onDirectoryCreate(final File directory) { } public void onDirectoryChange(final File directory) { } public void onDirectoryDelete(final File directory) { } public void onFileCreate(final File file) { } public void onFileChange(final File file) { } public void onFileDelete(final File file) { } public void onStop(final FileAlterationObserver observer) { } }
可以通过如下定义一个适配器,仅仅实现我们需要的方法
public class FileMonitor extends FileAlterationListenerAdaptor { public void onFileCreate(final File file) { // 文件创建 doSomething(); } public void onFileDelete(final File file) { // 文件删除 doSomething(); } }
2、对象适配器模式
public interface Cock { public void gobble(); // 鸡的咕咕叫 public void fly(); // 飞 }
public interface Duck { public void quack(); // 鸭的呱呱叫 public void fly(); // 飞 }
public class WildCock implements Cock {
public void gobble() { System.out.println("咕咕叫"); } public void fly() { System.out.println("鸡也会飞哦"); } }
鸡有鸭的fly()方法,但没有鸭的gobble()方法,如果需要让鸡咕咕叫,那么需要适配
//适配器需要实现鸭才能实现呱呱叫的方法 public class CockAdaptor implements Duck { private Cock cock; //需要传入鸡,才能将鸡进行适配成鸭 public CockAdaptor(Cock cock) { this.cock = cock; } //外面实现鸭的呱呱叫 public void quack() { //内部使用鸡的咕咕叫 cock.gobble(); } public void fly() { } }
客户端有一只野鸡需要适配成鸭的呱呱叫
public static void main(String[] args) { // 有一只野鸡 Cock wildCock = new WildCock(); // 成功将野鸡适配成鸭 Duck duck = new CockAdapter(wildCock); ... }
对象适配器模式,就是我们有一只鸡需要让它呱呱叫,那么需要一个适配器,来讲鸡适配成鸭
3、类适配器模式
public interface Target { public void method1(); public void method2(); public void method3(); }
public class SomeThing { public void method1(){ } public void method2(){ } }
public class SomeAdaptor extends SomeThing implements Target { //通过继承自动获得了SomeThing中的method1和method2方法 public void method3() { } }
通过Target target = new SomeAdaptor();调用method方法。
适配器模式总结
1、类适配器和对象适配器模式的区别
类适配器采用的继承,对象适配器采用的包装。
类适配器是静态实现,对象适配器是动态实现,动态传入需要适配的对象(鸡),对象适配器比类多实例化一个对象。
2、适配器模式和代理模式的区别
代理模式是增强原有的方法(SpringAop),适配器模式是进项适配,比如将鸡包装成鸭,当做鸭使用。
装饰模式
从图中可以看到接口Component由两个实现类,ComponentA和ComponentB,如果要增强两个实现类,可以用装饰模式,用具体的装饰器来装饰实现类,实现功能增强的作用。
装饰,顾名思义,增加小功能,而且我们要满足可以添加多个小功能,有人说使用代理的模式,代理不容易实现增加多个功能,如果实现增加多个功能,可以代理再代理,但代码就复杂了。
从图中可以看出所有的装饰者们也都通过继承装饰基类间接的实现了component中的方法,他们和ComponentA和ComponentB的区别是他们只是装饰者。
下面再看看一个例子:
我们把快乐柠檬的饮料分为三类:红茶、绿茶、咖啡,在这三大类的基础上,又增加了许多的口味,什么金桔柠檬红茶、金桔柠檬珍珠绿茶、芒果红茶、芒果绿茶、芒果珍珠红茶、烤珍珠红茶、烤珍珠芒果绿茶、椰香胚芽咖啡、焦糖可可咖啡等等,每家店都有很长的菜单,但是仔细看下,其实原料也没几样,但是可以搭配出很多组合,如果顾客需要,很多没出现在菜单中的饮料他们也是可以做的。
在这个例子中,红茶、绿茶、咖啡是最基础的饮料,其他的像金桔柠檬、芒果、珍珠、椰果、焦糖等都属于装饰用的。当然,在开发中,我们确实可以像门店一样,开发这些类:LemonBlackTea、LemonGreenTea、MangoBlackTea、MangoLemonGreenTea......但是,很快我们就发现,这样子干肯定是不行的,这会导致我们需要组合出所有的可能,而且如果客人需要在红茶中加双份柠檬怎么办?三份柠檬怎么办?万一有个变态要四份柠檬,所以这种做法是给自己找加班的。
用代码实现
首先定义抽象基类Component类:
public abstract class Beverage { // 返回描述 public abstract String getDescription(); // 返回价格 public abstract double cost(); }
定义红茶,绿茶等茶品(ComponnetA和ComponentB)
public class GreenTea extends Beverage { public String getDescription() { return "绿茶"; } public double cost() { return 11; } }
public class BlackTea extends Beverage { public String getDescription() { return "红茶"; } public double cost() { return 10; } }
定义调料类,装饰基类
// 调料 public abstract class Condiment extends Beverage { }
定义柠檬芒果等调料,它们属于装饰者肯定要继承Componnet抽象基类
public class Lemon extends Condiment { private Beverage bevarage; // 这里很关键,需要传入具体的饮料,如需要传入没有被装饰的红茶或绿茶, // 当然也可以传入已经装饰好的芒果绿茶,这样可以做芒果柠檬绿茶 public Lemon(Beverage bevarage) { this.bevarage = bevarage; } public String getDescription() { // 装饰 return bevarage.getDescription() + ", 加柠檬"; } public double cost() { // 装饰 return bevarage.cost() + 2; // 加柠檬需要 2 元 }
public class Mango extends Condiment { private Beverage bevarage; public Mango(Beverage bevarage) { this.bevarage = bevarage; } public String getDescription() { return bevarage.getDescription() + ", 加芒果"; } public double cost() { return bevarage.cost() + 3; // 加芒果需要 3 元 } }
客户端调用
public static void main(String[] args) { // 首先,我们需要一个基础饮料,红茶、绿茶或咖啡 Beverage beverage = new GreenTea(); // 开始装饰 beverage = new Lemon(beverage); // 先加一份柠檬 beverage = new Mongo(beverage); // 再加一份芒果 System.out.println(beverage.getDescription() + " 价格:¥" + beverage.cost()); //"绿茶, 加柠檬, 加芒果 价格:¥16" }
再来说说java IO中的装饰者模式
InputStream相当于抽象基类Componnet,FileInputStream和ByteArrayInputStream和PipedInputStream相当于基础输入流,红茶绿茶等,FilterInputStream是装饰基类继承了InputStream,BufferedInputStream和DataInputStream和LineNumInputStream继承了装饰基类,属于一个个装饰者。
当然在java IO中就不适用于与面向接口编程了,
InputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));
这样的inputStream还是不能读取行号不具备缓冲流的作用,因为读取行号的功能在LineInputStream中。应该这样使用
LineNumberInputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));
观察者订阅需要观察的主题,已经主题中有改动时通知观察者;
首先定义主题,每个主题需要持有观察者列表的引用,用于在数据变更时通知观察者。
public class Subject { private List<Observer> observers = new ArrayList<Observer>(); private int state; public int getState() { return state; } public void setState(int state) { this.state = state; // 数据已变更,通知观察者们 notifyAllObservers(); } public void attach(Observer observer){ observers.add(observer); } // 通知观察者们 public void notifyAllObservers(){ for (Observer observer : observers) { observer.update(); } } }
public abstract class Observer { protected Subject subject; public abstract void update(); }
如果只有一个观察者就不用定义观察者了,但是实际当中,当事件发生时,我们需要短信通知,邮件通知等等,还是需要多个观察者的。
定义两个具体的观察者类:
public class HexaObserver extends Observer { public HexaObserver(Subject subject) { this.subject = subject; this.subject.attach(this); } @Override public void update() { String result = Integer.toHexString(subject.getState()).toUpperCase(); System.out.println("订阅的数据发生变化,新的数据处理为十六进制值为:" + result); } }
public class BinaryObserver extends Observer { // 在构造方法中进行订阅主题 public BinaryObserver(Subject subject) { this.subject = subject; // 通常在构造方法中将 this 发布出去的操作一定要小心 this.subject.attach(this); } // 该方法由主题类在数据变更的时候进行调用 @Override public void update() { String result = Integer.toBinaryString(subject.getState()); System.out.println("订阅的数据发生变化,新的数据处理为二进制值为:" + result); } }
客户端调用:
public static void main(String[] args) { // 先定义一个主题 Subject subject1 = new Subject(); // 定义观察者 new BinaryObserver(subject1); new HexaObserver(subject1); // 模拟数据变更,这个时候,观察者们的 update 方法将会被调用 subject.setState(11); }
output:
订阅的数据发生变化,新的数据处理为二进制值为:1011
订阅的数据发生变化,新的数据处理为十六进制值为:B
jdk 也提供了相似的支持,具体的可以参考 java.util.Observable 和 java.util.Observer 这两个类。
观察者模式一般适用于中间件消息队列的实现。
模板模式
模板模式用于继承结构中需要继承模板中的模板方法;
抽象模板类
public abstract class AbstractTemplate { // 这就是模板方法 public void templateMethod(){ init(); apply(); // 这个是重点 end(); // 可以作为钩子方法 } protected void init() { System.out.println("init 抽象层已经实现,子类也可以选择覆写"); } // 留给子类实现 protected abstract void apply(); protected void end() { } }
模板方法中有三个方法,init()和apply()和end(),init方法已经实现。子类继承init方法并且自己实现apply和end方法
public class ConcreteTemplate extends AbstractTemplate { public void apply() { System.out.println("子类实现抽象方法 apply"); } public void end() { System.out.println("我们可以把 method3 当做钩子方法来使用,需要的时候覆写就可以了"); } }
客户端调用模板方法
public static void main(String[] args) { AbstractTemplate t = new ConcreteTemplate(); // 调用模板方法 t.templateMethod(); }