享元模式
享元模式是设计模式中少数几个以提高系统性能为目的的模式之一。它的核心思想是:如果在一个系统中存在多个相同的对象,那么只需要共享一份对象的拷贝,而不必为每一次使用都创建新的对象。在享元模式中,由于需要构造和维护这些可以共享的对象,因此,常常会出现一个工厂类,用于维护和创建对象。
享元模式对性能提升的主要帮助有亮点:
(1)可以节省重复创建对象的开销,因为被享元模式维护的相同对象只会被创建一次,当创建对象比较耗时时,便可以节省大量时间。
(2)由于创建对象的数量减少,所以对系统内存的需求也减小,这将使得GC的压力也相应地降低,进而使得系统拥有一个更健康的内存结构和更快的反应速度。
享元模式的主要角色由享元工厂、抽象享元、具体享元类和主函数几部分组成。他们的功能如下:
角色 | 作用 |
享元工厂 | 用以创建具体享元类,维护相同的享元对象。它保证相同的享元对象可以被系统共享。即,期内部使用类类似单例模式的算法,当请求对象已经存在时,直接返回对象,不存在时,在创建对象。 |
抽象享元 | 定义需共享的对象的业务接口。享元类被创建出来总是为了实现某些特定的业务逻辑,而抽象享元便定义这些逻辑的语义行为 |
具体享元类 | 实现抽象享元类的接口,完成某一具体逻辑 |
Main | 使用享元模式的组件,通过享元工厂取得享元对象 |
基于以上角色,享元模式的结构图如下:
享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象。一般情况下,享元工厂会维护一个对象列表,当任何组件尝试获取享元类时,如果请求的享元类已经被创建,则直接返回已有的享元类;若没有,则创建一个新的享元对象,并将它加入到维护队列中。
享元模式的一个典型应用是在SAAS系统中。SAAS即As A Service,目前比较流行的一种软件应用模式。
以一个人事管理系统的SAAS软件为例,假设公司甲、乙、丙均为这个SAAS系统的用户,则定义每个公司为这套系统的一个租户。每个公司(租户)又各有100个员工。如果这个公司的所有员工都可以登陆这套系统查看自己的收入情况,并且为了系统安全,每个公司(租户)都拥有自己独立的数据库。为了是系统的设计最为合理,在这种情况下,便可以使用享元模式为每个租户分别提供工资查询的接口,而一个公司(租户)下的所有员工可以共享一个查询(因为一个租户下所有的员工数据都存放在一个数据库中,它门共享数据连接)。这样,系统只需要3个享元实例,就足以应付300个员工的查询请求。系统的结构如下图:
图中,ReportManagerFactory为享元工厂,负责创建具体的报表工具,它确保每个公司(租户)下所有的员工,都共享一个具体的享元实例(FinancialReportManager 或者EmployeeReportManager)。这样,当公司甲的两个员工登陆,进行财务查询时,系统不必为两个员工都新建FinancialReportManager,而可以让他们共享一个FinancialReportManager实例。
通过本示例,还可以进一步了解享元工厂和对象池的一个重要区别。在一个对象池中,所有的对象都是等价的,任意两个对象在任何使用场景中都可以被对象池中的其他对象代替。而在享元模式中,享元工厂所维护的所有对象都是不同的,任何两个对象间不能相互代替。如本例中,为公司甲创建的FinancialReportManagerA和为公司乙创建的FinancialReportManagerB分别对应了后台各自不同的数据库,因此,两者是不可能相互替代的。
本例中享元对象接口的实现如下,他用于创建一个报表。即,所有报表生成类将作为享元对象在一个公司(租户)中共享。
public interface IReportManager { public String createReport(); }
以下是两个报表生成的实例,分别对应员工财务收入报表和员工个人信息报表。他们都是具体的享元类。
public class FinancialReportManager implements IReportManager {//财务报表 protected String tenantId = null; //租户ID public FinancialReportManager(String tenantId) { this.tenantId = tenantId; } @Override public String createReport() { return "This is a financial report"; } }
public class EmployeeReportManager implements IReportManager {//员工报表 protected String tenantId = null; public EmployeeReportManager(String tenantId) { this.tenantId = tenantId; //租户ID } @Override public String createReport() { return "This is a employee report"; } }
最为核心的享元工厂类实现如下,它也是享元模式的精髓所在。它确保同一个公司(租户)使用相同的对象产生报表。这是相当有意义的,否则系统可能会为每个员工生成各自的报表对象,导致系统开销激增。
public class ReportManagerFactory { Map<String,IReportManager> financialReportManager = new HashMap<String,IReportManager>(); Map<String,IReportManager> employeeReportManager = new HashMap<String, IReportManager>(); IReportManager getFinancialReportManager(String tenantId) { IReportManager r = financialReportManager.get(tenantId);//通过租户ID获取享元 if(r == null){ r = new FinancialReportManager(tenantId); financialReportManager.put(tenantId,r);//维护已创建的享元对象 } return r; } IReportManager getEmployeeReportManager(String tenantId){ IReportManager r = employeeReportManager.get(tenantId);//通过租户ID获取享元 if(r == null) { r = new EmployeeReportManager(tenantId); employeeReportManager.put(tenantId,r);//维护已创建的享元对象 } return r; } }
使用享元模式的方法如下:
public static void main(String[] args) { ReportManagerFactory rmf = new ReportManagerFactory(); IReportManager rm = rmf.getFinancialReportManager("A"); System.out.println(rm.createReport()); }
ReportManagerFactory作为享元工厂,以租客的ID为索引,维护了一个享元对象的集合,它确保相同的租客的请求都返回同一个享元实例,确保享元对象的有效复用。
读---Java 程序性能优化 葛一鸣