结构型模式下
1、享元模式FlyWeight
面向对象程序设计的过程中,有时候会面临创建大量相同或者相似对象实例的问题,创建那么多的对象会耗费很多的系统资源,他是系统性能提高的一个瓶颈。
例如图像中的坐标点或者颜色,例如地图数据中的坐标或建筑物
这时候如果能够把他们相同的部分提取出来,则能节约大量的系统资源。这就是享元模式的产生背景。
享元,就是共享元素的意思。
1.1 享元模式的定义与特点
享元模式指的是将对象划分为内部状态和外部状态,从而减少共享对象数量的一种模式。可以共享的内容称作内部状态,需要外部环境来设置的不能共享的的内容称作外部状态。其中外部状态和内部状态是相互独立的,外部状态的变化不会引起内部状态的变化。由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。也就是说,享元模式的本质是分离与共享 : 分离变与不变,并且共享不变。把一个对象的状态分成内部状态和外部状态,内部状态即是不变的,外部状态是变化的;然后通过共享不变的部分,达到减少对象数量并节约内存的目的。
享元对象主要的优点是:相同的对象只要保存一份,这降低了系统中细粒度对象主要给内存带来的压力。
缺点是:为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性,读取享元模式的外部状态会使得运行时间稍微变长。
1.2 享元模式的结构与实现
享元模式的主要角色如下:
(1)抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入
(2)具体享元角色(concrete Flyweight)实现抽象享元角色中所规定的的接口。
(3)非享元角色(Unsharable Flyweight)是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中
(4)享元工厂角色(Flyweight Factory):负责创建和管理享元角色,当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户,不存在的话,创建一个新的享元对象。用于存储具有相同内部状态的享元对象)。在享元模式中,共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称细粒度对象。
享元模式的类结构图如下:
图中的Unsharable Flyweight是非享元角色,里面包含了非共享的外部状态信息info,而Flyweight是抽象享元角色,里面包含享元方法operation(UnsharedConcrete Flyweight state),非享元的外部模式以参数的形式通过该方法传入;ConcreteFlyweight是具体享元角色,包含关键字key,它实现抽象享元接口;FlyweightFactory是享元工厂角色,它通过关键字key来管理具体享元;客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。
实现代码如下:
//抽象享元角色 interface FlyWeight{ public void operation(UnsharedConcreteFlyweight state); } //具体享元角色 class ConcreteFlyweight implements FlyWeight{ private String key; ConcreteFlyweight(String key){ this.key = key; System.out.println("具体享元" + key + "被创建!"); } @Override public void operation(UnsharedConcreteFlyweight outState) { System.out.println("具体享元" + key + "被调用"); System.out.println("非享元信息是 :" + outState.getInfo()); } } //享元工厂角色 class FlyweightFactory{ private HashMap<String, FlyWeight> flyweights = new HashMap<String, FlyWeight>(); public FlyWeight getFlyWeight(String key){ FlyWeight flyWeight = (FlyWeight) flyweights.get(key); if(flyWeight != null){ System.out.println("具体享元" + key + "已经存在,被成功获取!"); }else{ flyWeight = new ConcreteFlyweight(key); flyweights.put(key, flyWeight); } return flyWeight; } } //非享元角色 class UnsharedConcreteFlyweight{ private String info; UnsharedConcreteFlyweight(String info){ this.info = info; } public String getInfo(){ return info; } public void setInfo(String info){ this.info = info; } } public class TestFlyWeightPattern { public static void main(String[] args) { FlyweightFactory factory = new FlyweightFactory(); FlyWeight flyWeight1 = factory.getFlyWeight("a"); FlyWeight flyWeight2 = factory.getFlyWeight("b"); FlyWeight flyWeight3 = factory.getFlyWeight("c"); FlyWeight flyWeight4 = factory.getFlyWeight("a"); FlyWeight flyWeight5 = factory.getFlyWeight("d"); FlyWeight flyWeight6 = factory.getFlyWeight("b"); FlyWeight flyWeight7 = factory.getFlyWeight("a"); flyWeight1.operation(new UnsharedConcreteFlyweight("第一次调用a")); flyWeight2.operation(new UnsharedConcreteFlyweight("第二次调用b")); flyWeight3.operation(new UnsharedConcreteFlyweight("第三次调用c")); flyWeight4.operation(new UnsharedConcreteFlyweight("第一次调用a")); flyWeight5.operation(new UnsharedConcreteFlyweight("第一次调用d")); flyWeight6.operation(new UnsharedConcreteFlyweight("第一次调用b")); flyWeight7.operation(new UnsharedConcreteFlyweight("第一次调用a")); } }
通过执行结果可以看出,在这个对象池(HashMap)中,一直都只有一个对象存在,第一次使用的时候创建对象,之后的每次调用都用的是那个对象,不会再重新创建。
其实在Java中就存在这种类型的实例:String!以及Integer的Valueof(int i)方法
在JDK 7.0中,JVM将字符串保存在放置于堆中字符串常量池中。
String s1 = "abc";
String s2 = "abc";
上述S1和S2其实指向的都是字符串常量池中的同一个字符串地址。这就类似享元模式,字符串一旦定义之后就可以被共享使用,因为它们是不可改变的,同时被多处调用也不会存在任何隐患。
同理 原来为了提高性能少创建对象,jdk吧-128到127都缓存起来了,所以这个范围内都返回同一个实例,不在这个范围的就new一个出来。
见valueOf源码:
public static Integer valueOf(int i) { return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128]; } /** * A cache of instances used by {@link Integer#valueOf(int)} and auto-boxing */ private static final Integer[] SMALL_VALUES = new Integer[256]; static { for (int i = -128; i < 128; i++) { SMALL_VALUES[i + 128] = new Integer(i); } }
1.3 享元模式的扩展
在前面介绍的享元模式中,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式和复合享元模式
(1) 单纯享元模式,这种享元模式中的所有的具体享元类都是可以共享的,去掉了非共享的具体享元类
(2) 复合享元模式,这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享。
2、组合模式composite
现实生活中,存在很多“整体-部分”的关系。例如公司里面的总部和部门,大学里面的学院和系。软件开发中也是这样,文件系统中的文件与文件夹,对这些简单对象和符合对象的处理,如果用组合模式来实现就会很方便。
1.1 组合模式的定义与结构
组合模式的定义:有时候又叫做部分-整体的模式,他是一种将对象组合成树状的层次结构的模式,用来表示“部分--整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
模式的结构:
抽象构件角色(Component):主要作用是为了将树叶构件和树状构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构建完成。
树叶构件角色(Leaf):是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中的声明的公共接口。
树枝构件角色(Composite):是组合中分支节点对象。它有子节点,它实现抽象构件角色中声明的接口,它主要的作用是存储和管理子部件。
组合模式分为透明的组合模式和安全式的组合模式:
(1)透明模式:由于声明构件声明了所有子类中全部方法,所以客户端无须区别树叶对象和树枝对象。对客户端来说是透明的。注意透明组合模式也是组合模式的标准形式。但是缺点是树叶构件本来没有add、remove以及getChild()方法,却要实现他们(空实现或者抛出异常),这样带来一些安全问题。结构图如下:
(2)安全方式:在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树枝构件没有对子对象的管理方法。这样避免了上一种方式的安全性问题。
但是由于叶子和分支有的不同的接口,客户端在调试是要知道树叶对象和树枝对象的存在。失去了透明性。其结构图如下:
模式的实现:
假如要访问集合 c0={leaf1,{leaf2,leaf3}} 中的元素,其对应的树状图如图所示。
下面给出透明式的组合模式的实现代码,与安全式的组合模式的实现代码类似,只要简单修改即可。
//抽象构件 interface Componente{ public void add(Componente c); public void remove(Componente c); public Componente getChild(int i); public void operation(); } //树叶构件 class Leaf implements Componente{ private String name; public Leaf(String name){ this.name = name; } @Override public void add(Componente c) { } @Override public void remove(Componente c) { } @Override public Componente getChild(int i) { return null; } @Override public void operation() { System.out.println("树叶"+ name + ":被访问!"); } } //树枝构件 class Composite implements Componente{ private ArrayList<Componente> children = new ArrayList<>(); @Override public void add(Componente c) { children.add(c); } @Override public void remove(Componente c) { children.remove(c); } @Override public Componente getChild(int i) { return children.get(i); } @Override public void operation() { for(Object obj : children){ ((Componente)obj).operation(); } } } public class TestCompositePattern { public static void main(String[] args) { Componente c1 = new Composite(); Componente c2 = new Composite(); Componente leaf1 = new Leaf("1"); Componente leaf2 = new Leaf("2"); Componente leaf3 = new Leaf("3"); c1.add(leaf1); c1.add(c2); c2.add(leaf2); c2.add(leaf3); c1.operation(); } }
1.2 组合模式的应用实例
1、实现一个简单的目录树,有文件夹和文件两种类型,可以以树状形式展示文件结构。
// 抽象构件角色(Component):主要作用是为了将树叶构件和树状构件声明公共接口
abstract class Compone{ public String getName() { throw new UnsupportedOperationException("无法获取名称"); } public void add(Compone component) { throw new UnsupportedOperationException("无法添加"); } public void remove(Compone component) { throw new UnsupportedOperationException("无法删除"); } public void print() { throw new UnsupportedOperationException("无法打印"); } public String getContent() { throw new UnsupportedOperationException("无法获取内容"); } } //实现文件夹类,继承 Component,属于树枝构件 // 定义一个 List<Component> 类型的componentList属性, // 用来存储该文件夹下的文件和子文件夹,并实现 getName、add、remove、print等方法 class Folder extends Compone { private String name; private List<Compone> componentList = new ArrayList<>(); private Integer level;//增加一个 level 属性,体现文件和文件夹的层次结构 public Folder(String name) { this.name = name; } public String getName() { return this.name; } public void add(Compone compone) { this.componentList.add(compone); } public void remove(Compone compone) { this.componentList.remove(compone); } @Override public void print() { System.out.println(this.getName()); if(this.level == null) { this.level = 1; } String prefix = ""; for (int i = 0; i <this.level ; i++) { prefix += " "; } for (Compone compone : this.componentList) { if(compone instanceof Folder) { ((Folder) compone).level = this.level + 1; } System.out.print(prefix); compone.print(); } this.level = null; } } //文件类 File,属于树叶构件, //继承Compone父类,实现 getName、print、getContent等方法 class File extends Compone { private String name; private String content; public File(String name, String content) { this.name = name; this.content = content; } @Override public String getName() { return this.name; } @Override public void print() { System.out.println(this.getName()); } @Override public String getContent() { return this.content; } } public class CompositePatternDesign { public static void main(String[] args) { Folder txtfolder = new Folder("Home文件夹"); File file1 = new File("test.txt", "测试内容"); File file2 = new File("test1.txt", "测试内容"); txtfolder.add(file1); txtfolder.add(file2); Folder codeFolder = new Folder("代码文件夹"); File readme = new File("CompositePatternDesign.java", "代码内容"); codeFolder.add(readme); Folder srcFolder = new Folder("配置文件夹"); File code1 = new File("configure.xml", "配置文件1"); srcFolder.add(code1); codeFolder.add(srcFolder); txtfolder.add(codeFolder); txtfolder.print(); } }
1.3 组合模式的的适用场景及优缺点
组合模式适用于以下的情景:
(1)在需要表示一个对象整体与部分的层次结构的组合
(2)要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用结构中的所有对象的场合。
(3)在一个使用面向对象语言开发的系统中需要处理一个树形结构。
(4) 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
组合模式的主要优点是可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
在组合模式中增加新的树枝构件和树叶构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
缺点是:使得设计更加复杂,客户端需要花更多时间理清类之间的层次关系;在增加新构件时很难对容器中的构件类型进行限制。
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术