- 原文地址:Intermediate Design Patterns in Swift
- 原文做者:raywenderlich.com
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:iWeslie
- 校對者:swants, kirinzer
設計模式對於代碼的維護和提升可讀性很是有用,經過本教程你將學習 Swift 中的一些設計模式。前端
更新說明:本教程已由譯者針對 iOS 12,Xcode 10 和 Swift 4.2 進行了更新。android
新手教程:沒了解過設計模式?來看看設計模式的 入門教程 來閱讀以前的基礎知識吧。ios
在本教程中,你將學習如何使用 Swift 中的設計模式來重構一個名爲 Tap the Larger Shape 的遊戲。git
瞭解設計模式對於編寫可維護且無 bug 的應用程序相當重要,瞭解什麼時候採用何種設計模式是一項只能經過實踐學習的技能。這本教程再好不過了!github
但究竟什麼是設計模式呢?這是一個針對常見問題的正式文檔型解決方案。例如,考慮一下遍歷一個集合,你在此處使用 迭代器 設計模式:算法
var collection = ...
// for 循環使用迭代器設計模式
for item in collection {
print("Item is: \(item)")
}
複製代碼
迭代器 設計模式的價值在於它抽象出了訪問集合中每一項的實際底層機制。不管 collection
是數組,字典仍是其餘類型,你的代碼均可以用相同的方式訪問它們中的每一項。編程
不只如此,設計模式也是開發者文化的一部分,所以維護或擴展代碼的另外一個開發人員可能會理解迭代器設計模式,它們是用於推理出軟件架構的語言。swift
在 iOS 編程中有不少設計模式頻繁出現,例如 MVC 出如今幾乎每一個應用程序中,代理 是一個強大的,一般未被充分利用的模式,好比說你曾用過的 tableView,本教程討論了一些不爲人知但很是有用的設計模式。後端
若是你不熟悉設計模式的概念,此篇文章可能不適合如今的你,不妨先看一下 使用 Swift 的 iOS 設計模式 來開始吧。設計模式
Tap the Larger Shape 是一個有趣但簡單的遊戲,你會看到一對類似的形狀,你須要點擊二者中較大的一個。若是你點擊較大的形狀,你會獲得一分,反之你會失去一分。
看起來你好像只噴出了一些隨機的方塊、圓圈和三角形塗鴉,不過孩子們會買單的!:]
下載 入門項目 並在 Xcode 中打開。
注意:你須要使用 Xcode 10 和 Swift 4.2 及以上版本從而得到最大的兼容性和穩定性。
此入門項目包含完整遊戲,你將在本教程中對改項目進行重構並利用一些設計模式來使你的遊戲更易於維護而且更加有趣。
使用 iPhone 8 模擬器,編譯並運行項目,隨意點擊幾個圖形來了解這個遊戲的規則。你會看到以下圖所示的內容:
點擊較大的圖形就能得分。
點擊較小的圖形則會扣分。
在深刻了解設計模式的細節以前,先看一下目前編寫的遊戲。打開 Shape.swift 看一看並找到如下代碼,你無需進行任何更改,只須要看看就行:
import UIKit
class Shape {
}
class SquareShape: Shape {
var sideLength: CGFloat!
}
複製代碼
Shape
類是遊戲中可點擊圖形的基本模型。具體的一個子類 SquareShape
表示一個正方形:一個具備四條等長邊的多邊形。
接下來打開 ShapeView.swift 並查看 ShapeView
的代碼:
import UIKit
class ShapeView: UIView {
var shape: Shape!
// 1
var showFill: Bool = true {
didSet {
setNeedsDisplay()
}
}
var fillColor: UIColor = UIColor.orange {
didSet {
setNeedsDisplay()
}
}
// 2
var showOutline: Bool = true {
didSet {
setNeedsDisplay()
}
}
var outlineColor: UIColor = UIColor.gray {
didSet {
setNeedsDisplay()
}
}
// 3
var tapHandler: ((ShapeView) -> ())?
override init(frame: CGRect) {
super.init(frame: frame)
// 4
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapRecognizer)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleTap() {
// 5
tapHandler?(self)
}
let halfLineWidth: CGFloat = 3.0
}
複製代碼
ShapeView
是呈現通用 Shape
模型的 view。如下是其中代碼的逐行解析:
指明應用程序是否使用,並使用哪一種顏色來填充圖形,這是圖形內部的顏色。
指明應用程序是否使用,並使用哪一種顏色來給圖形描邊,這是圖形邊框的顏色。
一個處理點擊事件的閉包(例如更新得分)。若是你不熟悉 Swift 閉包,能夠在 Swift 閉包 中查看它們,但請記住它們與 Objective-C 裏的 block 相似。
設置一個 tap gesture recognizer,當玩家點擊 view 時調用 handleTap
。
當檢測到點擊手勢時調用 tapHandler
。
如今向下滾動而且查看 SquareShapeView
:
class SquareShapeView: ShapeView {
override func draw(_ rect: CGRect) {
super.draw(rect)
// 1
if showFill {
fillColor.setFill()
let fillPath = UIBezierPath(rect: bounds)
fillPath.fill()
}
// 2
if showOutline {
outlineColor.setStroke()
// 3
let outlinePath = UIBezierPath(rect: CGRect(x: halfLineWidth, y: halfLineWidth, width: bounds.size.width - 2 * halfLineWidth, height: bounds.size.height - 2 * halfLineWidth))
outlinePath.lineWidth = 2.0 * halfLineWidth
outlinePath.stroke()
}
}
}
複製代碼
如下是 SquareShapeView
如何進行繪製的:
若是配置爲顯示填充,則使用填充顏色填充 view。
若是配置爲顯示輪廓,則使用輪廓顏色給 view 描邊。
因爲 iOS 是以 position 爲中心繪製線條的,所以咱們在描邊路徑時須要將從 view 的 bounds 裏減去 halfLineWidth
。
很棒!如今你已經瞭解了這個遊戲裏的圖形是如繪製的,打開 GameViewController.swift 並查看其中的邏輯:
import UIKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1
beginNextTurn()
}
override var prefersStatusBarHidden: Bool {
return true
}
private func beginNextTurn() {
// 2
let shape1 = SquareShape()
shape1.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)
let shape2 = SquareShape()
shape2.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)
// 3
let availSize = gameView.sizeAvailableForShapes()
// 4
let shapeView1: ShapeView = SquareShapeView(frame: CGRect(x: 0, y: 0, width: availSize.width * shape1.sideLength, height: availSize.height * shape1.sideLength))
shapeView1.shape = shape1
let shapeView2: ShapeView = SquareShapeView(frame: CGRect(x: 0, y: 0, width: availSize.width * shape2.sideLength, height: availSize.height * shape2.sideLength))
shapeView2.shape = shape2
// 5
let shapeViews = (shapeView1, shapeView2)
// 6
shapeViews.0.tapHandler = { tappedView in
self.gameView.score += shape1.sideLength >= shape2.sideLength ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = { tappedView in
self.gameView.score += shape2.sideLength >= shape1.sideLength ? 1 : -1
self.beginNextTurn()
}
// 7
gameView.addShapeViews(newShapeViews: shapeViews)
}
private var gameView: GameView { return view as! GameView }
}
複製代碼
如下是遊戲邏輯的工做原理:
當 GameView
加載後開始新的一局。
在 [0.3, 0.8]
區間內取邊長繪製正方形,繪製的圖形也能夠在任何屏幕尺寸下縮放。
由 GameView
肯定哪一種尺寸的圖形適合當前屏幕。
爲每一個形狀建立一個 SquareShapeView
,並經過將圖形的 sideLength
比例乘以當前屏幕的相應 availSize
來調整形狀的大小。
將形狀存儲在元組中以便於操做。
在每一個 shape view 上設置點擊事件並根據玩家是否點擊較大的 view 來計算分數。
將形狀添加到 GameView
以便佈局顯示。
以上就是遊戲的完整邏輯。是否是很簡單?:]
你可能想問本身:「嗯,因此當我有一個工做遊戲時,爲何我須要設計模式呢?」那麼若是你想支持除了正方形之外的形狀又要怎麼辦呢?
你 本能夠 在 beginNextTurn
中添加代碼來建立第二個形狀,可是當你添加第三種、第四種甚至第五種形狀時,代碼將變得難以管理。
若是你但願玩家可以選擇別人的形狀又要怎麼辦呢?
若是你把全部代碼放在 GameViewController
中,你最終會獲得難以管理的包含硬編碼依賴的耦合度很高的代碼。
如下是你的問題的答案:設計模式有助於將你的代碼解耦成分離地很開的單位。
在進行下一步以前,我坦白,我已經偷偷地進入了一個設計模式。
如今,關於設計模式,如下的每一個部分都描述了不一樣的設計模式。咱們開始吧!
GameViewController
與 SquareShapeView
緊密耦合,這將不能爲之後使用不一樣的視圖來表示正方形或引入第二個形狀留出餘地。
你的第一個任務是使用 抽象工廠 設計模式給你的GameViewController
進行簡化和解耦。你將要在代碼中使用此模式,該代碼創建用於構造一組相關對象的API,例如你將暫時使用的 shape view,而無需對特定類進行硬編碼。
新建一個 Swift 文件,命名爲 ShapeViewFactory.swift 並保存,而後添加如下代碼:
import UIKit
// 1
protocol ShapeViewFactory {
// 2
var size: CGSize { get set }
// 3
func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView)
}
複製代碼
如下是你新的工廠的工做原理:
將 ShapeViewFactory
定義爲 Swift 協議,它沒有理由成爲一個類或結構體,由於它只描述了一個接口而自己並無功能。
每一個工廠應當有一個定義了建立形狀的邊界的尺寸,這對使用工廠生成的 view 佈局代碼相當重要。
定義生成形狀視圖的方法。這是工廠的「肉」,它須要兩個 Shape 對象的元組,並返回兩個 ShapeView 對象的元組。這基本上是從其原材料 — 模型中製造 view。
在 ShapeViewFactory.swift 的最後添加如下代碼:
class SquareShapeViewFactory: ShapeViewFactory {
var size: CGSize
// 1
init(size: CGSize) {
self.size = size
}
func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {
// 2
let squareShape1 = shapes.0 as! SquareShape
let shapeView1 = SquareShapeView(frame: CGRect(
x: 0,
y: 0,
width: squareShape1.sideLength * size.width,
height: squareShape1.sideLength * size.height))
shapeView1.shape = squareShape1
// 3
let squareShape2 = shapes.1 as! SquareShape
let shapeView2 = SquareShapeView(frame: CGRect(
x: 0,
y: 0,
width: squareShape2.sideLength * size.width,
height: squareShape2.sideLength * size.height))
shapeView2.shape = squareShape2
// 4
return (shapeView1, shapeView2)
}
}
複製代碼
你的 SquareShapeViewFactory
建造了 SquareShapeView
實例,以下所示:
使用一致的最大尺寸來初始化工廠。
從第一個傳遞的形狀構造第一個 shape view。
從第二個傳遞的形狀構造第二個 shape view。
返回包含兩個剛建立的 shape view 的元組。
最後,是時候使用 SquareShapeViewFactory
了。打開 GameViewController.swift,並所有替換爲如下內容:
import UIKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1 ***** 附加
shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
beginNextTurn()
}
override var prefersStatusBarHidden: Bool {
return true
}
private func beginNextTurn() {
let shape1 = SquareShape()
shape1.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)
let shape2 = SquareShape()
shape2.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)
// 2 ***** 附加
let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: (shape1, shape2))
shapeViews.0.tapHandler = { tappedView in
self.gameView.score += shape1.sideLength >= shape2.sideLength ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = { tappedView in
self.gameView.score += shape2.sideLength >= shape1.sideLength ? 1 : -1
self.beginNextTurn()
}
gameView.addShapeViews(newShapeViews: shapeViews)
}
private var gameView: GameView { return view as! GameView }
// 3 ***** 附加
private var shapeViewFactory: ShapeViewFactory!
}
複製代碼
這裏有三行新代碼:
初始化並存儲一個 SquareShapeViewFactory
。
使用此新工廠建立你的 shape view。
將新的 shape view 工廠存儲爲實例屬性。
主要的好處在於第二部分,其中你用一行替換了六行代碼。更好的是,你將複雜的 shape view 的建立代碼移出了 GameViewController
從而使類更小也更容易理解。
將 view 建立代碼移出 controller 是頗有幫助的,由於 GameViewController
充當 Controller 在 Model 和 View 之間進行協調。
編譯並運行,而後你應該看到相似如下內容:
你遊戲的視覺效果沒有任何改變,但你確實簡化了代碼。
若是你用 SomeOtherShapeView
替換 SquareShapeView
,那麼 SquareShapeViewFactory
的好處就會大放異彩。具體來講,你不須要更改 GameViewController
,你能夠將全部更改分離到 SquareShapeViewFactory
。
既然你已經簡化了 shape view 的建立,那麼你也同時能夠簡化 shape 的建立。像以前那樣建立一個新的 Swift 文件,命名爲 ShapeFactory.swift,並把如下代碼粘貼進去:
import UIKit
// 1
protocol ShapeFactory {
func createShapes() -> (Shape, Shape)
}
class SquareShapeFactory: ShapeFactory {
// 2
var minProportion: CGFloat
var maxProportion: CGFloat
init(minProportion: CGFloat, maxProportion: CGFloat) {
self.minProportion = minProportion
self.maxProportion = maxProportion
}
func createShapes() -> (Shape, Shape) {
// 3
let shape1 = SquareShape()
shape1.sideLength = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)
// 4
let shape2 = SquareShape()
shape2.sideLength = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)
// 5
return (shape1, shape2)
}
}
複製代碼
你的新 ShapeFactory
生產 shape 的具體步驟以下:
再一次地,就像你對 ShapeViewFactory
所作的那樣,將 ShapeFactory
聲明爲一個協議來得到最大的靈活性。
你但願你的 shape 工廠生成具備單位尺寸的形狀,例如,在 [0, 1]
的範圍內,所以你要存儲這個範圍。
建立具備隨機尺寸的第一個方形。
建立具備隨機尺寸的第二個方形。
將這對方形形狀做爲元組返回。
如今打開 GameViewController.swift 並在底部大括號結束以前的插入如下代碼:
private var shapeFactory: ShapeFactory!
複製代碼
而後在 viewDidLoad
的底部 beginNextTurn
的調用之上插入如下代碼:
shapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
複製代碼
最後把 beginNextTurn
替換爲如下代碼:
private func beginNextTurn() {
// 1
let shapes = shapeFactory.createShapes()
let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)
shapeViews.0.tapHandler = { tappedView in
// 2
let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape
// 3
self.gameView.score += square1.sideLength >= square2.sideLength ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = { tappedView in
let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape
self.gameView.score += square2.sideLength >= square1.sideLength ? 1 : -1
self.beginNextTurn()
}
gameView.addShapeViews(newShapeViews: shapeViews)
}
複製代碼
如下是上面代碼的解析:
使用新的 shape 工廠建立一個形狀元組。
從元組中提取形狀。
這樣你就能夠在這裏比較它們了。
再一次使用 抽象工廠 設計模式,經過將建立形狀的部分移出 GameViewController
來簡化代碼。
如今你甚至能夠添加第二個形狀,例如圓圈。你對正方形的惟一硬性依賴是下面 beginNextTurn
中的得分計算:
shapeViews.1.tapHandler = { tappedView in
// 1
let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape
// 2
self.gameView.score += square2.sideLength >= square1.sideLength ? 1 : -1
self.beginNextTurn()
}
複製代碼
在這裏你把形狀轉換爲 SquareShape
以便你能夠訪問它們的 sideLength
,圓沒有 sideLength
,而是「直徑」。
解決方案是使用 僱工 設計模式,它經過一個通用接口爲一組類(如形狀類)提供分數計算等方法。在你如今的狀況下,分數計算是僱工,形狀類做爲服務對象,而且 area
屬性扮演公共接口的角色。
打開 Shape.swift 並在 Shape
類的底部添加如下代碼:
var area: CGFloat { return 0 }
複製代碼
而後在 SquareShape
類的底部添加如下代碼:
override var area: CGFloat { return sideLength * sideLength }
複製代碼
如今你能夠根據其面積來判斷哪一個形狀更大。
打開 GameViewController.swift 並把 beginNextTurn
替換成如下內容:
private func beginNextTurn() {
let shapes = shapeFactory.createShapes()
let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)
shapeViews.0.tapHandler = { tappedView in
// 1
self.gameView.score += shapes.0.area >= shapes.1.area ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = { tappedView in
// 2
self.gameView.score += shapes.1.area >= shapes.0.area ? 1 : -1
self.beginNextTurn()
}
gameView.addShapeViews(newShapeViews: shapeViews)
}
複製代碼
根據形狀區域肯定較大的形狀。
仍是根據形狀區域肯定較大的形狀。
編譯並運行,你應該看到相似下面的內容,雖然遊戲看起來相同,但代碼如今更靈活了。
恭喜,你已經從遊戲邏輯中徹底解除了對正方形的依賴關係,若是你要建立和使用一些圓形的工廠,你的遊戲將變得更加完善。
「不要作一個古板的人!」在現實生活中多是一種侮辱,你的遊戲感受它被裝在一個形狀中,它渴望更流暢的線條和更多的符合空氣動力學的形狀。
你須要引入一些流暢的「善良的圓」,如今打開 Shape.swift 並在文件底部添加如下代碼:
class CircleShape: Shape {
var diameter: CGFloat!
override var area: CGFloat { return CGFloat.pi * diameter * diameter / 4.0 }
}
複製代碼
你的圓只須要知道它能夠計算自身面積的「直徑」就能夠支持 僱工 模式。
接下來經過添加 CircleShapeFactory
來構建 CircleShape
對象。打開 ShapeFactory.swift 並在文件底部添加如下代碼:
class CircleShapeFactory: ShapeFactory {
var minProportion: CGFloat
var maxProportion: CGFloat
init(minProportion: CGFloat, maxProportion: CGFloat) {
self.minProportion = minProportion
self.maxProportion = maxProportion
}
func createShapes() -> (Shape, Shape) {
// 1
let shape1 = CircleShape()
shape1.diameter = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)
// 2
let shape2 = CircleShape()
shape2.diameter = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)
return (shape1, shape2)
}
}
複製代碼
這段代碼遵循一個熟悉的模式:第1部分 和 第2部分 建立了一個 CircleShape
併爲其指定一個隨機的 diameter
。
你須要解決另外一個問題,這樣作可能會防止一個混亂的幾何圖形的革命。看吧,你如今擁有的是 「沒有表明性的幾何圖形」,你知道當形狀不足時,形狀會變得多麼乾淨哈!
取悅你的玩家很容易,你須要的只是用 CircleShapeView
在屏幕上 繪製 你的新 CircleShape
對象。:]
打開 ShapeView.swift
並在文件底部添加如下內容:
class CircleShapeView: ShapeView {
override init(frame: CGRect) {
super.init(frame: frame)
// 1
self.isOpaque = false
// 2
self.contentMode = UIView.ContentMode.redraw
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
if showFill {
fillColor.setFill()
// 3
let fillPath = UIBezierPath(ovalIn: self.bounds)
fillPath.fill()
}
if showOutline {
outlineColor.setStroke()
// 4
let outlinePath = UIBezierPath(ovalIn: CGRect(
x: halfLineWidth,
y: halfLineWidth,
width: self.bounds.size.width - 2 * halfLineWidth,
height: self.bounds.size.height - 2 * halfLineWidth))
outlinePath.lineWidth = 2.0 * halfLineWidth
outlinePath.stroke()
}
}
}
複製代碼
對上述內容的解釋依次爲如下幾個部分:
因爲圓沒法填充其 view 的 bounds,所以你須要告訴 UIKit 該 view 是透明的,這意味着能透過它看到背後的東西。若是你沒有意識到這點,那麼這個圓將會有一個醜陋的黑色背景。
因爲視圖是透明的,所以應在 bounds 更改時進行重繪。
畫一個用 fillColor
填充的圓圈。稍後,你將建立 CircleShapeViewFactory
,它會確保 CircleView
具備相等的寬度和高度,所以畫出來的形狀將是圓形而不是橢圓形。
給圓用 lineWidth 進行描邊。
如今你將在 CircleShapeViewFactory
中建立CircleShapeView
對象。
打開 ShapeViewFactory.swift 並在文件的底部添加如下代碼:
class CircleShapeViewFactory: ShapeViewFactory {
var size: CGSize
init(size: CGSize) {
self.size = size
}
func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {
let circleShape1 = shapes.0 as! CircleShape
// 1
let shapeView1 = CircleShapeView(frame: CGRect(
x: 0,
y: 0,
width: circleShape1.diameter * size.width,
height: circleShape1.diameter * size.height))
shapeView1.shape = circleShape1
let circleShape2 = shapes.1 as! CircleShape
// 2
let shapeView2 = CircleShapeView(frame: CGRect(
x: 0,
y: 0,
width: circleShape2.diameter * size.width,
height: circleShape2.diameter * size.height))
shapeView2.shape = circleShape2
return (shapeView1, shapeView2)
}
}
複製代碼
這是將建立圓而不是正方形的工廠。第1部分 和 第2部分 使用傳入的形狀建立 CircleShapeView
實例。請注意你的代碼是如何確保圓圈具備相同的寬度和高度,所以它們呈現爲完美的圓形而不是橢圓形。
最後,打開 GameViewController.swift 並替換 viewDidLoad
中對應的兩行,用如下內容分配形狀和視圖工廠:
shapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())
shapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)
複製代碼
如今編譯並運行項目,你應該看到相似下面的截圖。
請注意你是如何在 GameViewController
中添加新形狀而不會對遊戲邏輯產生太大影響的,抽象工廠和僱工設計模式使之成爲可能。
如今是時候來看看第三種設計模式了:建造者。
假設你想要改變 ShapeView
實例的外觀 - 例如它們是否應顯示,以及用什麼顏色來填充和描邊。 建造者 設計模式使這種對象的配置變得更加容易和靈活。
解決此配置問題的一種方法是添加各類構造函數,可使用諸如 CircleShapeView.redFilledCircleWithBlueOutline()
之類的類便利初始化方法,也能夠添加具備各類參數和默認值的初始化方法。
然而不幸的是,它不是一種可擴展的技術,由於你須要爲每種組合編寫新方法或初始化程序。
建造者很是優雅地解決了這個問題,由於它建立了一個具備單一用途的類來配置已經初始化的對象。若是你將讓建造者來構建紅色的圓,而後再構建藍色的圓,則無需更改 CircleShapeView
就可達到目的。
建立一個新文件 ShapeViewBuilder.swift 並添加如下代碼:
import UIKit
class ShapeViewBuilder {
// 1
var showFill = true
var fillColor = UIColor.orange
// 2
var showOutline = true
var outlineColor = UIColor.gray
// 3
init(shapeViewFactory: ShapeViewFactory) {
self.shapeViewFactory = shapeViewFactory
}
// 4
func buildShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {
let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)
configureShapeView(shapeView: shapeViews.0)
configureShapeView(shapeView: shapeViews.1)
return shapeViews
}
// 5
private func configureShapeView(shapeView: ShapeView) {
shapeView.showFill = showFill
shapeView.fillColor = fillColor
shapeView.showOutline = showOutline
shapeView.outlineColor = outlineColor
}
private var shapeViewFactory: ShapeViewFactory
}
複製代碼
如下是你的新的 ShapeViewBuilder
的工做原理:
存儲配置 ShapeView
的填充屬性。
存儲配置 ShapeView
的描邊屬性。
初始化建造者來持有 ShapeViewFactory
從而構造 view。這意味着建造者並不須要知道它是來建造 SquareShapeView
仍是 CircleShapeView
抑或是其餘形狀的 view。
這是公共 API,當有一對 Shape
時,它會建立並初始化一對 ShapeView
。
根據建造者的存儲了的配置來對 ShapeView
進行配置。
如今來部署你新的 ShapeViewBuilder
,打開 GameViewController.swift,在大括號結束以前將如下代碼添加到類的底部:
private var shapeViewBuilder: ShapeViewBuilder!
複製代碼
如今在 viewDidLoad
裏 beginNextTurn
調用的上方添加如下代碼來填充新屬性:
shapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)
shapeViewBuilder.fillColor = UIColor.brown
shapeViewBuilder.outlineColor = UIColor.orange
複製代碼
最後用如下代碼替換 beginNextTurn
中建立 shapeViews
的那一行:
let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes: shapes)
複製代碼
編譯並運行,你將看到如下內容:
說實話我也以爲填充顏色很醜,可是先別吐槽,畢竟咱們目前關注點不在於它是有多麼好看。
如今來強化建造者的力量。仍是在 GameViewController.swift
裏,將 viewDidLoad
對應的兩行更改成使用方形工廠:
shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
shapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
複製代碼
編譯並運行,你將看到如下內容:
注意建造者模式是如何使新的顏色方案來應用到正方形和圓形上的。沒有它的話你須要在 CircleShapeViewFactory
和 SquareShapeViewFactory
中來單獨設置顏色。
此外,更改成另外一種配色方案將涉及大量代碼的修改。經過將 ShapeView
顏色配置限制爲單個 ShapeViewBuilder
,你還能夠將顏色更改隔離到單個類。
每次點擊一個形狀,你都會進行一個回合,每回合的結果能夠是得分或者減分。
若是你的遊戲能夠自動跟蹤全部回合,統計數據和得分,那會不會有幫助呢?
建立一個名爲 Turn.swift 的新文件,並使用如下代碼替換其內容:
class Turn {
// 1
let shapes: [Shape]
var matched: Bool?
init(shapes: [Shape]) {
self.shapes = shapes
}
// 2
func turnCompletedWithTappedShape(tappedShape: Shape) {
let maxArea = shapes.reduce(0) { $0 > $1.area ? $0 : $1.area }
matched = tappedShape.area >= maxArea
}
}
複製代碼
你的新 Turn
類作了如下事情:
存儲玩家每一回合看到的形狀,以及是否點擊了較大的形狀。
在玩家點擊形狀後記錄該回合已經結束。
要控制玩家玩的回合順序,請建立一個名爲 TurnController.swift 的新文件,並使用如下代碼替換其內容:
class TurnController {
// 1
var currentTurn: Turn?
var pastTurns: [Turn] = [Turn]()
// 2
init(shapeFactory: ShapeFactory, shapeViewBuilder: ShapeViewBuilder) {
self.shapeFactory = shapeFactory
self.shapeViewBuilder = shapeViewBuilder
}
// 3
func beginNewTurn() -> (ShapeView, ShapeView) {
let shapes = shapeFactory.createShapes()
let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes: shapes)
currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])
return shapeViews
}
// 4
func endTurnWithTappedShape(tappedShape: Shape) -> Int {
currentTurn!.turnCompletedWithTappedShape(tappedShape: tappedShape)
pastTurns.append(currentTurn!)
let scoreIncrement = currentTurn!.matched! ? 1 : -1
return scoreIncrement
}
private let shapeFactory: ShapeFactory
private var shapeViewBuilder: ShapeViewBuilder
}
複製代碼
你的 TurnController
工做原理以下:
存儲當前和過去的回合。
接收一個 ShapeFactory
和一個 ShapeViewBuilder
。
使用此工廠和建造者爲每一個新的回合建立形狀和視圖,並記錄當前回合。
在玩家點擊形狀後記錄回合結束,並根據該回合玩家點擊的形狀計算得分。
如今打開 GameViewController.swift,並在底部大括號上方添加如下代碼:
private var turnController: TurnController!
複製代碼
向上滾動到 viewDidLoad
,在調用 beginNewTurn
這行以前,插入如下代碼:
turnController = TurnController(shapeFactory: shapeFactory, shapeViewBuilder: shapeViewBuilder)
複製代碼
用如下代碼替換 beginNextTurn
:
private func beginNextTurn() {
// 1
let shapeViews = turnController.beginNewTurn()
shapeViews.0.tapHandler = { tappedView in
// 2
self.gameView.score += self.turnController.endTurnWithTappedShape(tappedShape: tappedView.shape)
self.beginNextTurn()
}
// 3
shapeViews.1.tapHandler = shapeViews.0.tapHandler
gameView.addShapeViews(newShapeViews: shapeViews)
}
複製代碼
你的新代碼的工做原理以下:
讓 TurnController
開始一個新的回合並返回一個 ShapeView
元組用於回合。
當玩家點擊 ShapeView
時,通知控制器回合結束,而後計算得分。請注意 TurnController
是如何把得分計算的過程抽象出來並進一步簡化 GameViewController
。
因爲你移除了對特定形狀的顯式引用,所以第二個形狀視圖能夠與第一個形狀視圖共享相同的 tapHandler
閉包。
依賴注入 設計模式的一個實例應用是它將其依賴項傳遞給 TurnController
初始化器,初始化器的參數主要是要注入的形狀和工廠的依賴項。
因爲 TurnController
沒有假定使用哪一種類型的工廠,所以你能夠自由地在不一樣的工廠間進行交換。
這不只使你的遊戲更加靈活,還讓自動化測試變得更容易了。若是你想的話,它容許你向特殊的 TestShapeFactory
和 TestShapeViewFactory
類傳遞參數。這些多是特殊的存根或模擬,可使測試更容易、更可靠而且更快速。
Build and run and check that it looks like this:編譯並運行,你會看到以下圖:
界面好像沒什麼變化,可是 TurnController
已經開放了你的代碼,因此它可使用更復雜的回合機制:根據回合計算得分而後在每一回合之間選擇性的改變形狀,甚至根據玩家的表現調整比賽難度。
我如今特別高興由於我在寫這個教程時正在吃一塊派,也許這就是爲何咱們在遊戲中要添加圓形了哈。:]
你應該感到高興,由於你在使用設計模式重構遊戲代碼方面作得很好,遊戲所以變得很容易擴展和維護。
說到派,呃,Pi,你要怎麼把這些圓形放回遊戲中呢?如今你的 GameViewController
可使用 圓或正方形,但只能使用其中一個。並不必定都要限制的死死的。
接下來你將使用 策略 模式來管理遊戲裏的形狀。
策略 設計模式容許你根據程序在運行時肯定的內容來設計算法。在這種狀況下,算法將選擇向玩家呈現什麼樣的形狀。
你能夠設計許多不一樣的算法:一種是隨機選擇形狀,一種是挑選形狀來給玩家一點挑戰或者幫助他獲勝更多,等等!策略 經過對每一個策略必須實現的行爲的抽象聲明來定義一系列算法,這使得該族內的算法能夠互換。
若是你猜測你將會把策略做爲一個 Swift protocol
來實現,那你就猜對了!
Create a new file named TurnStrategy.swift, and replace its contents with the following code:建立一個名爲 TurnStrategy.swift 的新文件,並使用如下代碼替換其內容:
// 1
protocol TurnStrategy {
func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView)
}
// 2
class BasicTurnStrategy: TurnStrategy {
let shapeFactory: ShapeFactory
let shapeViewBuilder: ShapeViewBuilder
init(shapeFactory: ShapeFactory, shapeViewBuilder: ShapeViewBuilder) {
self.shapeFactory = shapeFactory
self.shapeViewBuilder = shapeViewBuilder
}
func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView) {
return shapeViewBuilder.buildShapeViewsForShapes(shapes: shapeFactory.createShapes())
}
}
class RandomTurnStrategy: TurnStrategy {
// 3
let firstStrategy: TurnStrategy
let secondStrategy: TurnStrategy
init(firstStrategy: TurnStrategy, secondStrategy: TurnStrategy) {
self.firstStrategy = firstStrategy
self.secondStrategy = secondStrategy
}
// 4
func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView) {
if Utils.randomBetweenLower(lower: 0.0, andUpper: 100.0) < 50.0 {
return firstStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)
} else {
return secondStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)
}
}
}
複製代碼
如下是你的新的 TurnStrategy
進行的操做:
這是在一個協議中定義的一個抽象方法,該方法獲取遊戲中上一個回合的數組,並返回形狀視圖來顯示下一回合。
實現一個使用 ShapeFactory
和 ShapeViewBuilder
的基本策略,此策略實現了現有行爲,其中形狀視圖與之前同樣來自單個工廠和建造者。請注意你在此處再次使用 依賴注入,這意味着此策略不關心它使用的是哪個工廠或建造者。
隨機使用其餘兩種策略之一來實施隨機策略。你在這裏使用了組合,所以 RandomTurnStrategy
能夠表現得像兩個可能不一樣的策略。可是因爲它是一個 策略
,因此任何使用 RandomTurnStrategy
的代碼都隱藏了該組合。
這是隨機策略的核心。它以 50% 的機率隨機選擇第一種或第二種策略。
如今你須要使用你的策略。打開 TurnController.swift 並用如下內容替換:
class TurnController {
var currentTurn: Turn?
var pastTurns: [Turn] = [Turn]()
// 1
init(turnStrategy: TurnStrategy) {
self.turnStrategy = turnStrategy
}
func beginNewTurn() -> (ShapeView, ShapeView) {
// 2
let shapeViews = turnStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)
currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])
return shapeViews
}
func endTurnWithTappedShape(tappedShape: Shape) -> Int {
currentTurn!.turnCompletedWithTappedShape(tappedShape: tappedShape)
pastTurns.append(currentTurn!)
let scoreIncrement = currentTurn!.matched! ? 1 : -1
return scoreIncrement
}
private let turnStrategy: TurnStrategy
}
複製代碼
如下是詳細步驟:
接收傳遞的策略並將其存儲在 TurnController
實例中。
使用策略生成 ShapeView
對象,以便玩家能夠開始新的回合。
注意: 這將會致使 GameViewController.swift 中出現語法錯誤。可是別擔憂,這只是暫時的,你將在下一步中修復錯誤。
使用 策略 設計模式的最後一步是調整你的 GameViewController
從而來使用你的 TurnStrategy
。
打開 GameViewController.swift 並用如下內容替換:
import UIKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1
let squareShapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
let squareShapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
let squareShapeViewBuilder = shapeViewBuilderForFactory(shapeViewFactory: squareShapeViewFactory)
let squareTurnStrategy = BasicTurnStrategy(shapeFactory: squareShapeFactory, shapeViewBuilder: squareShapeViewBuilder)
// 2
let circleShapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())
let circleShapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)
let circleShapeViewBuilder = shapeViewBuilderForFactory(shapeViewFactory: circleShapeViewFactory)
let circleTurnStrategy = BasicTurnStrategy(shapeFactory: circleShapeFactory, shapeViewBuilder: circleShapeViewBuilder)
// 3
let randomTurnStrategy = RandomTurnStrategy(firstStrategy: squareTurnStrategy, secondStrategy: circleTurnStrategy)
// 4
turnController = TurnController(turnStrategy: randomTurnStrategy)
beginNextTurn()
}
override var prefersStatusBarHidden: Bool {
return true
}
private func shapeViewBuilderForFactory(shapeViewFactory: ShapeViewFactory) -> ShapeViewBuilder {
let shapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)
shapeViewBuilder.fillColor = UIColor.brown
shapeViewBuilder.outlineColor = UIColor.orange
return shapeViewBuilder
}
private func beginNextTurn() {
let shapeViews = turnController.beginNewTurn()
shapeViews.0.tapHandler = { tappedView in
self.gameView.score += self.turnController.endTurnWithTappedShape(tappedShape: tappedView.shape)
self.beginNextTurn()
}
shapeViews.1.tapHandler = shapeViews.0.tapHandler
gameView.addShapeViews(newShapeViews: shapeViews)
}
private var gameView: GameView { return view as! GameView }
private var turnController: TurnController!
}
複製代碼
你修改後的 GameViewController
使用 TurnStrategy
的詳細步驟以下:
建立一個策略來建立正方形。
建立一個策略來建立圓形。
建立策略來隨機選擇是使用正方形仍是圓形策略。
建立回合控制器來使用隨機策略。
編譯並運行,而後玩五到六輪,你應該看到相似於如下的內容。
請注意你的遊戲是如何在正方形和圓形之間隨機交替的。此時你能夠輕鬆地添加第三個形狀來,如三角形或平行四邊形,你的 GameViewController
能夠經過切換策略來使用它。
考慮一下本教程開頭的示例:
var collection = ...
// for 循環使用迭代器設計模式
for item in collection {
print("Item is: \(item)")
}
複製代碼
是什麼使得 for item in collection
這個循環工做的呢?答案是 Swift 的 SequenceType
。
經過在 for ... in
循環中使用 迭代器 模式,你能夠迭代任何遵循 SequenceType
協議的類型。
內置的集合類型 Array
和 Dictionary
是遵循 SequenceType
的,所以除非你要編寫本身的集合,不然一般不須要考慮 SequenceType
。不過我仍然很高興瞭解這個模式。:]
你常常看到的與 迭代器 結合使用的另外一種設計模式是 命令 模式,它會捕獲在被詢問時在目標上調用特定行爲的概念。
在本教程中你將使用 命令 來肯定一個 回合
的勝負並計算遊戲的分數。
建立一個名爲 Scorer.swift 的新文件,並使用如下代碼替換:
// 1
protocol Scorer {
func computeScoreIncrement<S>(_ pastTurnsReversed: S) -> Int where S : Sequence, Turn == S.Iterator.Element
}
// 2
class MatchScorer: Scorer {
func computeScoreIncrement<S>(_ pastTurnsReversed: S) -> Int where S : Sequence, S.Element == Turn {
var scoreIncrement: Int?
// 3
for turn in pastTurnsReversed {
if scoreIncrement == nil {
// 4
scoreIncrement = turn.matched! ? 1 : -1
break
}
}
return scoreIncrement ?? 0
}
}
複製代碼
依次來看看每一步:
定義你的 命令 類型並聲明它的行爲讓它接收一個你能夠用 迭代器 來迭代的過去全部回合的集合。
一個 Scorer
的具體實現,根據它們是否獲勝來計算得分。
使用 迭代器 迭代過去的回合。
將獲勝回合的得分計爲 +1,輸掉的回合得分爲 -1。
如今打開 TurnController.swift 並在類的最底部添加如下代碼:
private var scorer: Scorer
複製代碼
而後將如下代碼添加到初始化器 init(turnStrategy:)
的末尾:
self.scorer = MatchScorer()
複製代碼
Finally, replace the line in endTurnWithTappedShape
that declares and sets scoreIncrement
with the following:最後把 endTurnWithTappedShape
裏 scoreIncrement
的聲明替換爲:
let scoreIncrement = scorer.computeScoreIncrement(pastTurns.reversed())
複製代碼
注意你將在計算得分以前反轉 pastTurns
,由於計算得分的順序和回合進行的順序相反,而 pastTurns
存儲着最開始的回合,換句話說就是咱們將在數組的最後 append 最新的回合。
編譯並運行項目,你注意到一些奇怪的事了嗎?我打賭你的得分因某種緣由沒有改變。
你須要使用 責任鏈 模式來改變你的得分。
責任鏈 模式會捕獲跨一組數據調度多個命令的概念。在本練習中,你將發送不一樣的 Scorer
命令來以多種附加方式計算你的玩家得分。
例如你不只會爲比賽的勝負加或減一分,並且還會爲連續比賽的連勝得到獎勵分。責任鏈 容許你以不會打斷現有記分員的方式添加第二個 Scorer
的實現。
打開 Scorer.swift 並在 MatchScorer
裏的最上方添加如下代碼:
var nextScorer: Scorer? = nil
複製代碼
而後在 Scorer
協議的最後添加:
var nextScorer: Scorer? { get set }
複製代碼
如今 MatchScorer
和其餘全部的 Scorer
都代表它們經過 nextScorer
屬性實現了 責任鏈 模式。
在 computeScoreIncrement
裏用如下代碼替換 return
語句:
return (scoreIncrement ?? 0) + (nextScorer?.computeScoreIncrement(pastTurnsReversed) ?? 0)
複製代碼
如今你能夠在 MatchScorer
以後向鏈中添加另外一個 Scorer
並將其得分自動添加到 MatchScorer
計算的分數中。
注意:
??
運算符是 Swift 的 合併空值運算符。若是可選值非 nil 則將其值展開,若是可選值爲 nil 則返回 ?? 後的另外一個值。實際上a ?? b
與a != nil ? a! : b
同樣。這是一個很好的速記,咱們鼓勵你在你的代碼中使用它。
要來演示這一點,請打開 Scorer.swift 並將如下代碼添加到文件末尾:
class StreakScorer: Scorer {
var nextScorer: Scorer? = nil
func computeScoreIncrement<S>(_ pastTurnsReversed: S) -> Int where S : Sequence, S.Element == Turn {
// 1
var streakLength = 0
for turn in pastTurnsReversed {
if turn.matched! {
// 2
streakLength += 1
} else {
// 3
break
}
}
// 4
let streakBonus = streakLength >= 5 ? 10 : 0
return streakBonus + (nextScorer?.computeScoreIncrement(pastTurnsReversed) ?? 0)
}
}
複製代碼
你漂亮的新的 StreakScorer
工做原理以下:
連續獲勝的次數。
若是該回合獲勝,則連續次數加一。
若是該回合輸了,則連續獲勝次數清零。
計算連勝獎勵,連勝 5 場或更多場獎勵 10 分!
要完成 責任鏈 模式,打開 TurnController.swift 並將如下行添加到初始化器 init(turnStrategy:)
的末尾:
self.scorer.nextScorer = StreakScorer()
複製代碼
很好,如今你正在使用 責任鏈。
編譯並運行,在前五個回合都獲勝的狀況下,你應該看到以下截圖。
請注意分數是如何從 5 一會兒變成 16 的,由於連勝五局,計算獎勵分 10 分和第六局得到的 1 分因此一共是 16 分。
這裏是本次教程的 最終項目。
你玩了一個有趣的遊戲 Tap the Larger Shape 並使用設計模式來添加更多的形狀以及加強其樣式,你還使用了設計模式來更精確地計算得分。
最值得注意的是,即便最終項目具備更多功能,其代碼實際上比你開始時更簡單且更易於維護。
爲何不使用這些設計模式進來一步擴展你的遊戲呢?能夠嘗試一下下面的想法。
添加更多形狀,如三角形、平行四邊形、星形等 提示:回想一下如何添加圓形,並按照相似的步驟順序添加新形狀。若是你想出一些很是酷的形狀,你也能夠本身嘗試一下!
添加分數變化時的動畫 提示:在 GameView.score
上使用 didSet
。
添加控件來讓玩家選擇遊戲使用的形狀類型 提示:在 GameView
中添加三個 UIButton
或一個帶有 Square、Circle 和 Mixed 三個選項的 UISegmentedControl
,它們應該將控件上的任何點擊事件經過閉包轉發給 觀察者。GameViewController
可使用這些閉包來調整它使用的 TurnStrategy
。
將形狀設置保留爲能夠恢復的首選項 提示:將玩家選擇的形狀類型存儲在 UserDefaults
中。嘗試使用一下 外觀 模式(詳細說明)來隱藏你對其餘人的持久性機制的選擇。
容許玩家選擇遊戲的配色方案 提示:使用 UserDefaults
來持久化存儲玩家的選擇。建立一個能夠接受持久選擇並相應地調整應用程序的 UI 的 ShapeViewBuilder
。當配色方案更改時,你是否可使用 NotificationCenter
來通知全部相關的 view 來做出相應的更新呢?
每當玩家獲勝時發出慶祝的鈴聲,失敗時發出悲傷的鈴聲 提示:擴展 GameView
和 GameViewController
之間使用的 觀察者 模式。
使用依賴注入將 Scorer 傳遞給 TurnController 提示:從初始化器中移除對 MatchScorer
和 StreakScorer
的引用。
感謝你完成本教程!你能夠在評論區分享你的問題和想法以及提高遊戲逼格的方法。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。