文章目录
前言
设计模式本质是面向对象设计原则的实际运用,是对类的封装性、继承性、多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下特点:
- 可以提高程序员的思维能力、编程能力和设计能力。
- 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性、维护性强。
遵循一些重要的设计原则。
- 面向接口编程,而不是面向实现。这个很重要,也是优雅的、可扩展的代码的第一步。
- 职责单一原则。每个类都应该只有一个单一的功能,并且该功能应该由这个类完全封装起来。
- 对修改关闭,对扩展开放。对修改关闭是说,我们辛辛苦苦加班写出来的代码,该实现的功能和该修复的 bug 都完成了,别人可不能说改就改;对扩展开放就比较好理解了,也就是说在我们写好的代码基础上,很容易实现扩展。
OCP(开闭原则,Open-Closed Principle):一个软件的实体应当对扩展开 放,对修改关闭。
DIP(依赖倒转原则,Dependence Inversion Principle):要针对接口编程, 不要针对实现编程。
LoD(迪米特法则,Law of Demeter):只与你直接的朋友通信,而避免和 陌生人通信。
设计模式GOF23分为三大类,分别为
- 创建型模式(单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式)。
- 结构型模式(适配器模式、桥接模式、装饰模式、组合模式、外观模式、亨元模式、代理模式)。
- 行为型模式(模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式)。
一.创建型模式
创建型模式的作用就是创建对象。
单例模式
核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
饿汉式(线程安全,调用效率高。 但是,不能延时加载。)
public class SingletonDemo02 {
private static SingletonDemo02 s = new SingletonDemo02();
private SingletonDemo02(){} //私有化构造器
public static SingletonDemo02 getInstance(){ return s;
}
//=================================================
public static void main(String[] args) {
SingletonDemo02 s = SingletonDemo02.getInstance();
SingletonDemo02 s2 = SingletonDemo02.getInstance();
System.out.println(s==s2); //结果为true
}
}
- static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问 题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。
- 问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!
懒汉式实现(线程安全,调用效率不高。 但是,可以延时加载。)
public class SingletonDemo01 {
private static SingletonDemo01 s;
private SingletonDemo01(){} //私有化构造器
public static synchronized SingletonDemo01 getInstance(){
if(s==null){
s = new SingletonDemo01();
}
return s;
}
}
• 要点: lazy load! 延迟加载, 懒加载! 真正用的时候才加载!
• 问题: 资源利用率高了。但是,每次调用getInstance()方法都要同步,并发 效率较低。
双重检测锁(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)
//双重检测锁 只有第一次才同步 创建
public class Singleton {
// 首先,也是先堵死 new Singleton() 这条路
private Singleton() {}
// 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
// 加锁
synchronized (Singleton.class) {
// 这一次判断也是必须的,不然会有并发问题
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 优点:提高了执行的效率 不必每次获取对象时都进行同步,只有第一次才同步 创建了以后就没必要了。
- 问题: 由于编译器优化原因和JVM底层内部模型原因, 偶尔会出问题。不建议使用
public class Singleton3 {
private Singleton3() {}
// 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
private static class Holder {
private static final Singleton3 instance = new Singleton3();
}
public static Singleton3 getInstance() {
return Holder.instance;
}
}
- 外部类没有static属性,则不会像饿汉式那样立即加载对象。
- 只有真正调用getInstance(),才会加载静态内部类。加载类时是线程 安全的。 instance是static final 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.
- 兼备了并发高效调用和延迟加载的优势!
上面几种也存在以下问题:反射可以破解,反序列化可以破解
解决反射存在问题可以在构造方法中手动 抛出异常控制
private SingletonDemo01() throws Exception{ if(s!=null){ throw new Exception(“只能创建一个对象”); //通过手动抛出异常,避免通过反射创建多个单例对象! } } //私有化构造器
解决反序列化问题,如果对象所在类定义了readResolve(),(实际是一种回调), 定义返回哪个对象。
枚举实现单例模式//反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象。 private Object readResolve() throws ObjectStreamException { return s; }
public enum SingletonDemo05 {
/** * 定义一个枚举的元素,它就代表了Singleton的一个实例。 */
INSTANCE;
/*** 单例可以有自己的操作 */
public void singletonOperation(){
//功能处理
}
public static void main(String[] args) {
SingletonDemo05 sd = SingletonDemo05.INSTANCE;
SingletonDemo05 sd2 = SingletonDemo05.INSTANCE;
System.out.println(sd==sd2); //true
}
}
- 优点:
实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞! - 缺点:
无延迟加载
单例模式如何选用:
- 单例对象 占用 资源 少,不需要 延时加载:
枚举式 好于 饿汉式- 单例对象 占用 资源 大,
需要 延时加载: 静态内部类式 好于 懒汉式
工厂模式
核心本质:
1. 实例化对象,用工厂方法代替new操作。
2. 将选择实现类、创建对象统一管理和控制。从而将调用者跟我们的实 现类解耦。
public class FoodFactory {
public static Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
工厂模式(使用工厂模式,是因为我们往往需要使用两个或两个以上的工厂。完全满足OCP,即它有非常良好的扩展性)
public interface FoodFactory {
Food makeFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new ChineseFoodA();
} else if (name.equals("B")) {
return new ChineseFoodB();
} else {
return null;
}
}
}
public class AmericanFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new AmericanFoodA();
} else if (name.equals("B")) {
return new AmericanFoodB();
} else {
return null;
}
}
}
其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。
客户端调用:
public class APP {
public static void main(String[] args) {
// 先选择一个具体的工厂
FoodFactory factory = new ChineseFoodFactory();
// 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
Food food = factory.makeFood("A");
}
}
虽然都是调用 makeFood(“A”) 制作 A 类食物,但是,不同的工厂生产出来的完全不一样。
第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。
核心在于,我们需要在第一步选好我们需要的工厂。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。
抽象工厂模式(不可以增加产品,可以增加产品族!)
类关联关系结构图如下:
这里做不同披萨的材料不同,将不同方法抽象出来,实现类去扩展
//披萨制作方法
public abstract class Pizza {
private String name;
public abstract void prepare();
public void bake() {
System.out.println(name+"baking");
}
public void cut() {
System.out.println(name+"cuting");
}
public void box() {
System.out.println(name+"boxing");
}
public void setName(String name){
this.name=name;
}
}
public class BJCheesePizza extends Pizza {
@Override
public void prepare() {
setName("北京的奶酪pizza");
System.out.println("北京的奶酪pizza 准备原材料");
}
}
public class BJPepperPizza extends Pizza {
@Override
public void prepare() {
setName("北京胡椒pizza");
System.out.println("北京胡椒pizza 准备原材料");
}
}
public class LDCheesePizza extends Pizza {
@Override
public void prepare() {
setName("伦敦奶酪pizza");
System.out.println("伦敦奶酪pizza 准备原材料");
}
}
public class LDPepperPizza extends Pizza {
@Override
public void prepare() {
setName("伦敦胡椒pizza");
System.out.println("伦敦胡椒pizza 准备原材料");
}
}
工厂子类:
//工厂子类 北京披萨工厂
public class BJFactory implements AbsFactory{
@Override
public Pizza createPizza(String orderType) {
Pizza pizza=null;
if (orderType.equals("cheese")){
pizza=new BJCheesePizza();
}else if (orderType.equals("pepper")){
pizza=new BJPepperPizza();
}
return pizza;
}
}
//工厂子类 伦敦披萨工厂
public class LDFactory implements AbsFactory{
@Override
public Pizza createPizza(String orderType) {
Pizza pizza=null;
if (orderType.equals("cheese")){
pizza=new LDCheesePizza();
}else if (orderType.equals("pepper")){
pizza=new LDPepperPizza();
}
return pizza;
}
}
调用方:
//订单披萨
public class OrderPizza {
AbsFactory factory;
public OrderPizza(AbsFactory factory){
setFactory(factory);
}
private void setFactory(AbsFactory factory){
Pizza pizza=null;
String orderType="";
this.factory=factory;
do {
orderType=getType();
pizza=factory.createPizza(orderType);
if (pizza!=null){
//生产披萨的方法
}else {
System.out.println("订购失败");
break;
}
}while (true);
}
//客户端希望订购的披萨种类
private String getType(){
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("输入披萨种类:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
//调用 传入工厂
public static void main(String[] args) {
new OrderPizza(new LDFactory());
}
}
- 用来生产不同产品族的全部产品。(对于增加新的产品,无能为力; 支持增加产品族)
- 抽象工厂可以将简单工厂和工厂模式进行整合,在有多个业务品种、业务 分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。
建造模式
本质:我们要建造一个复杂的产品。比如:神州飞船,Iphone。这个复杂的产品的创建。有这样 一个问题需要处理, 装配这些子组件是不是有个步骤问题? 实际开发中,我们所需要的对象构建时,也非常复杂,有很多步骤需要处理时。
开发中应用场景如:StringBuilder类的append方法,SQL中的PreparedStatement,JDOM中,DomBuilder、SAXBuilder
来一个中规中矩的建造者模式:
class User {
// 下面是“一堆”的属性
private String name;
private String password;
private String nickName;
private int age;
// 构造方法私有化,不然客户端就会直接调用构造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
// 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一样的一堆属性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 链式调用设置各个属性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
// 当然,可以在 “复制” 之前做点检验
public User build() {
if (name == null || password == null) {
throw new RuntimeException("用户名和密码必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年龄不合法");
}
// 还可以做赋予”默认值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
核心是:先把所有的属性都设置给 Builder,然后 build() 方法的时候,将这些属性复制给实际产生的对象。
看看客户端的调用:
public class APP {
public static void main(String[] args) {
User d = User.builder()
.name("foo")
.password("pAss12345")
.age(25)
.build();
}
}
说实话,建造者模式的链式写法很吸引人,但是,多写了很多“无用”的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。
原型模式
- 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 就是java中的克隆技术,以某个对象为原型,复制出新的对象。显然,新的对象具备原型对象的特点
- 优势有:效率高(直接克隆,避免了重新执行构造过程步骤) 。
- 克隆类似于new,但是不同于new。new创建新的对象属性采用的是默认值。克隆出的 对象的属性值完全和原型对象相同。并且克隆出的新对象改变不会影响原型对象。然后, 再修改克隆对象的值。
原型模式实现:
- Cloneable接口和clone方法
- Prototype模式中实现起来最困难的地方就是内存复制操作,所幸在Java中提供了 clone()方法替我们做了绝大部分事情。
- 浅克隆存在的问题
– 被复制的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都 仍然指向原来的对象。 - 深克隆如何实现?
– 深克隆把引用的变量指向复制过的新对象,而不是原有的被引用的对象。
– 深克隆:让已实现Clonable接口的类中的属性也实现Clonable接口 – 基本数据类型和String能够自动实现深度克隆(值的复制) - 开发中的应用场景 – 原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone 的方法创建一个对象,然后由工厂方法提供给调用者。
- spring中bean的创建实际就是两种:单例模式和原型模式。(当然,原型 模式需要和工厂模式搭配起来)
创建型模式总结
创建型模式:都是用来帮助我们创建对象的!
- – 单例模式: 保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
- – 工厂模式
- 简单工厂模式 :用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
- 工厂方法模式 –:用来生产同一等级结构中的固定产品。(支持增加任意产品)
- 抽象工厂模式 : 用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持增加产品族)
- –建造者模式 :分离了对象子组件的单独构造(由Builder来负责)和装配(由Director负责)。 从而可 以构造出复杂的对象。
- –原型模式 : 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
二.结构型模式
核心作用:是从程序的结构上实现松耦合,从而可以扩大整体的类结 构,用来解决更大的问题。
代理模式
核心作用: • 通过代理,控制对对象的访问! 可以详细控制访问某个(某类)对象的方法,在调用这个方法前做前置处理,调用这个方法后 做后置处理。(即:AOP的微观实现!)
核心角色:
• 抽象角色
– 定义代理角色和真实角色的公共对外方法
• 真实角色
– 实现抽象角色,定义真实角色所要实现的业务逻辑, 供代理角色调用。
– 关注真正的业务逻辑!
• 代理角色
– 实现抽象角色,是真实角色的代理,通过真实角色 的业务逻辑方法来实现抽象方法,并可以附加 自己的操作。
分类:
– 静态代理(静态定义代理类)
– 动态代理(动态生成代理类)
• JDK自带的动态代理
• javaassist字节码操作库实现
• CGLIB
• ASM(底层使用指令,可维护性较差)
public interface FoodService {
Food makeChicken();
Food makeNoodle();
}
public class FoodServiceImpl implements FoodService {
public Food makeChicken() {
Food f = new Chicken()
f.setChicken("1kg");
f.setSpicy("1g");
f.setSalt("3g");
return f;
}
public Food makeNoodle() {
Food f = new Noodle();
f.setNoodle("500g");
f.setSalt("5g");
return f;
}
}
// 代理要表现得“就像是”真实实现类,所以需要实现 FoodService
public class FoodServiceProxy implements FoodService {
// 内部一定要有一个真实的实现类,当然也可以通过构造方法注入
private FoodService foodService = new FoodServiceImpl();
public Food makeChicken() {
System.out.println("我们马上要开始制作鸡肉了");
// 如果我们定义这句为核心代码的话,那么,核心代码是真实实现类做的,
// 代理只是在核心代码前后做些“无足轻重”的事情
Food food = foodService.makeChicken();
System.out.println("鸡肉制作完成啦,加点胡椒粉"); // 增强
food.addCondiment("pepper");
return food;
}
public Food makeNoodle() {
System.out.println("准备制作拉面~");
Food food = foodService.makeNoodle();
System.out.println("制作完成啦")
return food;
}
}
客户端调用,注意,我们要用代理来实例化接口:
// 这里用代理类来实例化
FoodService foodService = new FoodServiceProxy();
foodService.makeChicken();
代理模式说白了就是做 “方法包装” 或做 “方法增强”。在面向切面编程中,其实就是动态代理的过程。比如 Spring 中,我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理,然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。
开发框架中应用场景:
– struts2中拦截器的实现
– 数据库连接池关闭处理
– Hibernate中延时加载的实现
– mybatis中实现拦截器插件
– AspectJ的实现
– spring中AOP的实现
• 日志拦截
• 声明式事务处理
– web service – RMI远程方法调用
适配器Adapter模式
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原 本由于接口不兼容而不能一起工作的那些类可以在一起工作。
应用场景:java.io.InputStreamReader(InputStream),java.io.OutputStreamWriter(OutputStream)
模式中的角色
– 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象 的类,也可以是接口。
– 需要适配的类(Adaptee):需要适配的类或适配者类。
– 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成 目标接口。
对象适配器模式
来看一个《Head First 设计模式》中的一个例子,我稍微修改了一下,看看怎么将鸡适配成鸭,这样鸡也能当鸭来用。因为,现在鸭这个接口,我们没有合适的实现类可以用,所以需要适配器。
public interface Duck {
public void quack(); // 鸭的呱呱叫
public void fly(); // 飞
}
public interface Cock {
public void gobble(); // 鸡的咕咕叫
public void fly(); // 飞
}
public class WildCock implements Cock {
public void gobble() {
System.out.println("咕咕叫");
}
public void fly() {
System.out.println("鸡也会飞哦");
}
}
鸭接口有 fly() 和 quare() 两个方法,鸡 Cock 如果要冒充鸭,fly() 方法是现成的,但是鸡不会鸭的呱呱叫,没有 quack() 方法。这个时候就需要适配了:
// 毫无疑问,首先,这个适配器肯定需要 implements Duck,这样才能当做鸭来用
public class CockAdapter implements Duck {
Cock cock;
// 构造方法中需要一个鸡的实例,此类就是将这只鸡适配成鸭来用
public CockAdapter(Cock cock) {
this.cock = cock;
}
// 实现鸭的呱呱叫方法
@Override
public void quack() {
// 内部其实是一只鸡的咕咕叫
cock.gobble();
}
@Override
public void fly() {
cock.fly();
}
}
客户端调用很简单了:
public static void main(String[] args) {
// 有一只野鸡
Cock wildCock = new WildCock();
// 成功将野鸡适配成鸭
Duck duck = new CockAdapter(wildCock);
...
}
到这里,大家也就知道了适配器模式是怎么回事了。无非是我们需要一只鸭,但是我们只有一只鸡,这个时候就需要定义一个适配器,由这个适配器来充当鸭,但是适配器里面的方法还是由鸡来实现的。
类适配器模式
class Adapter extends Adaptee implements Target{
public void request() {
super.specificRequest();
}
}
通过继承的方法,适配器自动获得了所需要的大部分方法。这个时候,客户端使用更加简单,直接 Target t = new Adapter (); 就可以了。
适配器模式总结
类适配和对象适配的异同
一个采用继承,一个采用组合;
类适配属于静态实现,对象适配属于组合的动态实现,对象适配需要多实例化一个对象。
总体来说,对象适配用得比较多。
适配器模式和代理模式的异同
比较这两种模式,其实是比较对象适配器模式和代理模式,在代码结构上,它们很相似,都需要一个具体的实现类的实例。但是它们的目的不一样,代理模式做的是增强原方法的活;
适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”,而鸡和鸭它们之间原本没有继承关系。
桥梁模式
桥接模式核心要点:
– 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立 的继承结构,使各个维度可以独立的扩展在抽象层建立关联。
客户端调用:
public static void main(String[] args) {
Shape greenCircle = new Circle(10, new GreenPen());
Shape redRectangle = new Rectangle(4, 8, new RedPen());
greenCircle.draw();
redRectangle.draw();
}
桥接模式总结:
- 桥接模式可以取代多层继承的方案。 多层继承违背了单一职责原则, 复用性较差,类的个数也非常多。桥接模式可以极大的减少子类的个 数,从而降低管理和维护的成本。
- 桥接模式极大的提高了系统可扩展性,在两个变化维度中任意扩展一 个维度,都不需要修改原有的系统,符合开闭原则。
桥接模式实际开发中应用场景
– JDBC驱动程序 – AWT中的Peer架构 – 银行日志管理:
• 格式分类:操作日志、交易日志、异常日志
• 距离分类:本地记录日志、异地记录日志 – 人力资源系统中的奖金计算模块:
• 奖金分类:个人奖金、团体奖金、激励奖金。
• 部门分类:人事部门、销售部门、研发部门。
– OA系统中的消息处理:
• 业务类型:普通消息、加急消息、特急消息
• 发送消息方式:系统内消息、手机短信、邮件
组合模式
组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。
直接看一个例子吧,每个员工都有姓名、部门、薪水这些属性,同时还有下属员工集合(虽然可能集合为空),而下属员工和自己的结构是一样的,也有姓名、部门这些属性,同时也有他们的下属员工集合。
public class Employee {
private String name;
private String dept;
private int salary;
private List<Employee> subordinates; // 下属
public Employee(String name,String dept, int sal) {
this.name = name;
this.dept = dept;
this.salary = sal;
subordinates = new ArrayList<Employee>();
}
public void add(Employee e) {
subordinates.add(e);
}
public void remove(Employee e) {
subordinates.remove(e);
}
public List<Employee> getSubordinates(){
return subordinates;
}
public String toString(){
return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]");
}
}
通常,这种类需要定义 add(node)、remove(node)、getChildren() 这些方法。
装饰模式
职责:
- 动态的为一个对象增加新的功能。
- 是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。
案例一
看客户端调用:
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"
// 如果我们需要 芒果-珍珠-双份柠檬-红茶:
Beverage beverage = new Mongo(new Pearl(new Lemon(new Lemon(new BlackTea()))));
}
案例二:自定义连接池
装饰模式(静态代理)
- 增加一个代理类 ,修改了close方法,其他方法交给被代理的对象
- 修改了MyDataSource的getConnection方法:return被代理的对象->return代理类对象
- 外界调用代码没有修改
自定义数据源类
//1.实现DataSource接口,重新里面的方法
public class MyDataSource implements DataSource {
//2.创建一个容器 LinkedList增删快,每移除一个元素,就获取了一个连接
LinkedList<Connection> pool=new LinkedList<>();
//3.初始化一些连接,放到容器中 将工具类,驱动,配置文件迁移到新项目
public MyDataSource() throws SQLException {
for (int i = 0; i < 5; i++) {
Connection conn = jdbcUtil2.getConnection(); //获取5次连接,保存到集合中
pool.addFirst(conn);
}
System.out.println("初始化:"+pool.size());
}
//4.实现连接的方法
@Override
public Connection getConnection() throws SQLException {
//pool移除最后一个
Connection conn = pool.removeLast();
// MyProxy myProxy = new MyProxy(conn, pool);//移花接木,代理对象重写了close方法
ClassLoader loader = conn.getClass().getClassLoader();
Class<?>[] interfaces = {java.sql.Connection.class};
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
if ("close".equals(name)){
pool.addFirst(conn);
System.out.println("将连接还给连接池了:动态代理");
}else {
//方法调用放入返回值
Object obj = method.invoke(conn, args);
return obj;
}
return null;
}
};
Connection myProxy = (Connection) Proxy.newProxyInstance(loader, interfaces, h);
System.out.println("被抽取连接:"+pool.size());
return myProxy; //返回代理对象
}
//5.将连接还给连接池,这个不符合规范,提高了学习成本,无法在使用面向接口编程思想,不利于扩展
public void backToPool(Connection conn){
pool.addFirst(conn);
System.out.println("连接换回来了:"+pool.size());
}
自定义代理类(只重新close方法)
public class MyProxy implements Connection {
Connection liuyan; //liuyan是连接的实现类
LinkedList<Connection> pool;
public MyProxy(Connection liuyan, LinkedList<Connection> pool){
this.liuyan=liuyan;
this.pool=pool;
}
@Override
public void close() throws SQLException {
System.out.println("MyProxy:close1");
pool.addFirst(liuyan);
}
@Override
public Statement createStatement() throws SQLException {
return liuyan.createStatement();
}
写一个读取文件数据源需要的配置文件参数,关闭io流
public class jdbcUtil2 {
static String driverName;
static String url;
static String username;
static String password;
static {
try {
Properties p = new Properties();
p.load(new FileInputStream("src/jdbc.properties"));
driverName = p.getProperty("driverName");
url = p.getProperty("url");
username = p.getProperty("username");
password = p.getProperty("password");
Class.forName(driverName);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
Connection conn = DriverManager.getConnection(url, username, password);
return conn;
}
public static void closeIo(AutoCloseable...ios){
for (AutoCloseable io : ios) { //这个的iOS第一个参数是属于代理类的liuyan和pool,代理类实现了Connection,而Connection继承了AutoCloseable类
if (io!=null){
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试
public class MyTestDemo {
public static void main(String[] args) throws SQLException {
DataSource mds = new MyDataSource(); //父类引用指向子类对象
Connection conn = mds.getConnection(); //获得代理对象liuyan和pool
String sql="select * from student";
PreparedStatement pstm = conn.prepareStatement(sql);
ResultSet resultSet = pstm.executeQuery();
while (resultSet.next()){
String name = resultSet.getString("name");
System.out.println(name);
}
//这里的conn就是代理对象
jdbcUtil2.closeIo(conn,pstm,resultSet);
}
}
//打印结果
初始化:5
被抽取连接:4
lisi
wangwu
MyProxy:closel
案例三:记录日志
public interface Command {
public void execute();
}
//一个用于记录日志的装饰器
public class LoggerDecorator implements Command{
Command cmd;
public LoggerDecorator(Command command){
this.cmd=command;
}
@Override
public void execute() {
Logger logger = Logger.getLogger("...");
//记录日志
logger.debug("...");
this.cmd.execute();
logger.debug("...");
}
}
//性能统计
public class PerformanceDecorator implements Command{
Command cmd;
public PerformanceDecorator(Command command){
this.cmd=command;
}
@Override
public void execute() {
// PerformanceUtil.startTimer("...");
this.cmd.execute();
// PerformanceUtil.endTimer("...");
}
}
public class PlaceOrderCommand implements Command{
Command cmd;
public PlaceOrderCommand(Command command){
this.cmd=command;
}
@Override
public void execute() {
//执行下单操作
}
}
public class PaymentCommand implements Command{
public void execute(){
//执行支付操作
}
}
使用:
Command cmd = new LoggerDecorator(new PerformanceDecorator(new PlaceOrderCommand()));
cmd.execute();
// 如果 PaymentCommand 只需要打印日志,那么装饰一次就可以了。
Command cmd = new LoggerDecorator(new PaymentCommand());
cmd.execute();
•总结:
–装饰模式(Decorator)也叫包装器模式(Wrapper)
–装饰模式降低系统的耦合度,可以动态的增加或删除对象的职责,并
使得需要装饰的具体构建类和具体装饰类可以独立变化,以便增加新
的具体构建类和具体装饰类。
•优点
–扩展对象功能,比继承灵活,不会导致类个数急剧增加
–可以对一个对象进行多次装饰,创造出不同行为的组合,得到功能更
加强大的对象。
–具体构建类和具体装饰类可以独立变化,用户可以根据需要自己增加新的具体构件子类和具体装饰子类。
•缺点
–产生很多小对象。大量小对象占据内存,一定程度上影响性能。
–装饰模式易于出错,调试排查比较麻烦。
•装饰模式和桥接模式的区别:
–两个模式都是为了解决过多子类对象问题。但他们の诱因不一样。桥模式是对象自身现有机制沿着多个维度变化,是既有部分不稳定。装饰模式是为了增加新的功能。
外观模式(门面模式)
核心:为子系统提供统一的入口。封装子系统的复杂性,便于客户端调用。
public class ShapeMaker {
private Shape circle;
private Shape rectangle;
private Shape square;
public ShapeMaker() {
circle = new Circle();
rectangle = new Rectangle();
square = new Square();
}
/**
* 下面定义一堆方法,具体应该调用什么方法,由这个门面来决定
*/
public void drawCircle(){
circle.draw();
}
public void drawRectangle(){
rectangle.draw();
}
public void drawSquare(){
square.draw();
}
}
看看现在客户端怎么调用:
public static void main(String[] args) {
ShapeMaker shapeMaker = new ShapeMaker();
// 客户端调用现在更加清晰了
shapeMaker.drawCircle();
shapeMaker.drawRectangle();
shapeMaker.drawSquare();
}
门面模式的优点显而易见,客户端不再需要关注实例化时应该使用哪个实现类,直接调用门面提供的方法就可以了,因为门面类提供的方法的方法名对于客户端来说已经很友好了。
享元模式
- 场景:
内存属于稀缺资源,不要随便浪费。如果有很多个完全相同或相似的对象,我们可以通过享元模式,节省内存。 - 核心:享元模式以共享的方式高效地支持大量细粒度对象的重用。
•享元模式开发中应用的场景:
–享元模式由于其共享的特性,可以在任何“池”中操作,比如:线程池、数据库连接池。
–String类的设计也是享元模式。
优点
–极大减少内存中对象的数量
–相同或相似对象内存中只存一份,极大的节约资源,提高系统性能
–外部状态相对独立,不影响内部状态
缺点
–模式较复杂,使程序逻辑复杂化
–为了节省内存,共享了内部状态,分离出外部状态,而读取外部状态
使运行时间变长。用时间换取了空间。
三.行为型模式
行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。
责任链模式
责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。
abstract public class Handler {
protected Handler successor;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
abstract String handleRequest(String msg);
}
从上面这个抽象类可以看出,每一个Handler对象都包含着一个successor成员,指向它的下一个任务处理者,就像链表节点的next指针一样。
public class HandlerA extends Handler {
@Override
String handleRequest(String msg) {
if(msg.contains("a")){
msg = msg.replace('a', '*');
} else if(this.successor != null){
msg = this.successor.handleRequest(msg);
}
return msg;
}
}
class HandlerB extends Handler {
@Override
String handleRequest(String msg) {
if(msg.contains("b")){
msg = msg.replace('b', '*');
} else if(this.successor != null){
msg = this.successor.handleRequest(msg);
}
return msg;
}
}
class HandlerC extends Handler {
@Override
String handleRequest(String msg) {
if(msg.contains("c")){
msg = msg.replace('c', '*');
} else if(this.successor != null){
msg = this.successor.handleRequest(msg);
}
return msg;
}
}
在这三个Handler实现类中,做了相似的判断:
如果传入的消息字符串包含某个字母,则把对应的字母替换成*。一旦某个Handler替换了自己所负责的字母,就直接结束整个链路;如果没有自己所负责的字母,则指定下一个Handler继续处理
来看下客户端调用:
public class Client {
public static void main(String[] args) {
Handler handlerA = new HandlerA();
Handler handlerB = new HandlerB();
Handler handlerC = new HandlerC();
handlerA.setSuccessor(handlerB);
handlerB.setSuccessor(handlerC);
System.out.println(handlerA.handleRequest("apple"));
System.out.println(handlerA.handleRequest("bicycle"));
System.out.println(handlerA.handleRequest("color"));
}
}
同一组if–else if,如果if已经满足条件,则else if即便满足条件,也不会执行。if满足里就return
HandlerA中的this.successor是HandlerB的successorHandlerB是
HandlerC的successor
在客户端代码中,可以灵活设置整个链路和处理者的次序,然后直接调用第一个处理者的handleRequest方法,就相当于启动了整个链路。
做过Web开发的小伙伴都知道,当客户端对Web应用发出HTTP请求的时候,会首先经过Tomcat容器的一层层过滤器(Filter),过滤器会针对请求的访问权限、参数合法性等方面进行验证和过滤。
这一层一层过滤器的实现,就使用了职责链模式。
策略模式
本质:分离算法,选择实现。
下面设计的场景是,我们需要画一个图形,可选的策略就是用红色笔来画,还是绿色笔来画,或者蓝色笔来画。
首先,先定义一个策略接口:
public interface Strategy {
public void draw(int radius, int x, int y);
}
然后我们定义具体的几个策略:
public class RedPen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class GreenPen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class BluePen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
使用策略的类:
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeDraw(int radius, int x, int y){
return strategy.draw(radius, x, y);
}
}
客户端演示:
public static void main(String[] args) {
Context context = new Context(new BluePen()); // 使用绿色笔来画
context.executeDraw(10, 0, 0);
}
观察者模式
开发中常见的场景:
–聊天室程序的,服务器转发给所有客户端
–网络游戏(多人联机对战)场景中,服务器将客户端的状态进行分发
–邮件订阅
–Servlet中,监听器的实现
–Android中,广播机制
–JDK的AWT中事件处理模型,基于观察者模式的委派事件模型(DelegationEventModel)
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 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 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 static void main(String[] args) {
// 先定义一个主题
Subject subject1 = new Subject();
// 定义观察者
new BinaryObserver(subject1);
new HexaObserver(subject1);
// 模拟数据变更,这个时候,观察者们的 update 方法将会被调用
subject.setState(11);
//output:
//订阅的数据发生变化,新的数据处理为二进制值为:1011
//订阅的数据发生变化,新的数据处理为十六进制值为:B
}
模板方法模式
在含有继承结构的代码中,模板方法模式是非常常用的。
通常会有一个抽象类:
public abstract class AbstractTemplate {
// 这就是模板方法
public void templateMethod() {
init();
apply(); // 这个是重点
end(); // 可以作为钩子方法
}
protected void init() {
System.out.println("init 抽象层已经实现,子类也可以选择覆写");
}
// 留给子类实现
protected abstract void apply();
protected void end() {
}
}
模板方法中调用了 3 个方法,其中 apply() 是抽象方法,子类必须实现它,其实模板方法中有几个抽象方法完全是自由的,我们也可以将三个方法都设置为抽象方法,让子类来实现。也就是说,模板方法只负责定义第一步应该要做什么,第二步应该做什么,第三步应该做什么,至于怎么做,由子类来实现。
我们写一个实现类:
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();
}
如果不理解方法回调(钩子函数)参考下图:
状态模式
核心:用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题
结构:
–Context环境类:环境类中维护一个State对象,他是定义了当前的状态。
–State抽象状态类
–ConcreteState具体状态类:每一个类封装了一个状态对应的行为
定义状态接口:
public interface State {
public void doAction(Context context);
}
定义减库存的状态:
public class DeductState implements State {
public void doAction(Context context) {
System.out.println("商品卖出,准备减库存");
context.setState(this);
//... 执行减库存的具体操作
}
public String toString() {
return "Deduct State";
}
}
定义补库存状态:
public class RevertState implements State {
public void doAction(Context context) {
System.out.println("给此商品补库存");
context.setState(this);
//... 执行加库存的具体操作
}
public String toString() {
return "Revert State";
}
}
前面用到了 context.setState(this),我们来看看怎么定义 Context 类:
public class Context {
private State state;
private String name;
public Context(String name) {
this.name = name;
}
public void setState(State state) {
this.state = state;
}
public void getState() {
return this.state;
}
}
我们来看下客户端调用,大家就一清二楚了:
public static void main(String[] args) {
// 我们需要操作的是 iPhone X
Context context = new Context("iPhone X");
// 看看怎么进行补库存操作
State revertState = new RevertState();
revertState.doAction(context);
// 同样的,减库存操作也非常简单
State deductState = new DeductState();
deductState.doAction(context);
// 如果需要我们可以获取当前的状态
// context.getState().toString();
}
行为型模式总结
- 职责链模式:避免请求发送者和接收者耦合,让多个对象都有可能接收请求,将这些对象连成一条链,并且沿着这条链传递请求,直到有对象处理为止.
- 策略模式:定义一系列算法,并将每个算法封装在一个类中
- 模板方法:定义一个操作的算法骨架,将某些易变的步骤延迟到子类中实现
- 观察者模式:当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新
- 状态模式:允许一个对象在其内部状态改变时改变它的行为
- 命令模式:将一个请求封装为一个对象,从而使得请求调用者和请求接收者解耦
- 解释器模式:描述如何为语言定义一个文法,如何解析
- 中介者模式:通过一个中介对象来封装一系列的对象交互,使得各对象不需要相互引用
- 迭代器模式:提供了一种方法来访问聚合对象
- 备忘录模式:捕获一个对象的内部状态,并保存之;需要时,可以恢复到保存的状态
- 访问者模式:表示一个作用于某对象结构中的各元素的操作,它使得用户可以在不改变各元素的类的前提下定义作用于这些元素的新操作