引言
家人们,谁懂啊,一觉醒来,我竟然重生了,摇身一变成了共享书店老板。最近有本神书爆火,叫《重生之霸道总裁学习设计模式拯救小娇妻》,一听这书名,妥妥的爆款预定。我这商业头脑瞬间被激活,毫不犹豫直接豪购一万本,堆在我的豪华仓库里,当时我就觉得自己简直是商业鬼才,下一个巴菲特非我莫属!
为了高效管理这些书,我翻出了祖传的图书管理系统。看着那系统界面,一个个录入书的信息,这不得把我手累废?我这程序员天赋瞬间觉醒,直接在后台敲起代码,一个 for 循环,“唰唰唰” 就 new 出一万个对象,把封面、页数、作者这些元数据一股脑全存进去了。当时我就想,我要是重生当程序员,那肯定是业界天花板,年薪百万不是梦!
没过多久,一群帅气多金的顾客涌进我的书店,清一色都要借这本《重生之霸道总裁学习设计模式拯救小娇妻》。看着书一本本借出去,租金数字蹭蹭往上涨,我笑得嘴角都快咧到后脑勺了。然而,乐极生悲啊家人们!就在我给一本书录入借阅状态的时候,“啪” 的一下,跟放鞭炮似的,系统毫无征兆地崩了!我当时就傻眼了,眼泪 “唰” 地一下就出来了,这不是玩我嘛!我捂着眼大喊:“停停停,这像话吗?” 这年轻的系统可真是不讲武德,说崩就崩,一点面子都不给!
没办法,我只能麻溜地把书店关了,赶紧排查系统问题。这不查不知道,一查吓一跳,祖传系统那小身板,内存直接爆了!我一拍脑门才反应过来,那一万个对象,每个都存着一样的元数据,再一个个录借阅信息,内存不爆才怪。我对着这些对象在心里喊:“你们耗子尾汁,好好反思反思!” 我就琢磨着,这些对象一开始都一模一样,就后面借阅信息不同,能不能就用一个原始对象,根据借阅信息来区分它们呢?嘿,这就引出了咱们今天要深入研究的设计模式 ——享元模式(Flyweight) !
故事纯属虚构,请勿当真,谁家好人做的管理系统不用数据库来持久化,直接写内存的?
概念
定义
享元模式是一种结构型设计模式,主要用于减少创建对象的数量,以降低内存占用和提高性能。
它通过共享已经存在的对象来避免创建重复的对象,这些被共享的对象称为享元。享元模式的核心思想是将对象的状态分为内部状态和外部状态,内部状态是可以共享的部分,通常是不变的,与对象的上下文无关;外部状态则是不可共享的部分,会随环境变化而变化,在使用享元对象时由客户端传入。
结构
- 享元工厂(Flyweight Factory):负责创建和管理享元对象。它维护一个享元池,用于存储已经创建的享元对象。当客户端请求一个享元对象时,享元工厂首先检查享元池中是否已经存在该对象,如果存在,则直接返回;如果不存在,则创建一个新的享元对象并放入享元池中。
- 抽象享元(Flyweight):定义了享元对象的公共接口,所有具体享元类都必须实现这个接口。这个接口通常包含一个方法,用于接收外部状态并根据内部状态和外部状态进行相应的操作。
- 具体享元(Concrete Flyweight):实现了抽象享元接口,是实际被共享的对象。它包含了内部状态,并实现了在接收外部状态后进行操作的具体逻辑。
- 非共享具体享元(Unshared Concrete Flyweight):在某些情况下,可能存在一些与享元对象结构相似,但不适合共享的对象,它们就是非共享具体享元。这些对象通常包含一些无法共享的状态或行为。
设计原则
单一职责原则
享元模式中,各类职责明确。享元工厂负责创建、管理享元对象;抽象享元定义通用行为;具体享元实现行为并处理内部状态,非共享具体享元处理非共享状态。这样让类功能清晰,降低耦合,便于维护和测试。
开闭原则
享元模式对扩展开放,对修改封闭。新增具体享元类型时,创建新类实现抽象享元接口即可,无需修改现有享元工厂和其他享元类,保证系统可扩展性和稳定性。
里氏替换原则
具体享元类作为抽象享元类的子类,能完全替换父类在系统中的位置,且不影响系统功能。具体享元类要实现抽象享元类的方法,保持语义和行为不变,保障代码继承关系稳定,提高复用性与可维护性。
依赖倒置原则
享元模式里,高层模块(如客户端)依赖抽象享元类,而非具体享元类。享元工厂也通过抽象享元类管理、提供对象。这降低模块耦合,让系统更灵活,具体享元类实现变化时,只要抽象接口不变,高层模块就无需修改。
接口隔离原则
抽象享元类接口应最小化,只含具体享元类必实现的方法,避免具体享元类实现无关方法,提升系统内聚性,降低耦合,让具体享元类更专注、灵活,易于维护。
示例
我们以引言中的例子编写示例代码。
在这个例子中,各个部分与享元模式的对应关系如下:
- 书:可以看作是享元对象,书中的元数据(封面、页数、作者等)是内部状态,这些信息在所有书籍实例中是相同的,可以共享。
- 图书管理系统:包含享元工厂和相关的管理逻辑。
- 书的借阅状态、书的编号:外部状态,每本书的编号、借出时间,借出对象等是不同的,不能共享。
类图
C++实现
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>// 抽象享元类
class BookInterface {
public:virtual void printInfo() const = 0;virtual ~BookInterface() = default;
};// 具体享元类,存储书的内部状态
class Book : public BookInterface {
private:std::string isbn;std::string title;std::string author;int pages;public:Book(const std::string &isbn, const std::string &title,const std::string &author, int pages): isbn(isbn), title(title), author(author), pages(pages) {}void printInfo() const override {std::cout << "ISBN: " << isbn << ", Title: " << title<< ", Author: " << author << ", Pages: " << pages << std::endl;}
};// 享元工厂类
class BookFactory {
private:std::unordered_map<std::string, std::shared_ptr<BookInterface>> books;public:std::shared_ptr<BookInterface> getBook(const std::string &isbn,const std::string &title,const std::string &author, int pages) {if (books.find(isbn) == books.end()) {books[isbn] = std::make_shared<Book>(isbn, title, author, pages);}return books[isbn];}
};// 非共享具体享元类,存储书的外部状态
class BookInstance {
private:std::shared_ptr<BookInterface> book;std::string bookId;std::string borrower;std::string borrowDate;public:BookInstance(const std::shared_ptr<BookInterface> &book,const std::string &bookId, const std::string &borrower,const std::string &borrowDate): book(book), bookId(bookId), borrower(borrower), borrowDate(borrowDate) {}void printInfo() const {book->printInfo();std::cout << "Book ID: " << bookId << ", Borrower: " << borrower<< ", Borrow Date: " << borrowDate << std::endl;}
};int main() {BookFactory factory;// 创建享元对象auto book1 =factory.getBook("123-4-56-789123-0","重生之霸道总裁学习设计模式拯救小娇妻", "总裁大人", 395);// 创建书的外部状态BookInstance instance1(book1, "001", "Alice", "2023-10-01");BookInstance instance2(book1, "002", "Bob", "2023-10-02");// 打印信息instance1.printInfo();instance2.printInfo();return 0;
}
Java实现
import java.util.HashMap;
import java.util.Map;// 抽象享元接口
interface BookInterface {void printInfo();
}// 具体享元类,存储书的内部状态
class Book implements BookInterface {private final String isbn;private final String title;private final String author;private final int pages;public Book(String isbn, String title, String author, int pages) {this.isbn = isbn;this.title = title;this.author = author;this.pages = pages;}@Overridepublic void printInfo() {System.out.println("ISBN: " + isbn + ", Title: " + title +", Author: " + author + ", Pages: " + pages);}
}// 享元工厂类
class BookFactory {private final Map<String, BookInterface> books = new HashMap<>();public BookInterface getBook(String isbn, String title, String author,int pages) {return books.computeIfAbsent(isbn,k -> new Book(isbn, title, author, pages));}
}// 非共享具体享元类,存储书的外部状态
class BookInstance {private final BookInterface book;private final String bookId;private final String borrower;private final String borrowDate;public BookInstance(BookInterface book, String bookId, String borrower,String borrowDate) {this.book = book;this.bookId = bookId;this.borrower = borrower;this.borrowDate = borrowDate;}public void printInfo() {book.printInfo();System.out.println("Book ID: " + bookId + ", Borrower: " + borrower +", Borrow Date: " + borrowDate);}
}// 主类
public class FlyweightPatternExample {public static void main(String[] args) {BookFactory factory = new BookFactory();// 创建享元对象BookInterface book1 = factory.getBook("123-4-56-789123-0", "重生之霸道总裁学习设计模式拯救小娇妻", "总裁大人",395);// 创建书的外部状态BookInstance instance1 =new BookInstance(book1, "001", "Alice", "2023-10-01");BookInstance instance2 =new BookInstance(book1, "002", "Bob", "2023-10-02");// 打印信息instance1.printInfo();instance2.printInfo();}
}
代码解释
BookInterface
类:这是一个抽象类(或接口),定义了printInfo()
方法,作为抽象享元类。Book
类:继承自BookInterface
类,实现了printInfo()
方法,包含书的内部状态(isbn
、title
、author
、pages
),是具体享元类。BookFactory
类:负责创建和管理Book
对象,通过getBook()
方法返回BookInterface
类型的智能指针,与BookInterface
存在创建关系。BookInstance
类:包含书的外部状态(bookId
、borrower
、borrowDate
),持有一个BookInterface
类型的智能指针,与BookInterface
存在使用关系。
享元模式流程说明
1. 定义抽象享元与具体享元
- 抽象享元(Flyweight):定义一个抽象接口,该接口声明了享元对象需要实现的操作。这个接口是所有具体享元类的公共规范,通常包含一个可以接收外部状态作为参数的方法,用于根据内部状态和传入的外部状态执行具体操作。
- 具体享元(Concrete Flyweight):实现抽象享元接口,包含对象的内部状态。内部状态是对象的固有属性,不随环境变化而改变,可在多个客户端之间共享。例如,在图形绘制系统中,颜色和形状等信息可以作为内部状态。
2. 创建享元工厂
- 享元工厂(Flyweight Factory):负责创建和管理享元对象。它维护一个享元池(通常是一个集合,如哈希表),用于存储已经创建的享元对象。享元工厂提供一个获取享元对象的方法,该方法接收一个标识(如键值)作为参数,用于查找或创建特定的享元对象。
3. 客户端请求享元对象
- 客户端向享元工厂请求一个享元对象,同时传入一个标识(键值)。这个标识用于唯一标识客户端需要的享元对象类型或具体实例。
4. 享元工厂处理请求
- 检查享元池:享元工厂接收到客户端的请求后,首先检查享元池中是否已经存在具有该标识的享元对象。
- 返回现有对象:如果享元池中存在该对象,享元工厂直接从享元池中取出该对象并返回给客户端。
- 创建新对象:如果享元池中不存在该对象,享元工厂会创建一个新的具体享元对象,并将其添加到享元池中,然后返回该新对象给客户端。
5. 客户端使用享元对象
- 客户端获取到享元对象后,将外部状态作为参数传递给享元对象的方法进行调用。外部状态是随环境变化而变化的状态,由客户端在使用时传入。例如,在图形绘制系统中,位置信息可以作为外部状态。
- 享元对象根据自身的内部状态和客户端传入的外部状态执行相应的操作,并将结果返回给客户端。
享元模式的优缺点
优点
1. 减少内存占用
享元模式的核心优势之一就是显著减少内存的使用量。在系统中,如果存在大量相似的对象,这些对象的部分状态是相同的(内部状态),将这些相同的状态提取出来并共享,可以避免为每个对象都重复存储这些相同的信息。例如,在一个图形绘制系统中,有大量的圆形对象,这些圆形的颜色、半径等属性可能是相同的,使用享元模式可以将这些相同的属性作为内部状态共享,只创建一个包含这些属性的对象,而不是为每个圆形都创建一个完整的对象,从而大大降低了内存的占用。
2. 提高性能
由于减少了对象的创建数量,系统在运行过程中创建和销毁对象的开销也随之减少。对象的创建和销毁是比较耗时的操作,尤其是在创建大量对象时,会对系统性能产生较大影响。享元模式通过共享对象,避免了重复创建相同的对象,减少了系统的开销,提高了系统的响应速度和运行效率。同时,共享对象的复用也减少了垃圾回收的压力,进一步提升了性能。
3. 增强可维护性和可扩展性
享元模式将对象的状态分为内部状态和外部状态,使得代码的结构更加清晰。内部状态集中管理,外部状态由客户端在使用时传入,这种分离方式使得代码的逻辑更加清晰,易于理解和维护。当需要对享元对象进行修改或添加新的功能时,只需要在相应的具体享元类中进行操作,不会影响到其他部分的代码,提高了代码的可维护性。此外,当需要增加新的享元对象类型时,只需要创建新的具体享元类并实现抽象享元接口,而不需要对现有代码进行大规模修改,具有良好的可扩展性。
4. 遵循设计原则
享元模式遵循了一些重要的设计原则,如单一职责原则、开闭原则等。具体享元类只负责处理内部状态和实现抽象享元接口定义的方法,职责明确;对扩展开放,对修改封闭,当需要添加新的享元对象类型时,只需要扩展具体享元类,而不需要修改现有代码。
缺点
1. 增加系统复杂性
享元模式需要将对象的状态进行分离和管理,这增加了系统的设计和实现难度。开发人员需要仔细区分对象的内部状态和外部状态,并确保外部状态在使用时正确传递给享元对象。同时,享元工厂的管理也需要一定的成本,需要考虑享元对象的创建、销毁和维护等问题,这使得系统的结构变得更加复杂,增加了开发和维护的成本。
2. 可能不适用于所有场景
享元模式并不适用于所有情况。对于那些对象状态差异较大,难以区分内部状态和外部状态的场景,或者共享对象带来的收益不明显的场景,使用享元模式可能会增加不必要的复杂性,而不会带来明显的性能提升。例如,如果对象的状态大部分都是不同的,共享的部分很少,那么使用享元模式可能会得不偿失。
3. 维护外部状态的复杂性
由于享元对象的部分状态是外部传入的,客户端需要正确地维护这些外部状态,确保享元对象在不同的上下文环境中能够正确地工作。这可能会增加客户端代码的复杂性和维护成本。如果外部状态管理不当,可能会导致享元对象的行为出现异常,影响系统的正确性。
4. 线程安全问题
在多线程环境下使用享元模式时,需要考虑线程安全问题。如果多个线程同时访问和修改享元对象的状态,可能会导致数据不一致的问题。需要采取适当的同步机制来保证线程安全,这进一步增加了系统的复杂性。
注意事项
状态区分与管理
- 清晰划分内部和外部状态
- 内部状态是可共享且不随环境变化的部分,外部状态则会根据使用场景而改变。必须准确区分这两种状态,否则会影响享元模式的效果。例如在一个文本排版系统中,字符的字体、字号属于内部状态,而字符在页面上的位置则是外部状态。
- 若状态划分错误,将本应作为外部状态的部分也共享,会导致不同使用场景下出现数据错误;若将内部状态作为外部状态处理,则无法实现对象共享,失去了享元模式节省内存的优势。
- 外部状态的正确传递
- 由于享元对象依赖客户端传入外部状态来完成具体操作,所以客户端要确保在调用享元对象方法时,正确传递所需的外部状态。比如在一个图形绘制系统中,当使用共享的圆形对象绘制不同位置的圆时,要准确传入圆心坐标等外部状态。
- 如果外部状态传递错误或不完整,可能会使享元对象的行为不符合预期,导致系统出现逻辑错误。
享元工厂的管理
- 线程安全问题
- 在多线程环境下,享元工厂负责创建和管理享元对象,可能会出现多个线程同时请求创建相同享元对象的情况。如果不进行同步控制,可能会导致重复创建对象,破坏享元模式的共享机制。
- 可以使用同步锁等机制来保证线程安全。例如在 Java 中,可以使用
synchronized
关键字或ReentrantLock
来对享元工厂的创建对象方法进行同步。
- 享元池的维护
- 享元工厂通常使用一个享元池(如哈希表)来存储已创建的享元对象。随着系统运行,享元池中的对象可能会越来越多,占用大量内存。因此需要考虑对享元池进行合理的维护,例如设置对象的最大数量,当达到上限时进行对象的清理操作。
- 同时,要注意清理策略的合理性,避免误删正在使用的对象。比如可以采用引用计数或定时清理等方式。
系统复杂性与性能权衡
- 增加的系统复杂度
- 享元模式将状态分离、引入享元工厂等操作,会增加系统的设计和实现复杂度。开发人员需要花费更多的精力来理解和维护代码,尤其是在状态管理和对象交互方面。
- 在决定是否使用享元模式时,需要权衡系统复杂度的增加与节省内存、提高性能带来的收益。如果系统中相似对象数量较少,使用享元模式可能会得不偿失。
- 性能开销
- 虽然享元模式的初衷是提高性能,但在某些情况下,也可能会引入额外的性能开销。例如,享元工厂在查找和创建对象时,需要进行哈希查找等操作,这会带来一定的时间开销。
- 要确保享元模式的使用不会成为系统的性能瓶颈,在设计和实现过程中进行性能测试和优化。
可维护性和扩展性
- 代码的可维护性
- 由于享元模式涉及多个类和复杂的状态管理,代码的可维护性可能会受到影响。在修改或扩展享元对象的功能时,需要仔细考虑对整个系统的影响,避免引入新的问题。
- 可以通过良好的代码注释、模块化设计等方式来提高代码的可维护性。
- 系统的扩展性
- 当需要增加新的享元对象类型或修改享元对象的内部状态时,要确保系统具有良好的扩展性。例如,在享元工厂中添加新的对象创建逻辑时,要尽量减少对现有代码的修改。
- 可以采用面向接口编程、遵循开闭原则等设计思想来提高系统的扩展性。
享元模式与单例模式的对比
对比维度 | 享元模式 | 单例模式 |
---|---|---|
设计目的 | 减少内存占用和提高性能,通过共享相似对象实现 | 确保一个类在系统中只有一个实例,并提供全局访问点 |
对象数量 | 可存在多个享元对象,相同内部状态的对象共享实例 | 整个系统生命周期内,一个类仅一个实例 |
状态管理 | 将对象状态分为内部(可共享)和外部(随环境变)状态 | 不区分内外部状态,单例对象自身管理状态 |
应用场景 | 适用于大量细粒度且部分状态可共享的对象场景,如文本编辑、图形处理 | 适用于需确保唯一实例且全局访问的场景,如数据库连接池、日志记录器 |
实现方式 | 依赖享元工厂创建和管理享元对象,使用享元池存储对象 | 有饿汉式、懒汉式等多种实现,懒汉式需考虑线程安全 |
应用场景
存在大量相似对象
当系统中存在大量细粒度的对象,并且这些对象具有很多相同的属性时,使用享元模式可以显著减少内存的使用。例如在一个文本编辑软件中,会有大量的字符对象,这些字符可能具有相同的字体、字号、颜色等属性。通过享元模式,将这些相同的属性作为内部状态进行共享,避免为每个字符都创建一个包含所有属性的完整对象,从而节省大量内存。
内存资源紧张
如果系统的内存资源有限,而又需要处理大量的对象,享元模式是一个很好的解决方案。它通过共享对象,减少了对象的创建数量,降低了内存占用。比如在一些嵌入式系统或者移动应用中,内存资源相对较少,使用享元模式可以优化系统性能,避免因内存不足导致的系统崩溃或运行缓慢。
对象状态可区分
能够清晰地将对象的状态分为内部状态和外部状态是使用享元模式的关键条件。内部状态是对象的固有属性,不随环境变化而改变,可以被多个对象共享;外部状态则随环境变化而变化,由客户端在使用时传入。例如在一个图形绘制系统中,图形的颜色、形状等属性可以作为内部状态共享,而图形的位置、大小等属性可以作为外部状态由客户端根据具体需求进行设置。
系统性能要求高
频繁创建和销毁对象会消耗大量的系统资源,影响系统的性能。享元模式通过共享对象,避免了重复创建相同的对象,减少了对象创建和销毁的开销,从而提高了系统的性能。特别是在对性能要求较高的实时系统、游戏开发等领域,享元模式可以显著提升系统的响应速度和运行效率。
多次重复使用相同对象
如果某些对象在系统中会被多次重复使用,使用享元模式可以提高对象的复用率。例如在一个数据库连接池中,数据库连接对象会被多个数据库操作重复使用,通过享元模式将这些连接对象进行共享,可以避免频繁创建和关闭数据库连接,提高数据库操作的性能。