设计模式六:Builder Pattern

builder
  1. director 接受参数并协调 builder。通常是一个 view controller 或者 view controller 中的一个 helper 类。
  2. product 是创建的复杂对象。 这可以是一个结构体或类,具体取决于所需的引用语义。 它通常是一个模型,但它可以为任何类型。
  3. builder 接受逐步输入并处理 product 的创建。 它通常是一个类,因此可以通过引用重用它。
什么时候使用 Builder Pattern ?

如果要使用一系列步骤创建复杂对象,请使用 Builder 模式。

当 product 需要多个输入时,此模式特别有效。 Builder 将这些输入用于创建 product 的方式进行抽象,并按照 director 想要提供的顺序接受它们。

例如,您可以使用此模式来实施 “hamburger builder” 。该 product 可以是汉堡包模型,其中包含肉类选择,浇头和调味汁等输入。 director 可以是一个员工对象,它知道如何构建汉堡包,或者它可以是一个接受用户输入的视图控制器。

因此,“hamburger builder” 可以按任何顺序接受肉类选择,浇头和酱料,并根据要求制作汉堡包。

What should you be careful about?

Builder Pattern 适合那些需要多个输入,使用一系列步骤创建的复杂 product 。 如果您的 product 没有多个输入或无法一步一步创建,那么 Builder Pattern 不适合使用。

一个示例:

CreateQuestionGroupViewController 使用 table view 接受用于创建 QuestionGroup 的输入,它显示 CreateQuestionGroupTitleCell 和CreateQuestionCell 以收集用户的输入。因此,CreateQuestionGroupViewController 是 director,而 QuestionGroup 是 product。
QuestionBuilder 和 QuestionGroupBuilder 为 builder。

CreateQuestionGroupViewController.swift:

public protocol CreateQuestionGroupViewControllerDelegate {
  func createQuestionGroupViewControllerDidCancel(_ viewController: CreateQuestionGroupViewController)

  func createQuestionGroupViewController(_ viewController: CreateQuestionGroupViewController,
                                         created questionGroup: QuestionGroup)
}

public class CreateQuestionGroupViewController: UITableViewController {

  // MARK: - Properties
  public var delegate: CreateQuestionGroupViewControllerDelegate?
  public let questionGroupBuilder = QuestionGroupBuilder()

  public override func viewDidLoad() {
    super.viewDidLoad()
    tableView.tableFooterView = UIView()
  }

  // MARK: - IBActions
  @IBAction func cancelPressed(_ sender: Any) {
    delegate?.createQuestionGroupViewControllerDidCancel(self)
  }

  @IBAction func savePressed(_ sender: Any) {
    do {
      let questionGroup = try questionGroupBuilder.build()
      delegate?.createQuestionGroupViewController(
        self, created: questionGroup)

    } catch {
      displayMissingInputsAlert()
    }
  }

  public func displayMissingInputsAlert() {
    let alert = UIAlertController(
      title: "Missing Inputs",
      message: "Please provide all non-optional values",
      preferredStyle: .alert)

    let okAction = UIAlertAction(title: "Ok",
                                 style: .default,
                                 handler: nil)
    alert.addAction(okAction)
    present(alert, animated: true, completion: nil)
  }
}

// MARK: - UITableViewDataSource
extension CreateQuestionGroupViewController {

  fileprivate struct CellIdentifiers {
    fileprivate static let add = "AddQuestionCell"
    fileprivate static let title = "CreateQuestionGroupTitleCell"
    fileprivate static let question = "CreateQuestionCell"
  }

