Python设计模式之享元模式(8)

享元模式(Flyweight Pattern):复用现有的同类对象,改善资源使用

主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。

OOP编程中容易出现对象创建带来的性能和内存占用问题,需要满足以下条件:

  • 需要使用大量对象(python里我们可以用__slots__节省内存占用)
  • 对象太多难以存储或解析大量对象。
  • 对象识别不是特别重要,共享对象中对象比较会失败。

经常使用对象池技术来实现共享对象,比如数据库中经常使用连接池来减少开销,预先建立一些连接池,每次取一个连接和数据库交互。

1 介绍

享元模式:运用共享技术有效地支持大量细粒度对象的复用。通过为相似对象引入数据共享来最小化内存使用,提升性能。

意图:运用共享技术有效地支持大量细粒度的对象。

原理:

  • 将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的
  • 需要的时候将对象从享元池中取出,即可实现对象的复用
  • 通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份

主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

何时使用: 

  • 系统中有大量对象。
  • 这些对象消耗大量内存。
  • 这些对象的状态大部分可以外部化。
  • 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
  • 系统不依赖于这些对象身份,这些对象是不可分辨的。

如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。

优点:大大减少对象的创建,降低系统的内存,使效率提高。

缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

注意事项: 

  • 注意划分外部状态和内部状态,否则可能会引起线程安全问题。
  • 这些类必须有一个工厂对象加以控制。
  • 享元模式,换句话说就是共享对象,在某些对象需要重复创建,且最终只需要得到单一结果的情况下使用。因为此种模式是利用先前创建的已有对象,通过某种规则去判断当前所需对象是否可以利用原有对象做相应修改后得到想要的效果,如以上教程的实例,创建了20个不同效果的圆,但相同颜色的圆只需要创建一次便可,相同颜色的只需要引用原有对象,改变其坐标值便可。此种模式下,同一颜色的圆虽然位置不同,但其地址都是同一个,所以说此模式适用于结果注重单一结果的情况。

    举一个简单例子,一个游戏中有不同的英雄角色,同一类型的角色也有不同属性的英雄,如刺客类型的英雄有很多个,按此种模式设计,利用英雄所属类型去引用原有同一类型的英雄实例,然后对其相应属性进行修改,便可得到最终想得到的最新英雄;比如说你创建了第一个刺客型英雄,然后需要设计第二个刺客型英雄,你利用第一个英雄改变属性得到第二个刺客英雄,最新的刺客英雄是诞生了,但第一个刺客英雄的属性也随之变得与第二个相同,这种情况显然是不可以的。

2 适用场景

享元模式可以避免大量非常相似类的开销,在程序设计中,有时会生成大量细粒度的类实例来表示数据,如果这些实例除了几个参数外基本相同,就可以把参数已到实例外面,在方法调用时,把它们传进来,就可以通过共享大幅度减少单个实例的数目。

一个享元(Flyweight)就是一个包含状态独立的不可变(又称固有的)数据的共享对象。依赖状态的可变(又称非固有的)数据不应是享元的一部分,因为每个对象的这种信息都不同,无法共享。如果享元需要非固有的数据,应该由客户端代码显式地提供。

用一个例子可能有助于解释实际应用场景中如何使用享元模式。假设我们正在设计一个性能关键的游戏,例如第一人称射击(First-Person Shooter,FPS)游戏。在FPS游戏中,玩家(士兵) 共享一些状态,如外在表现和行为。例如,在《反恐精英》游戏中,同一团队(反恐精英或恐怖分子)的所有士兵看起来都是一样的(外在表现)。同一个游戏中,(两个团队的)所有士兵都有一些共同的动作,比如,跳起、低头等(行为)。这意味着我们可以创建一个享元来包含所有共同的数据。当然,士兵也有许多因人而异的可变数据,这些数据不是享元的一部分,比如,枪支、健康状况和地理位置等。

主要适用以下场景:

  • 系统有大量相似对象。
  • 需要缓冲池的场景。

 

3 实现步骤

享元模式包含以下4个角色: Flyweight(抽象享元类) ConcreteFlyweight(具体享元类) UnsharedConcreteFlyweight(非共享具体享元类) FlyweightFactory(享元工厂类)

