設計模式 in Go: Flyweight
結構模式關注代碼、組件以及接口的有效組織與調用,主要解決如對象間的關係管理、提供必要抽象與具體實現相分離,以及將多個不同的庫或框架集成爲一個統一且連貫的系統等問題。
今天我們開始第 6 個結構模式的學習 —— Flyweight(享元模式)。
問題背景:
程序執行時會創建太多相似的對象副本,所以內存消耗會非常高。觀察這些這些對象定義發現,它們包含了若干的共同屬性(字段值相同),而其他字段則不同。這些對象確實應該不可避免要創建,但如何減少 RAM 消耗呢?能否從共同屬性入手,來嘗試優化呢。
解決方案:
Flyweight 模式用於需要在內存中創建和存儲大量對象的情況。例如,一個圖形編輯器需要處理成千上萬的形狀對象,如線條、圓形、矩形等。爲每種形狀創建完整的對象在內存使用方面會非常昂貴,特別是因爲這些對象共享許多通用狀態,如顏色、線條樣式、填充模式等。
如果沒有使用 Flyweight 模式,編輯器會因爲在一個對象中創建這些共享狀態的冗餘副本而消耗大量內存。這不僅效率低下,還會由於過度使用內存導致應用程序運行緩慢。
Flyweight 模式的核心是將共享的內在狀態與特定於對象的外在狀態分離:
-
內在狀態存儲在可以在所有形狀對象之間共享的 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