原理
复用对象,节省内存,前提是对象必须是不可变对象
所谓不可变对象就是指没有提供任何属性的set方法,对象创建完后就不能修改任何属性,目的是为了避免某个使用方修改了共享对象的属性对其他使用方造成影响
实现方式
将对象设计成享元(共享的单元),在内存中只保存一份,供多处引用,这样可以减少内存中对象的数据量达到节省内存的目的
不仅相同的对象可以设计成享元,也可以将对象中相同的部分抽取出来,设计成享元,让大量相似对象引用这些享元
最佳实践
需求1
开发一个象棋游戏,一个游戏厅中有上万个房间,每个房间有一个棋局。棋局要保存每个棋子的数据,比如:棋子类型、棋子颜色、棋子位置。
ChessPiece表示棋子,ChessBoard表示棋盘
未使用享元模式的实现
type ChessPiece struct {
id int
text string
color string
positionX int
positionY int
}
func NewChessPiece(id int, text string, color string, positionX int, positionY int) *ChessPiece {
return &ChessPiece{
id: id,
text: text,
color: color,
positionX: positionX,
positionY: positionY,
}
}
type ChessBoard struct {
chessPieceMap map[int]*ChessPiece
}
func NewChessBoard() *ChessBoard {
chessPieceMap := map[int]*ChessPiece{
1: NewChessPiece(1, "车", "black", 0, 0),
//...剩余其他棋子
}
return &ChessBoard{
chessPieceMap: chessPieceMap,
}
}
这种实现的缺陷:每个棋盘有30个棋子,如果有上万个房间那么就有三十万个棋子,即三十万个ChessPiece对象,很占用内存,我们可以将ChessPiece对象共享的属性(每个ChessPiece除了坐标不一样其他属性都一样)抽取出来作为享元,节省内存
采用享元模式的实现
type ChessPieceUnit struct {
id int
text string
color string
}
func NewChessPieceUnit(id int, text string, color string) *ChessPieceUnit {
return &ChessPieceUnit{
id: id,
text: text,
color: color,
}
}
type ChessPieceUnitFactory struct {
chessPieceUnitMap map[int]*ChessPieceUnit
}
func NewChessPieceUnitFactory() *ChessPieceUnitFactory {
chessPieceUnitMap := map[int]*ChessPieceUnit{
1: NewChessPieceUnit(1, "车", "black"),
//...剩余其他棋子
}
return &ChessPieceUnitFactory{
chessPieceUnitMap: chessPieceUnitMap,
}
}
func (f *ChessPieceUnitFactory) CreateInstance(id int) *ChessPieceUnit {
return f.chessPieceUnitMap[id]
}
type ChessPieceV2 struct {
chessPieceUnit *ChessPieceUnit
positionX int
positionY int
}
func NewChessPieceV2(chessPieceUnit *ChessPieceUnit, positionX int, positionY int) *ChessPieceV2 {
return &ChessPieceV2{
chessPieceUnit: chessPieceUnit,
positionX: positionX,
positionY: positionY,
}
}
type ChessBoardV2 struct {
chessPieceMap map[int]*ChessPieceV2
}
func NewChessBoardV2() *ChessBoardV2 {
factory := NewChessPieceUnitFactory()
chessPieceMap := map[int]*ChessPieceV2{
1: NewChessPieceV2(factory.CreateInstance(1), 0, 0),
//...剩余其他棋子
}
return &ChessBoardV2{
chessPieceMap: chessPieceMap,
}
}
我们发现使用了享元模式后,上万个房间也只有30个ChessPieceUnit对象,大大减少内存占用
需求2
实现一个简化的文本编辑器,该文本编辑器只包含文字编辑功能,编辑器只记录文字和格式信息(字体、大小、颜色等)
代码实现
type Word struct {
char rune
font int
size int
color string
}
func NewWord(char rune, font int, size int, color string) *Word {
return &Word{
char: char,
font: font,
size: size,
color: color,
}
}
type Editor struct {
words []*Word
}
func (e *Editor) AppendWords(words []*Word) {
e.words = append(e.words, words...)
}
这样每次新增一个word的时候都需要创建一个新的Word对象,而很多Word对象其实它们的格式是一样的,所以可以将字体格式这个
相似的部分抽取出来做成享元,节省内存
type WordStyle struct {
font int
size int
color string
}
type WordV2 struct {
wordStyle *WordStyle
char rune
}
type WordStyleFactory struct {
wordStyleCache []*WordStyle
mu sync.Mutex
}
//工厂由于要给多个线程使用,这里操作cache会有并发安全问题,所以加一个mutex锁
func (f *WordStyleFactory) CreateInstance(font int, size int, color string) *WordStyle {
newWordStyle := &WordStyle{
font: font,
size: size,
color: color,
}
f.mu.Lock()
defer f.mu.Unlock()
for i := range f.wordStyleCache {
wordStyle := f.wordStyleCache[i]
if reflect.DeepEqual(newWordStyle, wordStyle) {
return wordStyle
}
}
f.wordStyleCache = append(f.wordStyleCache, newWordStyle)
return newWordStyle
}
var wordStyleFactory *WordStyleFactory
var wordStyleFactoryOnce sync.Once
func GetWordStyleFactory() *WordStyleFactory {
wordStyleFactoryOnce.Do(func() {
wordStyleFactory = new(WordStyleFactory)
})
return wordStyleFactory
}
func NewWordV2(char rune, font int, size int, color string) *WordV2 {
return &WordV2{
wordStyle: GetWordStyleFactory().CreateInstance(font, size, color),
char: char,
}
}
现在多个word对象可以共享同一个wordstyle对象,节省内存
总结
享元模式和单例、缓存、池化技术的区别
享元模式和单例的区别:
享元模式一个类可以创建多个对象,单例一个类只能创建一个对象
和缓存的区别:
享元的目的是节省内存,缓存的目的是加速访问
和池化技术的区别:
享元对象被多处同时使用
池中的对象只被一处使用,使用完后放回需放回池中
享元模式的缺点:
享元模式对垃圾回收并不友好,因为享元工厂类一直持有享元对象的引用,就导致享元对象在没有任何代码使用的情况下也不会被垃圾回收,所以一些生命周期短的、使用并不频繁的对象使用享元模式可能适得其反,导致内存占用增加。因此在使用享元模式时一定要经过验证才行。