零基础设计模式——结构型模式 - 享元模式
第三部分:结构型模式 - 享元模式 (Flyweight Pattern)
在学习了外观模式如何简化复杂子系统的接口后,我们来探讨享元模式。享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
- 核心思想:运用共享技术有效地支持大量细粒度的对象。
享元模式 (Flyweight Pattern)
“运用共享技术有效地支持大量细粒度的对象。” (Use sharing to support large numbers of fine-grained objects efficiently.)
想象一下你在开发一个文字处理器。文档中可能包含成千上万个字符。如果为每个字符(如 ‘a’, ‘b’, ‘c’ 等)都创建一个独立的对象,并且每个对象都存储其字形、字体、大小等信息,那么内存消耗会非常巨大。
享元模式通过共享那些状态不随上下文变化的对象来解决这个问题。对于字符来说:
- 内部状态 (Intrinsic State):字符的字形(如 ‘A’ 的形状)。这是可以共享的,因为字符 ‘A’ 无论出现在文档的哪个位置,其基本形状是不变的。
- 外部状态 (Extrinsic State):字符在文档中的位置、颜色、应用的特定格式(如粗体、斜体)。这是不可共享的,因为它随字符出现的上下文而变化。外部状态由客户端在需要时提供给享元对象。
享元工厂会缓存并复用具有相同内部状态的字符对象。当需要一个字符时,客户端向工厂请求,工厂检查缓存中是否已有该字符,有则返回,无则创建、缓存并返回。
1. 目的 (Intent)
享元模式的主要目的:
- 减少对象数量:通过共享相同的对象来显著减少系统中细粒度对象的数量。
- 节省内存:由于对象数量减少,内存占用也随之降低。
- 提高性能:减少了对象创建和销毁的开销,从而可能提高系统性能。
2. 生活中的例子 (Real-world Analogy)
-
围棋棋子:
- 棋盘上有大量的黑子和白子。每个黑子除了颜色(黑)和形状(圆形)这些内部状态外,没有其他本质区别。白子同理。
- 我们不需要为棋盘上的每一个黑子都创建一个全新的对象。实际上,我们只需要两个原型对象:一个代表所有黑子,一个代表所有白子(享元对象)。
- 棋子在棋盘上的位置(如 (3,4))是外部状态,由棋局(客户端)在落子时指定。
-
自行车租赁:
- 一个城市有很多共享单车。这些单车(享元对象)的物理属性(品牌、型号、颜色)是固定的(内部状态)。
- 用户租用单车时,单车被使用的地点、时长、当前用户等是外部状态。
- 租赁系统不需要为每个潜在用户或每个可能的骑行都预先准备一辆全新的、独一无二的自行车。它维护一个自行车池,按需分配。
-
印刷厂的铅字:
- 在活字印刷中,每个字符(如’A’, ‘B’, ‘中’, ‘国’)的铅字字模是固定的(内部状态)。
- 排版时,这些铅字被放置在不同的位置,并可能应用不同的油墨颜色(外部状态)。
- 印刷厂不需要为书中出现的每一个’A’都制作一个新的铅字模,而是重复使用同一个’A’字模。
-
Java 中的
String
常量池:- 当你创建一个字符串字面量,如
String s1 = "abc"; String s2 = "abc";
,Java 会检查字符串常量池。如果 “abc” 已经存在,s2
会直接引用池中的对象,而不是创建一个新的。这里的 “abc” 就是一个享元对象。
- 当你创建一个字符串字面量,如
3. 结构 (Structure)
享元模式通常包含以下角色:
- Flyweight (享元接口):所有具体享元类的超类或接口,通过这个接口,Flyweight 可以接受并作用于外部状态。
- ConcreteFlyweight (具体享元类):实现了 Flyweight 接口,并为内部状态增加存储空间。ConcreteFlyweight 对象必须是可共享的。它所存储的状态必须是内部的,即它必须独立于 ConcreteFlyweight 对象的场景。
- UnsharedConcreteFlyweight (非共享具体享元类):并非所有的 Flyweight 子类都需要被共享。Flyweight 接口使共享成为可能,但它并不强制共享。在 Flyweight 层次结构中,UnsharedConcreteFlyweight 对象通常将 ConcreteFlyweight 对象作为子节点(如果它们形成了某种结构,如组合模式中的情况)。
- FlyweightFactory (享元工厂):创建并管理 Flyweight 对象。当客户端请求一个 Flyweight 时,工厂提供一个已创建的实例或者创建一个(如果不存在的话),并将其存储起来以便将来复用。
- Client (客户端):维持一个对 Flyweight 的引用。计算或存储一个或多个 Flyweight 的外部状态。
工作流程:
- 客户端需要一个享元对象时,向
FlyweightFactory
请求。 - 客户端传递用于识别享元对象的键(通常基于内部状态)。
FlyweightFactory
检查其缓存(如一个Map
)中是否已存在具有该键的享元对象。- 如果存在,工厂返回缓存中的对象。
- 如果不存在,工厂创建一个新的
ConcreteFlyweight
对象,用传入的键(或其对应的内部状态)初始化它,将其存入缓存,然后返回给客户端。
- 客户端接收到享元对象后,在调用其
operation()
方法时,需要传入外部状态。 - 享元对象的
operation()
方法利用传入的外部状态和自身的内部状态来完成操作。
UnsharedConcreteFlyweight
对象不由工厂管理,客户端可以直接创建和使用它们,它们通常用于那些不能完全共享的情况。
4. 适用场景 (When to Use)
享元模式在以下所有条件都满足时可以考虑使用:
- 应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成了很大的存储开销。
- 对象的大多数状态都可以变为外部状态。即对象可以被划分为内部状态(共享)和外部状态(随场景变化)。
- 剥离出对象的外部状态后,可以用相对较少的共享对象取代大量对象。
- 应用程序不依赖于对象标识。由于对象是共享的,所以不能假设每个对象都是唯一的实例。
例如:文本编辑器中的字符、图形系统中的图形元素(如点、线)、游戏中的粒子效果或大量重复的NPC(非玩家角色)等。
5. 优缺点 (Pros and Cons)
优点:
- 极大地减少了内存中对象的数量:通过共享对象,显著降低了内存消耗。
- 提高了性能:减少了对象创建和垃圾回收的开销。
- 将对象的内部状态和外部状态分离:使得对象结构更清晰。
缺点:
- 增加了系统的复杂性:需要分离内部状态和外部状态,并引入工厂类来管理享元对象,这使得系统设计和实现更为复杂。
- 外部状态的管理:客户端需要负责计算或存储外部状态,并在调用享元对象操作时正确传递,这可能给客户端带来额外的负担。
- 运行时开销:虽然节省了内存,但在运行时查找和传递外部状态可能会引入一些时间开销。不过通常情况下,内存节省带来的好处远大于这点开销。
- 线程安全问题:如果享元对象的方法需要修改内部状态(虽然通常不应该),或者工厂的实现不是线程安全的,那么多线程环境下可能会有问题。
6. 实现方式 (Implementations)
让我们以绘制不同颜色的圆形为例。圆形的“形状是圆”是内部状态,而圆心坐标、半径和颜色是外部状态。为了简化,我们假设“颜色”也是内部状态,而坐标和半径是外部状态。
享元接口 (Shape - Flyweight)
// shape.go (Flyweight interface)
package drawing// Shape 享元接口
type Shape interface {Draw(x, y, radius int) // 外部状态: x, y, radiusGetColor() string // 内部状态的一部分,用于工厂的key
}
// Shape.java (Flyweight interface)
package com.example.drawing.flyweight;// 享元接口
public interface Shape {void draw(int x, int y, int radius); // 外部状态: x, y, radiusString getColor(); // 内部状态的一部分,用于工厂的key
}
具体享元类 (Circle - ConcreteFlyweight)
// circle.go (ConcreteFlyweight)
package drawingimport "fmt"// Circle 具体享元类
type Circle struct {color string // 内部状态
}func NewCircle(color string) *Circle {fmt.Printf("Creating Circle of color: %s\n", color) // 观察创建过程return &Circle{color: color}
}func (c *Circle) GetColor() string {return c.color
}func (c *Circle) Draw(x, y, radius int) {fmt.Printf("Drawing a %s circle at (%d, %d) with radius %d\n", c.color, x, y, radius)
}
// Circle.java (ConcreteFlyweight)
package com.example.drawing.flyweight;// 具体享元类
public class Circle implements Shape {private final String color; // 内部状态 (final 确保不可变)public Circle(String color) {System.out.println("Creating Circle of color: " + color); // 观察创建过程this.color = color;}@Overridepublic String getColor() {return color;}@Overridepublic void draw(int x, int y, int radius) {System.out.printf("Drawing a %s circle at (%d, %d) with radius %d%n", color, x, y, radius);}
}
享元工厂 (ShapeFactory - FlyweightFactory)
// shape_factory.go (FlyweightFactory)
package drawingimport ("fmt""sync"
)// ShapeFactory 享元工厂
type ShapeFactory struct {circleMap map[string]*Circle // Go的map不是线程安全的,实际应用需加锁lock sync.Mutex
}var factoryInstance *ShapeFactory
var once sync.Once// GetShapeFactory 获取工厂单例 (线程安全)
func GetShapeFactory() *ShapeFactory {once.Do(func() {factoryInstance = &ShapeFactory{circleMap: make(map[string]*Circle),}})return factoryInstance
}// GetCircle 根据颜色获取圆形享元对象
func (sf *ShapeFactory) GetCircle(color string) *Circle {sf.lock.Lock()defer sf.lock.Unlock()circle, exists := sf.circleMap[color]if !exists {circle = NewCircle(color) // 创建新的享元对象sf.circleMap[color] = circle}return circle
}func (sf *ShapeFactory) GetTotalCirclesCreated() int {sf.lock.Lock()defer sf.lock.Unlock()return len(sf.circleMap)
}
// ShapeFactory.java (FlyweightFactory)
package com.example.drawing.factory;import com.example.drawing.flyweight.Circle;
import com.example.drawing.flyweight.Shape;import java.util.HashMap;
import java.util.Map;// 享元工厂
public class ShapeFactory {// 使用HashMap存储享元对象,key为颜色private static final Map<String, Shape> circleMap = new HashMap<>();// 根据颜色获取圆形享元对象// synchronized 保证线程安全,或者使用 ConcurrentHashMappublic static synchronized Shape getCircle(String color) {Circle circle = (Circle) circleMap.get(color);if (circle == null) {circle = new Circle(color); // 创建新的享元对象circleMap.put(color, circle); // 存入池中System.out.println("Putting circle with color " + color + " into cache.");}return circle;}public static synchronized int getTotalCirclesCreated() {return circleMap.size();}
}
客户端使用
// main.go (示例用法)
/*
package mainimport ("./drawing""fmt""math/rand""time"
)var colors = []string{"Red", "Green", "Blue", "Yellow", "Black"}func main() {rand.Seed(time.Now().UnixNano())factory := drawing.GetShapeFactory()fmt.Println("--- Client: Drawing 20 circles ---")for i := 0; i < 20; i++ {color := colors[rand.Intn(len(colors))] // 随机选择颜色circle := factory.GetCircle(color) // 从工厂获取享元对象x := rand.Intn(100)y := rand.Intn(100)radius := rand.Intn(50) + 5circle.Draw(x, y, radius) // 传递外部状态进行绘制}fmt.Printf("\nTotal distinct circle objects created: %d\n", factory.GetTotalCirclesCreated())// 尽管绘制了20次,但实际创建的Circle对象数量最多为颜色种类数 (5)
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.drawing.factory.ShapeFactory;
import com.example.drawing.flyweight.Shape;import java.util.Random;public class Main {private static final String[] colors = {"Red", "Green", "Blue", "Yellow", "Black"};private static final Random random = new Random();public static void main(String[] args) {System.out.println("--- Client: Drawing 20 circles ---");for (int i = 0; i < 20; ++i) {String color = getRandomColor(); // 随机选择颜色Shape circle = ShapeFactory.getCircle(color); // 从工厂获取享元对象int x = getRandomCoordinate();int y = getRandomCoordinate();int radius = getRandomRadius();circle.draw(x, y, radius); // 传递外部状态进行绘制}System.out.printf("%nTotal distinct circle objects created: %d%n", ShapeFactory.getTotalCirclesCreated());// 尽管绘制了20次,但实际创建的Circle对象数量最多为颜色种类数 (5)}private static String getRandomColor() {return colors[random.nextInt(colors.length)];}private static int getRandomCoordinate() {return random.nextInt(100);}private static int getRandomRadius() {return random.nextInt(50) + 5;}
}
*/
7. 与其他模式的关系
- 组合模式 (Composite):享元模式常与组合模式一起使用,用来表示一个层次结构(如文档中的字符、段落、页面)。组合中的叶子节点可以是享元对象。
- 工厂模式 (Factory Method / Abstract Factory):享元工厂通常使用工厂方法来创建享元对象。
- 单例模式 (Singleton):享元工厂本身可以是单例,以确保全局只有一个工厂实例管理所有享元对象。
- 状态模式 (State) / 策略模式 (Strategy):这些模式的对象有时也可以实现为享元。例如,如果多个上下文对象共享相同的状态对象或策略对象。
8. 总结
享元模式是一种优化模式,通过共享对象来减少内存使用和提高性能,特别适用于系统中存在大量相似的细粒度对象的情况。它将对象的状态分为内部状态(可共享)和外部状态(由客户端提供),并使用工厂来管理和复用享元对象。虽然它会增加一些实现的复杂性,但在合适的场景下,其带来的资源节省效益是显著的。
记住它的核心:共享对象,分离内外状态,减少内存占用。