结构型 - 7. 享元模式

115 阅读5分钟

享元模式(Flyweight Design Pattern),日常使用场景并不算多,一般通过工厂模式来实现。

1. 享元模式的原理

“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。

实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

“不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

2. 享元模式 VS 单例、缓存、对象池

2.1 享元模式 VS 单例

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。

区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数

  1. “单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的。
  2. 对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。
  3. 多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象,

2.2 享元模式 VS 缓存

在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思。

平时所讲的缓存,例如“数据库缓存”“CPU 缓存”“MemCache 缓存”,主要是为了提高访问效率,而非复用。

2.3 享元模式 VS 对象池

对象池、连接池(比如数据库连接池)、线程池等也是为了复用。池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。

享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间

3. 享元模式的应用

  • Java 的Integer
  • Redis 整型复用

享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。

元模式对编程语言的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被垃圾回收机制自动回收掉。因此,在某些情况下,如果对象的生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。所以,除非经过线上验证,利用享元模式真的可以大大节省内存,否则,就不要过度使用这个模式,为了一点点内存的节省而引入一个复杂的设计模式,得不偿失啊。

4. 享元模式的代码实现

它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。

type Color int8

// ChessPieceUnit 享元对象
type ChessPieceUnit struct {
   id    int
   text  string
   color Color
}

var (
   pieces map[int8]*ChessPieceUnit
   Black  Color = 1
   Red    Color = 2
)

func init() {
   pieces = make(map[int8]*ChessPieceUnit)
   pieces[1] = &ChessPieceUnit{1, "车", Black}
   pieces[2] = &ChessPieceUnit{2, "马", Red}
   pieces[3] = &ChessPieceUnit{3, "将", Black}
   pieces[4] = &ChessPieceUnit{4, "帅", Red}
   // ...省略摆放其他棋子的代码...
}

// ChessPieceUnitFactory 生成享元对象的工厂类
type ChessPieceUnitFactory struct {
}

func (c *ChessPieceUnitFactory) GetChessPieceUnit(chessPieceUnitID int8) *ChessPieceUnit {
   return pieces[chessPieceUnitID]
}

// ChessPiece 棋子
type ChessPiece struct {
   unit      *ChessPieceUnit
   positionX int16
   positionY int16
}

// ChessBoard 棋盘
type ChessBoard struct {
   chessPieces map[int16]ChessPiece
}

func (c *ChessBoard) Init() {
   factory := &ChessPieceUnitFactory{}
   chessPieces := make(map[int16]ChessPiece)

   chessPieces[1] = ChessPiece{factory.GetChessPieceUnit(1), 1, 1}
   chessPieces[2] = ChessPiece{factory.GetChessPieceUnit(2), 1, 2}
   chessPieces[3] = ChessPiece{factory.GetChessPieceUnit(3), 2, 1}
   chessPieces[4] = ChessPiece{factory.GetChessPieceUnit(4), 2, 2}
   // ...省略摆放其他棋子的代码...
   c.chessPieces = chessPieces
}

// 客户端使用
func TestChess(t *testing.T) {
   boards := make([]*ChessBoard, 100)
   for i := 0; i < 100; i++ {
      board := &ChessBoard{}
      board.Init()
      boards[i] = board
   }

   for i := 0; i < 100; i++ {
      t.Log(boards[i])
   }
}