設計模式 in Go: Flyweight

結構模式關注代碼、組件以及接口的有效組織與調用,主要解決如對象間的關係管理、提供必要抽象與具體實現相分離,以及將多個不同的庫或框架集成爲一個統一且連貫的系統等問題。

今天我們開始第 6 個結構模式的學習 —— Flyweight(享元模式)。

問題背景:

程序執行時會創建太多相似的對象副本,所以內存消耗會非常高。觀察這些這些對象定義發現,它們包含了若干的共同屬性(字段值相同),而其他字段則不同。這些對象確實應該不可避免要創建,但如何減少 RAM 消耗呢?能否從共同屬性入手,來嘗試優化呢。

解決方案:

Flyweight 模式用於需要在內存中創建和存儲大量對象的情況。例如,一個圖形編輯器需要處理成千上萬的形狀對象,如線條、圓形、矩形等。爲每種形狀創建完整的對象在內存使用方面會非常昂貴,特別是因爲這些對象共享許多通用狀態,如顏色、線條樣式、填充模式等。

如果沒有使用 Flyweight 模式,編輯器會因爲在一個對象中創建這些共享狀態的冗餘副本而消耗大量內存。這不僅效率低下,還會由於過度使用內存導致應用程序運行緩慢。

Flyweight 模式的核心是將共享的內在狀態與特定於對象的外在狀態分離:

這通過重用 Flyweight 對象而不是複製其狀態,實現了顯著的內存節省,也有助於提高編輯器的性能。在內存受限的情況下,使用 Flyweight 模式可以高效處理大量對象。

下面示例藉助於圖形繪製來進一步解釋下 Flyweight 模式的原理,先理解渲染時的幾個層次:Shape->Point->Particle,Particle 是可供共享的最小單位。在這個示例中,我們通過繪製點來繪製一個圓,並且通過繪製粒子來繪製一個點。每個粒子包含一些不會改變的屬性,例如顏色。這個例子雖然簡單,但確實展示了享元模式的意義。

示例代碼:

/////////////////////////// flyweight.go ///////////////////////////
package flyweight

import (
 "fmt"
 "math"
)

// Shape draw this shape at pos
type Shape interface {
 Draw()
}

type Color string

const (
 Red   = "red"
 Blue  = "blue"
 Green = "green"
)

var pf = &ParticleFactory{
 particles: make(map[Color]*Particle),
}

// ParticleFactory creates particles according to `Color`
type ParticleFactory struct {
 particles map[Color]*Particle
}

// GetParticle returns reusable underlying particle instance with `Color`
// Here we doesn't consider safe when use it concurrently.
func (pf ParticleFactory) GetParticle(c Color) *Particle {
 if v, ok := pf.particles[c]; ok {
  return v
 }
 v := &Particle{c}
 pf.particles[c] = v
 return v
}

// Particle maintains the intrinsic state including color
type Particle struct {
 color Color
 // ...
 // ...
}

// Draw draw particle at `pos`, pos is the extrinsic state maintained
// by Point or Circle
func (c *Particle) Draw(pos Pos) {
 fmt.Printf("\t\\--> draw particle at <%.1f, %.1f> with color:%s\n",
  pos.X,
  pos.Y,
  c.color)
}

// Point a point which may be moved to <x,y> and rendered by different
// line weight
type Point struct {
 *Particle
 Pos        Pos
 LineWeight float64
}

// Draw draws the point at specific position using specific color and line
// weight.
//
// Note: I'm not a professional Computer Graphics engineers, I really don't
// know what's the differences btw a point or a particle, here I just try
// to treat a Point as a thing which may be rendered in many Particles.
func (p *Point) Draw() {
 fmt.Printf("\\--> draw point at <%.1f, %.1f> with color:%s with weight:%.1f\n",
  p.Pos.X,
  p.Pos.Y,
  p.color,
  p.LineWeight)
 for i := 0.0; i < p.LineWeight; i += 0.1 {
  for j := 0.0; j < p.LineWeight; j += 0.1 {
   p.Particle.Draw(Pos{p.Pos.X + i, p.Pos.Y + j})
  }
 }
}

func NewCircle(pos Pos, raidus float64, color Color, weight float64) *Circle {
 return &Circle{
  Color:      color,
  Center:     pos,
  Radius:     raidus,
  LineWeight: weight,
 }
}

// Circle a circle
type Circle struct {
 Color      Color
 Center     Pos
 Radius     float64
 LineWeight float64
}

// Draw circle will be rendered by different Points according to the formula
// (x-c.X)^2 + (y-c.Y)^2 = c.Raidus^2
func (c Circle) Draw() {
 fmt.Printf("draw circle at <%.1f, %.1f> with raidus %.1fcm, weight:%.1f\n",
  c.Center.X,
  c.Center.Y,
  c.Radius,
  c.LineWeight)

 // (x-pos.X)^2 + (y-pos.Y)^2 = c.Radius^2
 for x := c.Center.X - c.Radius; x < c.Center.X+c.Radius; x += 0.1 {
  y := math.Pow(math.Pow(c.Radius, 2)-math.Pow(x-c.Center.X, 2), 0.5) + c.Center.Y
  p := &Point{
   Particle:   pf.GetParticle(c.Color),
   LineWeight: c.LineWeight,
  }
  p.Pos = Pos{x, y}
  p.Draw()

  p.Pos = Pos{x, -y}
  p.Draw()
 }
}

type Pos struct {
 X, Y float64
}

Here’s the tests:

package flyweight_test

import (
 "flyweight"
 "testing"
)

func TestFlyweight(t *testing.T) {
 c := flyweight.NewCircle(flyweight.Pos{10, 10}, 5, flyweight.Red, 0.3)
 c.Draw()
}

ps:此處我們優先考慮可讀性,不會太關注編碼標準,如註釋、camelCase 類型名等。我們將多個文件的代碼組織到一個 codeblock 中僅僅是爲了方便閱讀,如果您想測試可以通過 git 下載源碼 github.com/hitzhangjie/go-patterns。

在需要創建大量相似對象的場景中(這些對象有些字段值相同可以共享),Flyweight 設計模式可以最小化內存使用並提高性能。

該模式優點:

該模式缺點:

總之,雖然享元模式在處理大量小型相似對象的大規模應用程序中提供了顯著的資源優化優勢,但需要仔細考慮和實現以避免增加系統維護的複雜性,並且要防止引入與併發管理相關的新的問題。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6Rqtpd4OsOy0nsyrPNZcMA