创建类模式将对象的创建和使用分离开来,结构类设计模式主要是为了解决类或对象之间的组合关系的,没有创建类设计模式那么简单,有的还比较复杂,尽量简洁的方式介绍,仍然是以前的方式,不重要的一提而过。
结构类设计模式包含的设计模式如下图:
简单说明:
- 代理模式,代理让我想到的像写专利代理,也许我们对自己写的专利内容比较熟悉,但是,我们对专利文档的格式要求,规范要求不熟悉,如果我们自己来申请专利就很麻烦,专利代理替我们修改相关的专利文档,提交给专利局审批,方便了我们,将专利申请前后的建档等工作隐藏在我们专利文档的前后,让我们可以专注于文档编写。
还比如我们开发中如果要记录前后的调用日志,进行访问鉴权都可以用代理模式。
- 桥接模式,桥接模式有两个方式:1)将抽象和它实现的实例分离开来,可以灵活替换不同的实现实例,经典的例子就是JDBC驱动,我们把JDBC的驱动常用的操作抽象,比如连接获取,语句准备,语句执行,结果集的获取的抽象的一系列操作,和它们涉及到的具体实践MySqlDriver,OracleDriver等实现分离开来,采用配置的方式灵活替换;2)一个类存在了二个或二个以上独立变化的维度,如果采用一个类增加了耦合度,我们把这两个变化的维度分离开来,让他们可以独立变化。
3.装饰器模式,正如名字一样,装饰嘛,从字面理解是为现有的东西增加点点缀,更好看些,装饰器模式也类似,就是为了现有的功能增加些其他功能,比如Java的IO中的BufferedInputStream 即为读取流增加带缓存的功能。
组合模式,如果数据可以表示成树状结构,比如部门和员工,那么我们可以采用让它们继承相同基类的办法,用统一的遍历不区分的处理组合和个体结构。
享元模式,内存中有大量相同的各类对象,各类对象都是不变的,我们可以将同一类的对象只保存一份,达到节约内存的目标,比如如果做个网络象棋游戏,我们可以把棋子的基本信息只保留一份,被各个棋局所共享。
适配器模式,适配适配故名思意,是为了改变现有的接口,让原来的接口适应新的接口形式,生活中我们常用的转接头就是个例子,适配的原因在于我们无法改变老的接口,也无法改变新的接口,只能建个中间层做转换。
7.门面模式,我们在开发接口的时候,为了更好的复用,将接口的粒度设置的比较小,但是比较小的话,外部使用起来不方面,所以为了让外部调用起来更方面,我们把几个小粒度的接口,再包装下成大的方便使用的接口,这个就是门面模式。
一 代理模式
正如我们上面所说的,代理模式为现有的代码引入附加功能,比如对每个函数调用都打印耗时啊,都要做接口性能统计啊,比如要统一做接口鉴权了,我们都可以采用这种模式。
示意图如下:
对于我们可以掌控的,我们可以让代理类和被代理类继承相同的接口,代理类依赖被代理类,通过外部注入方式引入,调用的时候,再被代理执行的方法前后添加需要增加的功能。
如果被代理类无法掌控,比如是依赖第三方类,我们可以新定义代理类继承被代理类,如上图。
不过如果我们需要代理的类很多,比如controller中我们很多请求都要控制权限,都要进行性能统计,每个都继承或实现相同接口的方法去写的话很麻烦,怎么办,其实也有好的办法,那就是采用动态代理的方式。
动态代理如果没接触过理解起来还是有些难度。首先要理解的是java的对象生成.
A a = new A();
上述是简单的创建java对象的语句,执行过程如下:
- java通过ClassLoader加载java的字节码生成Class对象(会执行静态方法),放在方法区。
- 在JVM堆上开辟一个空间用来存放新对象。
- 调用构造函数,来初始化这个空间为对象。
- 将堆栈上引用a指向堆上这个对象。
所以java的对象创建不一定要写个class类,我们如果可以直接得到Class对象,利用Class对象来创建对象。Class对象里面就是一些方法,构造函数和属性,我们又知道被代理类,那么是否可以通过被代理类来动态获得一个Class对象,这个我们通过这个Class对象来生成实际的代理对象。
举个例子:
package com.abc.mdpt.test;
import java.lang.reflect.*;
interface IOperator {
int getSum(int a, int b);
int getSub(int a, int b);
}
class Operator implements IOperator {
@Override
public int getSum(int a, int b) {
return a+b;
}
@Override
public int getSub(int a, int b) {
return a-b;
}
}
public class TestReflect {
public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
/* @param loader the class loader to define the proxy class
* @param interfaces the list of interfaces for the proxy class to implement*/
Class sumCal = Proxy.getProxyClass(Operator.class.getClassLoader(), Operator.class.getInterfaces());
Constructor[] cs = sumCal.getDeclaredConstructors();
for (Constructor ca : cs) {
System.out.println(ca);
}
Constructor constructor = sumCal.getDeclaredConstructor(InvocationHandler.class);
IOperator iOperator = (IOperator) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理之前记录日志");
long start = System.currentTimeMillis();
Operator operator = new Operator();
Integer re = (Integer) method.invoke(operator, args);
long end = System.currentTimeMillis();
System.out.println("代理之后记录:cost :"+ String.valueOf(end-start));
return re;
}
});
System.out.println("sum:"+ iOperator.getSum(111, 333));
System.out.println("sub:" + iOperator.getSub(11, 11333));
}
}
上面是个简单的动态代理,在操作前后打印日志信息,计算函数调用耗时,打印结果如下:
public com.abc.mdpt.test.$Proxy0(java.lang.reflect.InvocationHandler)
代理之前记录日志
代理之后记录:cost :0
sum:444
代理之前记录日志
代理之后记录:cost :1
sub:-11322
我们利用JDK内部Proxy类来生成一个动态代理的Class对象,然后通过newInstance来创建一个动态代理的对象,参数是个InvocationHandler的对象,对象实现了invoke方法,在内部我们定义了被代理的对象,然后在执行些代理的方法。
上述代码写的不够灵活,改下:
private static Object getProxy(final Object target) throws Exception {
//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
Object proxy = constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理之前记录日志");
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("代理之后记录:cost :"+ String.valueOf(end-start));
return result;
}
});
return proxy;
}
public static void main(String[] args) throws Exception {
IOperator iOperator = (IOperator) getProxy(new Operator());
System.out.println("sum:"+ iOperator.getSum(111, 333));
System.out.println("sub:" + iOperator.getSub(11, 11333));
}
二 桥接模式
桥接模式按照GoF书的含义:将抽象和实现分离,使其可以独立变化; 还有一个解释就是类有两个或多个独立变化的维度,我们可以通过组合方式,让多个维度可以独立变化。
JDBC就是好的例子,JDBC是抽象,Driver就是实现,JDBC将数据库的无关的,被抽象出来的类库。
//加载及注册JDBC驱动程序
Class.forName("com.mysql.jdbc.Driver");
加载的时候会执行静态初始化,将Mysql的数据库的Driver注册到DriverManager类中,然后:
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
后面就执行sql查询等:
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
rs.getString(1);
rs.getInt(2);
}
第二种解释,我觉得更实用,通过将多个维度分离开来独立变化。举个例子:
public interface MsgSender {
void send(String message);
}
public class TelephoneMsgSender implements MsgSender {
private List telephones;
public TelephoneMsgSender(List telephones) {
this.telephones = telephones;
}
@Override
public void send(String message) {
//...
}
}
public class EmailMsgSender implements MsgSender {
// 与TelephoneMsgSender代码结构类似,所以省略...
}
public class WechatMsgSender implements MsgSender {
// 与TelephoneMsgSender代码结构类似,所以省略...
}
public abstract class Notification {
protected MsgSender msgSender;
public Notification(MsgSender msgSender) {
this.msgSender = msgSender;
}
public abstract void notify(String message);
}
public class SevereNotification extends Notification {
public SevereNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
public class UrgencyNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
如上代码,告警有不同的级别,告警的通知方式有电话通知,微信通知,邮件通知等,在使用的时候不同的告警级别采用不同的告警方式,而且要灵活配置,采用告警方式,如果不采用桥接模式,两种变化原因夹杂在一起,代码很乱,难以维护。采用桥接模式后,对于不同的告警模式,可以采用不同的告警方式,很灵活,将原来的N*M的类的组合变成了N+M的组合。
三 装饰器模式
装饰器故名思意是对一个类的包装,使其的功能更强,比如我们将普通的文件读取加上带缓存的文件读取,这个带缓存的读取流功能就是一种装饰,如下:
InputStream in = new FileInputStream("test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
装饰器从表面看来,挺麻烦,直接定义个带缓存的读文件的流不香嘛,如果从单个类的角度来看确实这样,但是如果了解java的流读取,知道除了文件读取流,还有数组流读取,还有Data的数据流读取,每个读取都耦合缓存读取功能,功能定义重复,万一读取缓存的功能我们写的代码有问题,那很多类都需要修改,我们通过装饰器模式,将缓存的功能和读取的类型分离开来,可以灵活组合;比如我们可以灵活的给数据读取加缓存,给管道读取加缓存功能。
在我看来,装饰器模式用在相似的一组类中,我们把这些类的功能进行分层,每层实现特定的功能,组合起来使用,达到复用的目的。
用继承模式来实现的话,如果需要多个功能,比如读取数据和读取缓存,那么继承关系就比较复杂。
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A implements IA {
public void f() { //... }
}
public class ADecorator implements IA {
private IA a;
public ADecorator(IA a) {
this.a = a;
}
public void f() {
// 功能增强代码
a.f();
// 功能增强代码
}
}
继承相同的基类或实现相同的接口,就可以这样用:
// 包装下
new ADecorator (new A());
四 组合模式
组合模式挺简单的,将组合和个体都继承相同的类或实现相同的接口,然后再操作的时候就可以统一操作,比如计算文件夹或文件大小。
public abstract class FileSystemNode {
protected String path;
public FileSystemNode(String path) {
this.path = path;
}
public abstract int countNumOfFiles();
public abstract long countSizeOfFiles();
public String getPath() {
return path;
}
}
public class File extends FileSystemNode {
public File(String path) {
super(path);
}
@Override
public int countNumOfFiles() {
return 1;
}
@Override
public long countSizeOfFiles() {
java.io.File file = new java.io.File(path);
if (!file.exists()) return 0;
return file.length();
}
}
public class Directory extends FileSystemNode {
private List subNodes = new ArrayList<>();
public Directory(String path) {
super(path);
}
@Override
public int countNumOfFiles() {
int numOfFiles = 0;
for (FileSystemNode fileOrDir : subNodes) {
numOfFiles += fileOrDir.countNumOfFiles();
}
return numOfFiles;
}
@Override
public long countSizeOfFiles() {
long sizeofFiles = 0;
for (FileSystemNode fileOrDir : subNodes) {
sizeofFiles += fileOrDir.countSizeOfFiles();
}
return sizeofFiles;
}
public void addSubNode(FileSystemNode fileOrDir) {
subNodes.add(fileOrDir);
}
public void removeSubNode(FileSystemNode fileOrDir) {
int size = subNodes.size();
int i = 0;
for (; i < size; ++i) {
if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
break;
}
}
if (i < size) {
subNodes.remove(i);
}
}
}
五 享元模式
享元模式,享可以理解为共享,目的是为了减少内存使用,共享对象,元即可以是对象也可以是对象一部分,这部分当然我们也抽象出来成对象,以达到共享的目的。
比如我们常见的word编辑器,每个字符都可以设置不同字体颜色和大小,如果我们每个字符都定义一个style对象,对象包含字体,颜色和大小,那么会占用大量的内存,我们可以将style对象共享了下,我们在一篇文章中涉及到的样式不会太多,这样会减少内存的使用,如下:
public class CharacterStyle {
private Font font;
private int size;
private int colorRGB;
public CharacterStyle(Font font, int size, int colorRGB) {
this.font = font;
this.size = size;
this.colorRGB = colorRGB;
}
@Override
public boolean equals(Object o) {
CharacterStyle otherStyle = (CharacterStyle) o;
return font.equals(otherStyle.font)
&& size == otherStyle.size
&& colorRGB == otherStyle.colorRGB;
}
}
public class CharacterStyleFactory {
private static final HashMap styles = new LinkedHashMap<>();
public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
String key = font.toString()+String.valueOf(size)+String.valueOf(colorRGB);
if (styles.contains(key)) {
return styles.get(key);
}else {
CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
styles.put(key,newStyle);
return newStyle;
}
}
}
public class Character {
private char c;
private CharacterStyle style;
public Character(char c, CharacterStyle style) {
this.c = c;
this.style = style;
}
}
public class Editor {
private List chars = new ArrayList<>();
public void appendCharacter(char c, Font font, int size, int colorRGB) {
Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
chars.add(character);
}
}
上述只是示例代码,像Font都没定义,所以编译时候会报错的。
六 适配器模式
适配器模式故名思意,是将不同的东西进行适配,类似我们的转接头。使用的场景有:
- 为了统一接口,将使用不同类库实现同一个功能的接口进行统一,比如我们需要过滤敏感词,使用了不同的敏感词库,每种库的使用方法不同,我们将接口进行统一适配。
- 接口设计缺陷的弥补。
- 替换外部依赖,比如我们开始时候依赖外部库,现在要替换成我们自己开发的库,这个库的接口那又搞的和依赖的外部库不一样,那就用适配器来适配下吧。
比如java 开发中常用SL4j 来进行各种日志适配的打印。
适配器模式即可以采用继承的方式,用新的接口包装下老的接口调用;又可以采用注入的方式,将新接口调用,转成对注入的老对象的调用。
// 类适配器: 基于继承
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
//...重新实现f2()...
}
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
// 对象适配器:基于组合
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor implements ITarget {
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); //委托给Adaptee
}
public void f2() {
//...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
七 门面模式
在我们开发中,为了复用,将方法的粒度设置的很小,但是这个类使用起来就不太舒服,需要调用多个方法,才能实现需要的功能,门面模式就把这些小方法的调用组合起来形成大的方法,方便的调用。
这样就兼容了复用性和易用性。门面模式也可以利用在更大的方面,比如我们为了复用设计很多子系统,为了易用,我们会将多个子系统的功能结合起来。比如我们知道C语言可以直接调用系统函数也可以调用库函数,调用库函数,需要更少的参数,逻辑更简单,原因就是库函数封装了系统函数,简化了我们的操作,这可以看成一种门面模式的应用。
八 诗词欣赏
《桃花庵歌》
版本一:
桃花坞里桃花庵,桃花庵下桃花仙;
桃花仙人种桃树,又摘桃花换酒钱。
酒醒只在花前坐,酒醉还来花下眠;
半醒半醉日复日,花落花开年复年。
但愿老死花酒间,不愿鞠躬车马前;车
尘马足富者趣,酒盏花枝贫者缘。
若将富贵比贫者,一在平地一在天;
若将贫贱丨比车马,他得驱驰我得闲。
别人笑我太疯癫,我笑他人看不穿;
不见五陵豪杰墓,无花无酒锄作田.