今天Stone City的市長帶著Stone Mall的店長來找我們,他們希望也可以做一個像WoodMall一樣的系統,可以很快地計算出每個Shop帶給Mall的營收狀況。
目前Stone Mall與Wood Mall是合作關係,所以可以直接拿到Wood Mall的Source Code來參考:
我們看到每一個店家都會有一個專有的類別,可以用來存放各自的Revenue狀況。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class WoodComputerShop { let revenue:Float init(revenue:Float) { self.revenue = revenue } } class WoodBicycleShop { let revenue:Float init(revenue:Float) { self.revenue = revenue } } class WoodFurnitureShop { let revenue:Float init(revenue:Float) { self.revenue = revenue } } |
而WoodMall本身也會有一個類別,可以用來加入店家以及計算各店家帶給自己的收入。
其中WoodMall會向各店家抽取利潤的比例為:
- WoodComputerShop 5%
- WoodBicycleShop 15%
- WoodFurnitureShop 25%
WoodMall通過shops來管理店家,calculateRevenue方法來計算從各店家抽成的費用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class WoodMall { let shops:[Any] init(shops:Any...) { self.shops = shops } func calculateRevenue() -> Float { return shops.reduce(0, { total, shop in if let computerShop = shop as? WoodComputerShop { return total + computerShop.revenue * 0.05 } else if let bicycleShop = shop as? WoodBicycleShop { return total + bicycleShop.revenue * 0.15 } else if let furnitureShop = shop as? WoodFurnitureShop { return total + furnitureShop.revenue * 0.25 } else { return total } }) } } |
我們發現這樣的系統雖然目前來看是可以使用的,但如果有shop推出或者加入WoodMall時,這時候calculateRevenue方法又要做不少修改,
而且這一堆條件語句也會有越來越長的可能。
那麼有什麼更好的辦法呢?
訪問者模式(Visitor Pattern)
- 使用訪問者模式可以在不修改類的source code和不創建新的子類情況下進行擴展類的行為。
- 和「策略模式」不同的是,訪問者模式的使用對象是「各類對象的集合」。
訪問者模式通過「將操作對象的算法分離到一個單獨的對象」,並在該對象中定義能夠處理集合中各類對象的方法來解決上面我們提到的問題。
首先我們在Visitor.swift中定義兩個協議
1 2 3 4 5 6 7 8 9 10 |
protocol StoneShop { func accept(visitor:Visitor) } protocol Visitor { func visit(shop:StoneComputerShop) func visit(shop:StoneBicycleShop) func visit(shop:StoneFurnitureShop) } |
StoneShop協議定義了accept方法,Visitor協議定義了幾個visit方法,每個方法對應一種商店。這幾個方法是實現「雙分派」的關鍵。
StoneShop協議
我們讓所有的店家都遵循StoneShop協議,他們都會根據協議實現accept方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class StoneComputerShop: StoneShop { let revenue:Float init(revenue:Float) { self.revenue = revenue } func accept(visitor: Visitor) { visitor.visit(shop: self) } } class StoneBicycleShop: StoneShop { let revenue:Float init(revenue:Float) { self.revenue = revenue } func accept(visitor: Visitor) { visitor.visit(shop: self) } } class StoneFurnitureShop: StoneShop { let revenue:Float init(revenue:Float) { self.revenue = revenue } func accept(visitor: Visitor) { visitor.visit(shop: self) } } |
StoneMall新增一個accept方法
accept方法用來接收一個visitor對象,並調用對象集合中的每個對象的accept方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class StoneMall { let shops:[StoneShop] init(shops:StoneShop...) { self.shops = shops } // 上下文類,通過accept方法,將訪問者傳遞給對象 func accept(visitor: Visitor) { for shop in shops { shop.accept(visitor: visitor) } } } |
創建訪問者類
訪問者類負責處理shop對象集合。
使用了「雙分派」之後,就可以直接訪問某個類型對象的屬性了,不用在做類型的判斷。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ShopVisitor: Visitor { var totalRevenue:Float = 0 func visit(shop: StoneComputerShop) { totalRevenue += shop.revenue * 0.05 } func visit(shop: StoneBicycleShop) { totalRevenue += shop.revenue * 0.15 } func visit(shop: StoneFurnitureShop) { totalRevenue += shop.revenue * 0.25 } } |
最後組件調用的方式是這樣的:
1 2 3 4 5 6 7 8 |
let stoneComputerShop = StoneComputerShop(revenue: 8000) let stoneBicycleShop = StoneBicycleShop(revenue: 7500) let stoneFurnitureShop = StoneFurnitureShop(revenue: 15000) let stoneMall = StoneMall(shops: stoneComputerShop, stoneBicycleShop, stoneFurnitureShop) let shopVisitor = ShopVisitor() stoneMall.accept(visitor: shopVisitor) print("Stone Mall 一共盈利 \(shopVisitor.totalRevenue)") |
輸出的結果:
1 |
Stone Mall 一共盈利 5275.0 |
如果有需要,可以到github上看上面提到的「訪問者模式例子」