内部状态(Intrinsic State):存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享(例如:字符的内容)

外部状态(Extrinsic State):随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的(例如:字符的颜色和大小)

享元池(Flyweight Pool):存储共享实例对象的地方。

享元模式有多种实现方式:

3.1 使用元类实现享元池,控制实例的创建

具体步骤如下:

步骤一: 定义享元类,实现享元池,达到按需创建对象目的

步骤二:定义具体业务类

步骤三:客户端传递外部状态,获取结果

示例代码:

"""
步骤1:定义享元类,实现按需创建对象
"""

class FlyweightMeta(type):
    """
    当pool中存在类名和参数完全一致时,返回pool中缓存的instance;不存在时,创建新的instance
    """
    def __new__(mcls, name, parents, dct):
     """
     :param mcls: 被继承的元类,即type类
     :param name: 衍生类类名
     :param parents: 衍生类的父类属性
     :param dct: 新属性
     :return:
     """
     dct['pool'] = dict()
     return super(FlyweightMeta, mcls).__new__(mcls, name, parents, dct) #复用type.__new__方法

    def _serialize_params(cls, *args, **kwargs):
        args_list = list(map(str, args))
        args_list.extend([str(kwargs), cls.__name__])
        key = ''.join(args_list)
        return key

    def __call__(self, *args, **kwargs):
        key = self._serialize_params(*args, **kwargs)
        pool = getattr(self, 'pool', {})
        instance = pool.get(key, None)
        if not instance:
            instance = super(FlyweightMeta, self).__call__(*args, **kwargs)
            pool[key] = instance
        return instance

"""
步骤2:定义具体业务类,需要使用元类
"""
class Book(metaclass=FlyweightMeta):
    def __init__(self, name, price):
        # 可变参数name和price,由客户端提供
        self.name = name
        self.price = price

    def get_book(self):
        return "{} buy a book: {}, cost: {}".format(id(self), self.name, self.price)

if __name__ == "__main__":
    book_1 = Book("Python基础", 55)
    book_2 = Book("Python基础", 55)
    book_3 = Book("Python核心基础", 100)
    print(book_1.get_book())
    print(book_2.get_book())
    print(book_3.get_book())

执行结果:

4372629936 buy a book: Python基础, cost: 55
4372629936 buy a book: Python基础, cost: 55
4372629880 buy a book: Python核心基础, cost: 100

3.2 在业务基类或工厂类中实现享元模式,共享公用对象

案例一:
当客户端要创建Tree的一个实例时,会以tree_type参数传递树的种类。
树的种类用于检查是否创建过相同种类的树。如果是,则返回之前创建的对象;否则,将这个新的树种添加到池中,并返回相应的新对象
方法render()用于在屏幕上渲染一棵树。注意,享元不知道的所有可变(外部的)信息都需要由客户端代码显式地传递。
在当前案例中,每棵树都用到一个随机的年龄和一个address。为了让render()更加有用,有必要确保没有树会被渲染到另一个棵之上

代码示例:

import random
from enum import Enum

TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')

"""
步骤1:工厂类实现享元对象池
"""
class TreeFactory(object):
    pool = dict()
    def __new__(cls, tree_type, *args, **kwargs):
        obj = cls.pool.get(tree_type, None)
        if obj is None:
            obj = super(TreeFactory, cls).__new__(cls)
            cls.pool[tree_type] = obj
            obj.tree_type = tree_type
        return obj

"""
步骤2:业务类
"""
class Tree(TreeFactory):
    def __init__(self, tree_type):
        self.tree_type = tree_type #共享属性

    def render(self, age, address): # age, address为非共享属性
        print("{} render  a tree of type {} and age {} at {}".format(id(self), self.tree_type, age, address))

def main():
    rnd = random.Random()
    age_min, age_max = 1, 30
    app_tree = Tree(TreeType.apple_tree)
    cherry_tree = Tree(TreeType.cherry_tree)
    app_tree.render(rnd.randint(age_min, age_max), "南京")
    app_tree.render(rnd.randint(age_min, age_max), "上海")
    cherry_tree.render(rnd.randint(age_min, age_max), "南京")
    cherry_tree.render(rnd.randint(age_min, age_max), "上海")

if __name__ == "__main__":
    main()