  public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return questionGroupBuilder.questions.count + 2
  }

  public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let row = indexPath.row
    if row == 0 {
      return titleCell(from: tableView, for: indexPath)
    } else if row >= 1 &&
      row <= questionGroupBuilder.questions.count {
      return self.questionCell(from: tableView, for: indexPath)
    } else {
      return addQuestionGroupCell(from: tableView, for: indexPath)
    }
  }

  private func titleCell(from tableView: UITableView,
                         for indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.title,
                                             for: indexPath) as! CreateQuestionGroupTitleCell
    cell.delegate = self
    cell.titleTextField.text = questionGroupBuilder.title
    return cell
  }

  private func  questionCell(from tableView: UITableView,
                             for indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.question,
                                             for: indexPath) as! CreateQuestionCell
    cell.delegate = self
    let questionBuilder = self.questionBuilder(for: indexPath)
    cell.delegate = self
    cell.answerTextField.text = questionBuilder.answer
    cell.hintTextField.text = questionBuilder.hint
    cell.indexLabel.text = "Question \(indexPath.row)"
    cell.promptTextField.text = questionBuilder.prompt
    return cell
  }

  private func questionBuilder(for indexPath: IndexPath) -> QuestionBuilder {
    return questionGroupBuilder.questions[indexPath.row - 1]
  }

  private func addQuestionGroupCell(from tableView: UITableView,
                                    for indexPath: IndexPath) -> UITableViewCell {
    return tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.add,
                                         for: indexPath)
  }
}

// MARK: - UITableViewDelegate
extension CreateQuestionGroupViewController {

  public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    guard isLastIndexPath(indexPath) else { return }
    questionGroupBuilder.addNewQuestion()
    tableView.insertRows(at: [indexPath], with: .top)
  }

  private func isLastIndexPath(_ indexPath: IndexPath) -> Bool {
    return indexPath.row ==
      tableView.numberOfRows(inSection: indexPath.section) - 1
  }
}

// MARK: - CreateQuestionCellDelegate
extension CreateQuestionGroupViewController: CreateQuestionCellDelegate {

  public func createQuestionCell(_ cell: CreateQuestionCell,
                                 answerTextDidChange text: String) {
    questionBuilder(for: cell).answer = text
  }

  private func questionBuilder(for cell: CreateQuestionCell) -> QuestionBuilder  {
    let indexPath = tableView.indexPath(for: cell)!
    return questionBuilder(for: indexPath)
  }

  public func createQuestionCell(_ cell: CreateQuestionCell,
                                 hintTextDidChange text: String) {
    questionBuilder(for: cell).hint = text
  }

  public func createQuestionCell(_ cell: CreateQuestionCell,
                                 promptTextDidChange text: String) {
    questionBuilder(for: cell).prompt = text
  }
}

// MARK: - CreateQuestionGroupTitleCellDelegate
extension CreateQuestionGroupViewController: CreateQuestionGroupTitleCellDelegate {

  public func createQuestionGroupTitleCell(_ cell: CreateQuestionGroupTitleCell,
                                           titleTextDidChange text: String) {
    questionGroupBuilder.title = text
  }
}

QuestionGroupBuilder.swift:

public class QuestionGroupBuilder {

  public var questions = [QuestionBuilder()]
  public var title = ""

  public func addNewQuestion() {
    let question = QuestionBuilder()
    questions.append(question)
  }

  public func removeQuestion(at index: Int) {
    questions.remove(at: index)
  }

  public func build() throws -> QuestionGroup {
    guard self.title.count > 0 else { throw Error.missingTitle }
    guard self.questions.count > 0 else { throw Error.missingQuestions }
    let questions = try self.questions.map { try $0.build() }
    return QuestionGroup(questions: questions, title: title)
  }

  public enum Error: String, Swift.Error {
    case missingTitle
    case missingQuestions
  }
}

public class QuestionBuilder {
  public var answer = ""
  public var hint = ""
  public var prompt = ""

  public func build() throws -> Question {
    guard answer.count > 0 else { throw Error.missingAnswer }
    guard prompt.count > 0 else { throw Error.missingPrompt }
    return Question(answer: answer, hint: hint, prompt: prompt)
  }

  public enum Error: String, Swift.Error {
    case missingAnswer
    case missingPrompt
  }
}

效果图:


builder

Demo 下载

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容