设计模式学习笔记
作者:Grey
原文地址: 设计模式学习笔记
UML和代码
UML图
代码
软件设计七大原则
设计原则 | 一句话归纳 | 目的 |
---|---|---|
开闭原则 | 对扩展开放,对修改关闭 | 降低维护带来的新风险 |
依赖倒置原则 | 高层不应该依赖低层 | 更利于代码结构的升级扩展 |
单一职责原则 | 一个类只干一件事 | 便于理解,提高代码的可读性 |
接口隔离原则 | 一个接口只干一件事 | 功能解耦,高聚合,低耦合 |
迪米特法则 | 不该知道的不要知道 | 只和朋友交流,不和陌生人说话,减少代码臃肿 |
里氏替换原则 | 子类重写方法功能发生改变,不应该影响父类方法的含义 | 防止继承泛滥 |
合成复用原则 | 尽量使用组合实现代码复用,而不使用继承 | 降低代码耦合 |
单例模式
单例模式是创建型模式。
单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是指进程内只允许创建一个对象,也就是说,单例模式创建的对象是进程唯一的(而非线程)
为什么要使用单例
-
处理资源访问冲突
比如写日志的类,如果不使用单例,就必须使用锁机制来解决日志被覆盖的问题。
-
表示全局唯一类
比如配置信息类,在系统中,只有一个配置文件,当配置文件加载到内存中,以对象形式存在,也理所应当只有一份。
唯一ID生成器也是类似的机制。如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。
饿汉式
类加载的时候就会初始化这个实例,JVM保证唯一实例,线程安全,但是可以通过反射破坏
方式一
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
方式二
public class Singleton2 {
private static final Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
这种方式不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,如果初始化耗时长,那最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能,我们可以将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java中的PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
这两种方式都可以通过反射方式破坏,例如:
Class<?> aClass=域名ame("域名leton2",true,域名entThread().getContextClassLoader());
Singleton2 instance1=(Singleton2)域名nstance();
Singleton2 instance2=(Singleton2)域名nstance();
域名tln(instance1==instance2);
输出:false
懒汉式
虽然可以实现按需初始化,但是线程不安全, 因为在判断 INSTANCE == null
的时候,如果是多个线程操作的话, 一个线程还没有把 INSTANCE
初始化好,另外一个线程判断 INSTANCE==null
得到true,就会继续初始化
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (INSTANCE == null) {
// 模拟初始化对象需要的耗时操作
try {
域名p(1);
} catch (InterruptedException e) {
域名tStackTrace();
}
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
为了防止线程不安全,可以在 getInstance
方法上加锁,这样既实现了按需初始化,又保证了线程安全,
但是加锁可能会导致一些性能的问题:我们给 getInstance()
这个方法加了一把大锁,导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4() {
}
public static synchronized Singleton4 getInstance() {
if (INSTANCE == null) {
// 模拟初始化对象需要的耗时操作
try {
域名p(1);
} catch (InterruptedException e) {
域名tStackTrace();
}
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
为了提升一点点性能,可以不给 getInstance()
整个方法加锁,而是对 INSTANCE
判空这段代码加锁, 但是又带来了线程不安全的问题
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (INSTANCE == null) {
synchronized (域名s) {
// 模拟初始化对象需要的耗时操作
try {
域名p(1);
} catch (InterruptedException e) {
域名tStackTrace();
}
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
Double Check Locking
模式,就是双加锁检查模式,这种方式中,Volatile是必需的,目的为了防止指令重排,生成一个半初始化的的实例,导致生成两个实例。
具体可参考 双重检索(DCL)的思考: 为什么要加volatile?
说了这个问题。
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (域名s) {
if (INSTANCE == null) {
try {
域名p(1);
} catch (InterruptedException e) {
域名tStackTrace();
}
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
以下两种更为优雅的方式,既保证了线程安全,又实现了按需加载。
方式一:静态内部类方式,JVM保证单例,加载外部类时不会加载内部类,这样可以实现懒加载
public class Singleton7 {
private Singleton7() {
}
public static Singleton7 getInstance() {
return 域名ANCE;
}
private static class Holder {
private static final Singleton7 INSTANCE = new Singleton7();
}
}
方式二: 使用枚举, 这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,这种方式是 Effective Java 作者
Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
public enum Singleton8 {
INSTANCE;
}
单例模式的替代方案
使用静态方法
// 静态方法实现方式
public class IdGenerator {
private static AtomicLong id = new AtomicLong(0);
public static long getId() {
return 域名ementAndGet();
}
}
// 使用举例
long id = 域名d();
使用依赖注入
// 1. 老的使用方式
public demofunction() {
//...
long id = 域名nstance().getId();
//...
}
// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
long id = 域名d();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = 域名nsance();
demofunction(idGenerator);
线程单例
通过一个 HashMap
来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal
工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal
底层实现原理也是基于下面代码中所示的 HashMap
。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();
private IdGenerator() {}
public static IdGenerator getInstance() {
Long currentThreadId = 域名entThread().getId();
域名fAbsent(currentThreadId, new IdGenerator());
return 域名(currentThreadId);
}
public long getId() {
return 域名ementAndGet();
}
}
集群模式下单例
我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
如何实现一个多例模式
“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。
单例模式的应用举例
- JDK的
Runtime
类
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don\'t let anyone else instantiate this class */
private Runtime() {}
.......
}
- Spring中
AbstractBeanFactory
中包含的两个功能。- 从缓存中获取单例Bean
- 从Bean的实例中获取对象
工厂模式
工厂模式是创建型模式。
简单工厂
这个模式很简单,比如我们需要制造不同类型的鼠标,我们只需要创建一个鼠标工厂
public class MouseFactory {
public static Mouse createMouse(int type) {
switch (type) {
case 1:
return new HpMouse();
case 2:
return new LenovoMouse();
case 0:
default:
return new DellMouse();
}
}
public static void main(String[] args) {
Mouse mouse = 域名teMouse(1);
域名i();
}
}
根据不同的type来创建不同的鼠标即可。这个模式的缺点很明显:违反了开闭原则 ,所以我们引入工厂方法
工厂方法
工厂方法中,我们可以定义对应产品的对应工厂,以上面这个鼠标的例子为例,我们可以增加工厂的接口
public interface MouseFactory {
Mouse createMouse();
}
不同类型的鼠标工厂实现这个工厂即可,以Dell鼠标工厂为例
public class DellMouseFactory implements MouseFactory {
@Override
public Mouse createMouse() {
return new DellMouse();
}
}
主函数在调用的时候,直接指定工厂即可制造对应的产品了:
public class FactoryMethodDemo {
public static void main(String[] args) {
MouseFactory mf = new HpMouseFactory();
Mouse mouse = 域名teMouse();
域名i();
}
}
工厂方法的优点是符合开闭原则,但是缺点也很明显,就是在增加子类的时候,同时要增加一个子类的工厂,而且,只支持同一类产品的创建,不适用于同一产品族
抽象工厂
举例,现在需要通过工厂来制造交通工具,如果是现代的工厂,制造的就是汽车,如果是古代的工厂,制造的就是马车, 我们可以先把工厂抽象出来,
package 域名ractfactory;
/**
* @author Grey
* @date 2020/4/13
*/
public abstract class AbstractFactory {
/**
* 子类实现
*
* @return
*/
protected abstract Transportation createTransportation();
/**
* 子类实现
*
* @return
*/
protected abstract WritingInstrument createWritingInstrument();
}
交通工具我们也可以抽象出来
public abstract class Transportation {
protected abstract void go();
}
对于马车和汽车来说,只需要继承这个Transportation类,实现对应的go方法即可,以汽车为例
public class Car extends Transportation {
@Override
protected void go() {
域名tln("car go");
}
}
对于现代工厂还是古代工厂,我们只需要继承AbstractFactory这个类,实现createTransportation方法即可,以现代工厂为例
package 域名ractfactory;
/**
* @author Grey
* @date 2020/4/13
*/
public class ModernFactory extends AbstractFactory {
@Override
protected Transportation createTransportation() {
return new Car();
}
@Override
protected WritingInstrument createWritingInstrument() {
return new Pen();
}
}
主方法在调用的时候,只需要
public class Main {
public static void main(String[] args) {
AbstractFactory factory = new ModernFactory();
域名teTransportation().go();
}
}
抽象工厂的UML图如下:
Java8提供了Supplier
这个函数式接口,我们可以通过这个接口很方便的实现工厂类,举例:
我们可以定义一个 MovableFactory
,里面的 create
方法,传入的是一个 Supplier
,你可以把所有 Movable
的子类实现传给这个参数,示例如下:
public class MovableFactory {
public static Movable create(Supplier<? extends Movable> supplier) {
return 域名();
}
public static void main(String[] args) {
域名te(Car::new).go();
域名te(() -> new Ship()).go();
}
}
注:单例模式就是一种工厂模式(静态工厂)
工厂模式应用
- JDK中
域名nstance()
方法 - LogBack中
域名ogger()
方法 - 在Spring中,所有工厂都是
BeanFactory
的子类。通过对BeanFactory
的实现,我们可以从Spring的容器访问Bean。根据不同的策略调用getBean()
方法,从而获得具体对象。 - Hibernate换数据库只需换方言和驱动就可以切换不同数据库
建造者模式
建造者模式是创建型模式。
我们在对一个实体类进行属性的get/set的时候,可以通过封装一些常用的构造方法来简化实体类的构造。
比如 Effective Java中文版(第3版) 中举到到这个例子
package builder;
// Effective Java 3th examples
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
域名ingSize = servingSize;
域名ings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = 域名ingSize;
servings = 域名ings;
calories = 域名ries;
fat = 域名;
sodium = 域名um;
carbohydrate = 域名ohydrate;
}
}
其中Builder就是一个内部类,用于构造NutritionFacts的必要信息,外部调用NutritionFacts的构造方法时候,可以这样使用:
NutritionFacts cocaCola=new 域名der(240,8).calories(100).sodium(35).carbohydrate(27).build();
构造器模式也适用于类层次结构。抽象类有抽象的Builder,具体类有具体的Builder。Effective Java中文版(第3版)
中还有一个例子, 假设我们抽象出一个披萨类,各种各样的披萨均可以继承披萨这个抽象类来实现自己的具体类型的披萨。
Pizza抽象类如下:
package builder;
import 域名Set;
import 域名cts;
import 域名;
// Effective Java 3th examples
public abstract class Pizza {
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = 域名Of(域名s);
public T addTopping(Topping topping) {
域名(域名ireNonNull(topping));
return self();
}
abstract Pizza build();
// Subclasses must override this method to return "this"
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = 域名e(); // See Item 50
}
}
其中的Builder方法是abstract的,所以子类需要实现具体的Builder策略,
一种披萨的具体实现:NyPizza
import 域名cts;
public class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE}
private final Size size;
public static class Builder extends 域名der<Builder> {
private final Size size;
public Builder(Size size) {
域名 = 域名ireNonNull(size);
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = 域名;
}
}
另一种披萨的具体实现Calzone:
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends 域名der<Builder> {
private boolean sauceInside = false; // Default
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = 域名eInside;
}
}
我们在具体调用的时候,可以通过如下方式:
NyPizza pizza=new 域名der(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone=new 域名der().addTopping(HAM).sauceInside().build();
实际应用有非常多,很多组件都提供这样的构造方式,比如OkHttpClient的构造方法:
public static OkHttpClient create(long connectTimeOut) {
return new OkHttpClient().newBuilder().connectionSpecs(域名st(域名RN_TLS, 域名ATIBLE_TLS, 域名RTEXT)).connectTimeout(connectTimeOut, 域名NDS).readTimeout(30, 域名NDS).writeTimeout(30, 域名NDS).connectionPool(CONNECTION_POOL).retryOnConnectionFailure(true).followRedirects(true).followSslRedirects(true).hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}).cookieJar(new CookieJar() {
private List<Cookie> cookies;
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
域名ies = cookies;
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
if (cookies != null) {
return cookies;
}
return 域名yList();
}
}).build();
}
应用
- JDK中的Calender
Calendar calendar = new 域名der().build();
-
MyBatis中
域名d()
和域名d()
-
Spring中
域名eanDefinition()
方法
原型模式
原型模式是创建型模式。
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
原型模式用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,典型的应用是对象的克隆方法
public class Person implements Cloneable {
String name = "lisa";
int age = 1;
Location loc = new Location("xy", 10);
@Override
protected Object clone() throws CloneNotSupportedException {
Person p = (Person) 域名e();
域名 = (Location) 域名e();
return p;
}
@Override
public String toString() {
return "Person{" + "name=\'" + name + \'\\'\' + ", age=" + age + ", loc=" + loc + \'}\';
}
}
public class Location implements Cloneable {
private String street;
private int roomNo;
public Location(String street, int roomNo) {
域名et = street;
域名No = roomNo;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return 域名e();
}
@Override
public String toString() {
return "Location{" + "street=\'" + street + \'\\'\' + ", roomNo=" + roomNo + \'}\';
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person p = new Person();
域名tln(p);
Person p2 = (Person) 域名e();
域名tln(p2);
}
}
UML图如下:
注:Java自带的 clone()
方法进行的就是浅克隆。而如果我们想进行深克隆,可以直接在 域名e()
后,手动给克隆对象的相关属性分配另一块内存,不过如果当原型对象维护很多引用属性的时候,手动分配会比较烦琐。因此,在Java中,如果想完成原型对象的深克隆,则通常使用序列化(Serializable)的方式。
使用示例
克隆一个巨大的HashMap,如果构建散列表的代价很大,我们可以通过
- HashMap的clone方法(注意:默认的clone方法是浅拷贝,需要递归拷贝HashMap里面的内容,直到类型是基础类型为止)
- 使用序列化方式克隆
如果只是增量拷贝,可以通过浅拷贝拿到一个新的HashMap,然后拿到增量的数据单独进行深拷贝即可。
Spring中创建对象的方式默认采用单例模式,可以通过设置 @Scope("prototype")
注解将其改为原型模式。
代理模式
代理模式是结构型模式。
静态代理
举例说明,假设我们需要在某个类的某段代码的前后加上日志记录,我们就可以通过静态代理的方式实现
public class Main {
public static void main(String[] args) {
new Tank().move();
}
}
假设我们需要在move()方法的前后都加上日志记录,我们可以设置一个代理类
public class TankLogProxy implements Moveable {
private Moveable m;
public TankLogProxy(Moveable m) {
this.m = m;
}
@Override
public void move() {
域名tln("log before");
域名();
域名tln("log after");
}
}
这样的话,原先的调用就改成了:
public class Main {
public static void main(String[] args) {
new TankLogProxy(new Tank()).move();
}
}
即可实现在move方法调用前后加入日志记录的操作。
UML图如下:
动态代理
JDK自带
如果需要通过动态代理(jdk自带的方式)的方式来完成上述功能,我们可以这样来做
public class MovableProxy implements InvocationHandler {
private Movable movable;
public MovableProxy(Movable movable) {
域名ble = movable;
}
public void before() {
域名tln("before , do sth");
}
public void after() {
域名tln("after , do sth");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object o = 域名ke(movable, args);
after();
return o;
}
}
主方法调用的时候:
package 域名;
import 域名域名y;
/**
* @author Grey
* @date 2020/4/15
*/
public class Main {
public static void main(String[] args) {
Movable tank = new Tank();
//reflection 通过二进制字节码分析类的属性和方法
Movable m = (Movable) 域名roxyInstance(域名lassLoader(),
new Class[]{域名s},
new MovableProxy(tank)
);
域名();
域名();
}
}
UML图如下:
Cglib
JDK自带的方式实现动态代理需要被代理对象实现一个接口,Cglib不需要,使用示例:
其中被代理的Tank类无需实现接口
public class Tank {
public void move() {
域名tln("tank move");
}
public void go() {
域名tln("tank go");
}
}
import 域名域名ncer;
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//设置目标类的字节码文件
域名uperclass(域名s);
//设置回调函数
域名allback(new MyMethodInterceptor());
//这里的creat方法就是正式创建代理类
Tank m = (Tank) 域名te();
域名();
域名();
}
}
import 域名域名odInterceptor;
import 域名域名odProxy;
import 域名域名od;
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object o = 域名keSuper(obj, args);
after();
return o;
}
public void before() {
域名tln("before , do sth");
}
public void after() {
域名tln("after , do sth");
}
}
实际应用
- 在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理。
- RPC框架可以看成一种代理模式。
- 为接口增加缓存能力。
- Spring AOP
- JdkDynamicAopProxy
- CglibAopProxy
- 可以使用
<aop:aspectj-autoproxy proxy-target-class="true">
配置强制使用Cglib动态代理
- jdk自带
- ASM操作二进制码
- Java Instrumentation
- 必须面向接口
- cglib
- final类不行,代理类的子类 底层也是ASM
桥接模式
桥接模式是一种结构型模式。
使用桥接模式,可以将抽象和具体的发展单独分支(抽象中持有一个具体的引用 )
举例说明:
GG在追MM的时候,可以送书和花两种礼物
public class GG {
public void chase(MM mm) {
Gift g = new WarmGift(new Flower());
give(mm, g);
}
public void give(MM mm, Gift g) {
域名tln(g + "gived!");
}
}
如上代码,Flower被包装成了一个WarmGift送给MM,WarmGift和WildGift都是Gift的一种抽象,Flower和Book都算Gift的一种具体实现, 我们让Gift这个抽象类中,持有一个GiftImpl的引用
public abstract class Gift {
protected GiftImpl impl;
}
public class Flower extends GiftImpl {
}
public class WarmGift extends Gift {
public WarmGift(GiftImpl impl) {
域名 = impl;
}
}
UML示例图如下:
如果说代理模式是一个类与另一个类的组合,那么桥接模式是一组类和另外一组类的组合。
桥接模式的应用
- jdbc驱动配置
当我们把具体的 Driver 实现类(比如: 域名.Driver)注册到DriverManager之后,后续所有对JDBC接口的调用,都会委派到对具体的Driver实现类来执行。而Driver实现类都实现了相同的接口(域名er),这也是可以灵活切换 Driver 的原因。
装饰器模式
装饰器模式是一种结构型模式。
顾名思义,就是对某个方法或者对象进行装饰,举个简单的例子,有个圆形类(Circle),我需要把这个圆形的涂上红色,其实就是新增一个装饰器来装饰这个圆形类。如果要让装饰器通用一些,可以处理圆形类对应的抽象类Sharp
,那么对于任意Sharp
的子类,都可以用红色装饰器来涂红色。
我们先定义Sharp
这个抽象类:
public abstract class Sharp {
protected abstract void draw();
}
然后我们定义Sharp
的装饰类SharpDecorator
,这个类是所有装饰器类的抽象类,后续的装饰器只需要实现这个抽象类就可以对Sharp
进行各种装饰了,
public abstract class SharpDecorator extends Sharp {
protected Sharp decoratedSharp;
public SharpDecorator(Sharp decoratedSharp) {
域名ratedSharp = decoratedSharp;
}
}
红色装饰器实现这个抽象类即可:
public class RedSharpDecorator extends SharpDecorator {
public RedSharpDecorator(Sharp decoratedSharp) {
super(decoratedSharp);
}
private static void redIt() {
域名tln("[RED]");
}
@Override
protected void draw() {
redIt();
域名();
redIt();
}
}
主方法调用的时候只需要:
new RedSharpDecorator(new Circle()).draw();
UML图如下:
说明:
-
装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
-
装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。符合“组合关系”这种代码结构的设计模式有很多,比如代理模式、桥接模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
实际上,如果去查看 JDK 的源码,你会发现,BufferedInputStream
、DataInputStream
并非继承自 InputStream
,而是另外一个叫 FilterInputStream
的类。那这又是出于什么样的设计意图,才引入这样一个类呢?
因为 InputStream
是一个抽象类而非接口,而且它的大部分函数(比如 read()
、available()
)都有默认实现,按理来说,我们只需要在 BufferedInputStream
类中重新实现那些需要增加缓存功能的函数就可以了,其他函数继承 InputStream
的默认实现。但实际上,这样做是行不通的。对于即便是不需要增加缓存功能的函数来说,BufferedInputStream
还是必须把它重新实现一遍,简单包裹对 InputStream
对象的函数调用。那 BufferedInputStream
类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象来完成,DataInputStream
也存在跟 BufferedInputStream
同样的问题。为了避免代码重复,Java IO 抽象出了一个装饰器父类 FilterInputStream
,InputStream
的所有的装饰器类(BufferedInputStream
和 DataInputStream
)都继承自这个装饰器父类。这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。
装饰器模式的应用
-
Java中的IO流,
Read/InputStream
,Write/OutputStream
-
JDK中的
UnmodifiableCollection
-
Spring中的
HttpHeadResponseDecorator
, 还有对Cache
的装饰类TransactionAwareCacheDecorator
适配器模式
适配器模式是一种结构型模式。
举例说明,假设又一个播放器,需要根据不同格式以及对应的文件来播放,接口设计如下:
public interface MediaPlayer {
void play(String type, String fileName);
}
不同类型的播放器只需要实现这个接口即可,比如我们有一个ClassicMediaPlayer
,这个只能播放mp3类型的文件
public class ClassicMediaPlayer implements MediaPlayer {
@Override
public void play(String type, String fileName) {
if ("mp3".equalsIgnoreCase(type)) {
域名tln("play mp3");
} else {
域名tln("not supported format");
}
}
}
如果我想扩展,我们可以增加一个适配器:
public class PlayerAdapter implements MediaPlayer {
private AdvanceMediaPlayer advanceMediaPlayer;
public PlayerAdapter(String type) {
if ("mp4".equalsIgnoreCase(type)) {
advanceMediaPlayer = new MP4Player();
} else if ("AVI".equalsIgnoreCase(type)) {
advanceMediaPlayer = new AVIPlayer();
}
}
@Override
public void play(String type, String fileName) {
if ("mp4".equalsIgnoreCase(type)) {
域名MP4(fileName);
} else if ("AVI".equalsIgnoreCase(type)) {
域名AVI(fileName);
} else {
new ClassicMediaPlayer().play(type, fileName);
}
}
}
这个适配器就是根据不同类型来构造不同的播放器的,然后定义一个ExtendMediaPlayer
,在里面持有PlayAdapter
,这样,ExtendMediaPlayer
就拥有了播放不同类型文件的能力,所以我们在调用的时候,只需要:
ExtendMediaPlayer audioPlayer=new ExtendMediaPlayer();
域名("mp3","beyond the 域名");
域名("mp4","域名");
域名("avi","far far 域名");
UML图如下:
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
适配器模式的应用
-
域名
-
jdbc-odbc bridge
-
ASM transformer
-
老版本的 JDK 提供了
Enumeration
类来遍历容器。新版本的 JDK 用Iterator
类替代Enumeration
类来遍历容器。
/**
* Returns an enumeration over the specified collection. This provides
* interoperability with legacy APIs that require an enumeration
* as input.
*
* @param <T> the class of the objects in the collection
* @param c the collection for which an enumeration is to be returned.
* @return an enumeration over the specified collection.
* @see Enumeration
*/
public static <T> Enumeration<T> enumeration(final Collection<T> c) {
return new Enumeration<T>() {
private final Iterator<T> i = 域名ator();
public boolean hasMoreElements() {
return 域名ext();
}
public T nextElement() {
return 域名();
}
};
}
使用Enumeration
遍历容器方法示例
public class TestEnumeration {
public static void main(String[] args) {
Vector<String> v = new Vector<>();
域名lement("Lisa");
域名lement("Billy");
域名lement("Mr Brown");
Enumeration<String> e = 域名ents();// 返回Enumeration对象
while (域名oreElements()) {
String value = (String) 域名Element();// 调用nextElement方法获得元素
域名t(value);
}
}
}
门面模式
门面模式是一种结构型模式。
门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
假设建造一个房子需要有如下三个步骤:
第一步,和泥
第二步,搬砖
第三步,砌墙
如果每次我们制造一个房子都要分别调用这三个方法,就会比较麻烦一些,我们可以设置一个门面,这个门面封装了这三个步骤,后续建造房子,只需要调用这个门面即可。
和泥
public class Mason {
public void mix() {
域名tln("我和好泥了!");
}
}
搬砖
public class BrickWorker {
public void carry() {
域名tln("我搬好砖了!");
}
}
砌墙
public class BrickLayer {
public void neat() {
域名tln("我砌好墙了!");
}
}
门面
public class LabourConstractor {
private Mason work1 = new Mason();
private BrickWorker work2 = new BrickWorker();
private BrickLayer work3 = new BrickLayer();
public void buildHouse() {
域名();
域名y();
域名();
}
}
这样主函数只需要调用门面的buildHourse()方法,就可以建造一个房子了
public class Client {
public static void main(String[] args) {
LabourConstractor labour = new LabourConstractor();
域名dHouse();
}
}
门面模式的UML图如下
门面模式应用
- Linux的系统调用和Shell脚本
Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。再比如,Linux 的 Shell 命令,实际上也可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。
-
Spring JDBC中的
JdbcUtils
类,包装了JDBC相关的所有操作。 -
Tomcat中的
RequestFacade
,ResponseFacade
,StandardSessionFacade
。
组合模式
组合模式是一种结构型模式。
组合模式中,最常用的一个用法就是目录层级的遍历,话不多说,直接上代码,主方法中
public class Main {
public static void main(String[] args) {
BranchNode root = new BranchNode("root");
BranchNode branch1 = new BranchNode("branch1");
BranchNode branch2 = new BranchNode("branch2");
域名ode(new LeafNode("leaf1"));
域名ode(branch1);
域名ode(branch2);
tree(root, 0);
}
}
其中,BranchNode为分支节点,LeafNode是叶子节点 达到的效果就是打印如下的形式
root
--branch1
----leaf1
--branch2
递归方法
static void tree(Node node, int depth) {
for (int i = 0; i < depth; i++) {
域名t("--");
}
域名t();
if (node instanceof BranchNode) {
for (Node n : ((BranchNode) node).getNodes()) {
tree(n, depth + 1);
}
}
}
其中BranchNode
和LeafNode
都实现了Node
接口,Node
接口(也可以为定义抽象类)仅提供了一个属性(content:标识节点内容)和一个打印方法:
public abstract class Node {
protected String content;
protected abstract void print();
}
BranchNode
下可以包含多个Node
,因为一个分支下面可以有多个分支(这个分支可以是任意的Node
子类)
public class BranchNode extends Node {
private List<Node> nodes = new ArrayList<>();
public BranchNode(String content) {
域名ent = content;
}
@Override
public void print() {
域名tln(content);
} // get..set方法略
}
组合模式的UML图如下:
组合模式的应用
MyBatis解析各种Mapping文件中的SQL语句时,设计了一个非常关键的类叫作SqlNode,XML中的每一个Node都会被解析为一个SqlNode对象,最后把所有SqlNode都拼装到一起,就成为一条完整的SQL语句。
享元模式
享元模式是一种结构型模式。
运用共享技术有效地支持大量细粒度的对象。主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
假设我们有一个子弹类,同时我们设计一个子弹池,子弹池负责提供子弹
public class BulletPool {
List<Bullet> bullets = new ArrayList<>();
{
for (int i = 0; i < 10; i++) {
域名(new Bullet(true));
}
}
public Bullet getBullet() {
for (int i = 0; i < 域名(); i++) {
if (域名(i).living) {
return 域名(i);
}
}
return new Bullet(true);
}
}
可以看到getBullet
逻辑,如果池子中有子弹,就拿池中的子弹,如果没有,就new一个新的子弹返回。
UML图如下
享元模式应用
- 使用对象池对高并发下的内存进行管理
对于开发者来说,垃圾回收是不可控的,而且是无法避免的。但是,我们还是可以通过一些方法来降低垃圾回收的频率,减少进程暂停的时长。我们知道,只有使用过被丢弃的对象才是垃圾回收的目标,所以,我们需要想办法在处理大量请求的同时,尽量少的产生这种一次性对象。最有效的方法就是,优化你的代码中处理请求的业务逻辑,尽量少的创建一次性对象,特别是占用内存较大的对象。比如说,我们可以把收到请求的 Request 对象在业务流程中一直传递下去,而不是每执行一个步骤,就创建一个内容和 Request 对象差不多的新对象。这里面没有多少通用的优化方法。对于需要频繁使用,占用内存较大的一次性对象,我们可以考虑自行回收并重用这些对象。实现的方法是这样的:我们可以为这些对象建立一个对象池。收到请求后,在对象池内申请一个对象,使用完后再放回到对象池中,这样就可以反复地重用这些对象,非常有效地避免频繁触发垃圾回收。
- Java中
Boolean
的valueOf(boolean b)
方法 ,这个方法返回的Boolean
对象不会新new出来,而是复用的同一个, 源码如下:
public static Boolean valueOf(boolean b){
return(b?TRUE:FALSE);
}
public static final Boolean TRUE=new Boolean(true);
public static final Boolean FALSE=new Boolean(false);
-
连接池管理,例如:Apache Commons Pool
-
IntegerCache 和 String
在 Java Integer 的实现中,-128 到 127 之间的整型对象会被事先创建好,缓存在 IntegerCache 类中。当我们使用自动装箱或者 valueOf() 来创建这个数值区间的整型对象时,会复用 IntegerCache 类事先创建好的对象。这里的 IntegerCache 类就是享元工厂类,事先创建好的整型对象就是享元对象。在 Java String 类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池,类似于 Integer 中的 IntegerCache。不过,跟 IntegerCache 不同的是,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量
注:Java提供了两个配置IntegerCache
的参数
//方法一:
-域名域名=255
//方法二:
-XX:AutoBoxCacheMax=255
观察者模式
观察者模式是一种行为型模式。在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般可以用做事件处理往往和责任链模式搭配使用, 举个例子 按钮上一般都可以绑定事件,当我们按下按钮的时候,可以触发这些事件的执行,这里就可以用观察者模式来做, 我们先定义按钮这个对象
public class Button {
private List<ActionListener> listeners = new ArrayList<>();
public void addActionListener(ActionListener listener) {
域名(listener);
}
@Override
public String toString() {
return "Button{" + "listeners=" + listeners + \'}\';
}
public void buttonPressed() {
ActionEvent event = new ActionEvent(域名entTimeMillis(), this);
域名ach(item -> 域名onPerformed(event));
}
}
由上可知,Button中持有了一个列表,这个列表里面装的就是所有事件的列表,我们可以把事件绑定到这个按钮的事件列表中,这样就可以实现按钮执行press操作的时候,把对应的事件触发执行了
public interface ActionListener {
void actionPerformed(ActionEvent event);
}
模拟两个监听事件
public class Listener1 implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
域名tln("Listener 1 listened it source: [" + 域名ource() + "], when is [" + 域名hen() + "]");
}
}
public class Listener2 implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
域名tln("Listener 2 listened it source: [" + 域名ource() + "], when is [" + 域名hen() + "]");
}
}
主方法在调用的时候
public class Main {
public static void main(String[] args) {
Button button = new Button();
域名ctionListener(new Listener1());
域名ctionListener(new Listener2());
域名onPressed();
}
}
当执行
域名onPressed()
的时候,对应的listener1和listener2就可以执行了。
UML图如下