执行结果:

4365321272 render  a tree of type TreeType.apple_tree and age 5 at 南京
4365321272 render  a tree of type TreeType.apple_tree and age 13 at 上海
4369574768 render  a tree of type TreeType.cherry_tree and age 13 at 南京
4369574768 render  a tree of type TreeType.cherry_tree and age 29 at 上海

 

4 代码实践

享元模式有多种实现方式。

方式一:如上面的例子所示,采用元类实现享元

方式二:在业务基类中实现享元模式,共享公用对象。在子类中实现非共享属性。

代码案例如下:

案例1:咖啡店根据客户喜好,提供2中不同种类咖啡,且不同的咖啡可以支持多种容量size; 不同的size有不同的价格

"""
步骤一:定义产品类
"""
class Coffee(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def serve_coffee(self):
        print("{} coffee 做好了, 价格: {}".format(self.name, self.price))

"""
步骤二:定义产品工厂类,实现享元池
"""
class CoffeeFactory(object):
    """
    name为共享对象标识,含有相同name参数的实例会被缓存;
    name相同,但是price不同的参数,也会返回相同的实例,即price以缓存为准
    """
    coffee_dict = dict()
    def get_coffee(self, name, price):
        if not self.coffee_dict.__contains__(name):
            self.coffee_dict[name] = Coffee(name, price)
        return self.coffee_dict[name]

    def get_coffee_count(self):
        return len(self.coffee_dict)

"""
步骤三:客户端,传递非共享参数,获取coffee对象
"""
class Customer(object):
    def __init__(self, customer_name):
        self.customer_name = customer_name
        self.coffee = CoffeeFactory()

    def order(self, name, price):
        instance = self.coffee.get_coffee(name, price)
        instance.price = price # price为不可共享参数,customer强制修改属性值
        instance.serve_coffee()
        print('给客户:{} 的coffee: {}好了, 首款:{}'.format(self.customer_name, name, price))
        print("coffee的个数{}".format(self.coffee.get_coffee_count()))

if __name__ == "__main__":
    coffee_fact = CoffeeFactory()
    cust_a = Customer("Customer A")
    cust_b = Customer("Customer B")
    cust_c = Customer("Customer C")
    # coffee_1 和 coffee_2 共享相同的实例对象
    coffee_1 = cust_a.order("Moca", 25)
    coffee_2 = cust_b.order("Moca", 30)
    coffee_3 = cust_c.order("cappuccino", 35)

执行结果:

Moca coffee 做好了, 价格: 25
给客户:Customer A 的coffee: Moca好了, 首款:25
coffee的个数1
Moca coffee 做好了, 价格: 30
给客户:Customer B 的coffee: Moca好了, 首款:30
coffee的个数1
cappuccino coffee 做好了, 价格: 35
给客户:Customer C 的coffee: cappuccino好了, 首款:35
coffee的个数2

 

5 memoization与享元模式

memoization是一种优化技术,使用一个缓存来避免重复计算那些在更早的执行步骤中已经计算好的结果。memoization并不是只能应用于某种特定的编程方式,比如面向对象编程(Object-Oriented Programming,OOP)。在Python中,memoization可以应用于方法和简单的函数。享元则是一种特定于面向对象编程优化的设计模式,关注的是共享对象数据。

 

6 软件例子

Exaile音乐播放器(请参考网页t.cn/RqrjYHQ)使用享元来复用通过相同URL识别的对象 (在这里是指音乐歌曲)。创建一个与已有对象的URL相同的新对象是没有意义的,所以复用相同的对象来节约资源(请参考网页t.cn/RqrjQWr)。

Peppy是一个用Python语言实现的类XEmacs编辑器(请参考网页[t.cn/hbhSda]),它使用享元模式存储major mode状态栏的状态。这是因为除非用户修改,否则所有状态栏共享相同的属性(请参考网页[t.cn/Rqrjm9y])。这个软件原作者2014年就放弃了。

 

参考文献:

https://www.runoob.com/design-pattern/flyweight-pattern.html

https://www.cnblogs.com/welan/p/9128598.html

https://www.jianshu.com/p/2badd38475ea

https://www.cnblogs.com/onepiece-andy/p/python-flyweight-pattern.html

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值