【重温设计模式】结构型设计模式之门面模式,组合模式,享元模式
一、门面模式
1、为什么使用门面模式
门面模式。门面模式原理和实现都特别简单,应用场景也比较明确,主要在接口设计方面使用。
门面模式:为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
应用场景
1. 解决易用性问题:
- 设计原则、思想、模式很多都是相通的,是同一个道理不同角度的表述。实际上,从隐藏实现复杂性,提供更易用接口这个意图来看,门面模式有点类似之前讲到的迪米特法则(最少知识原则)和接口隔离原则:两个有交互的系统,只暴露有限的必要的接口。除此之外,门面模式还有点类似之前提到封装、抽象的设计思想,提供更抽象的接口,封装底层实现细节。
2、解决性能问题:
- 我们通过将多个接口调用替换为一个门面接口调用,减少网络通信成本
3、解决分布式事务问题
- 有两个业务领域模型,用户和钱包。这两个业务领域模型都对外暴露了一系列接口,比如用户的增删改查接口、钱包的增删改查接口。假设有这样一个业务场景:在用户注册的时候,我们不仅会创建用户(在数据库 User 表中),还会给用户创建一个钱包(在数据库的 Wallet 表中)。
- 利用数据库事务或者 Spring 框架提供的事务(如果是 Java 语言的话),在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。这就要求两个 SQL 操作要在一个接口中完成,所以,我们可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行两个 SQL 操作。
2、门面模式的案例
门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。比如,Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。再比如,Linux 的 Shell 命令,实际上也可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。
二、组合模式
1、为什么使用组合模式
- 将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。
- 组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
- 组合模式,将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。
2、组合模式的案例
//文件系统的node 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); } } }
三、享元模式
1、为什么使用享元模式
- 所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
- “不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。
享元模式和单例,缓存,池化技术的区别
- 应用享元模式是为了对象复用,节省内存。单例模式,是为了保证全局唯一。
- 在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
- 池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
2、享元模式的案例
// 享元类 public class ChessPieceUnit { private int id; private String text; private Color color; public ChessPieceUnit(int id, String text, Color color) { this.id = id; this.text = text; this.color = color; } public static enum Color {RED, BLACK} // ...省略其他属性和getter方法... } public class ChessPieceUnitFactory { private static final Map pieces = new HashMap<>(); static { pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK)); pieces.put(2, new ChessPieceUnit(2, "馬", ChessPieceUnit.Color.BLACK)); //...省略摆放其他棋子的代码... } public static ChessPieceUnit getChessPiece(int chessPieceId) { return pieces.get(chessPieceId); } }
java的Interger,Long等包装类型都采用的享元模式
public static void main(String[] args) { Integer integer=12; Integer integer1=12; Integer integer2=198; Integer integer3=198; //享元模式对Integer中的-128~127进行了缓冲,故地址是相等的,该范围以外就会new两个对象 System.out.println(integer == integer1);//true System.out.println(integer2 == integer3);//false } //自动装箱:会从缓存中获取享元对象 public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } //享元对象缓存工厂 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }