面向对象之设计模式生成器(构建器)模式-原理到实战应用(Dart版)
1. 引例
前两篇博文中,我们分别使用了两个与游戏相关的例子取构建游戏角色和带有装备的游戏角色。现在我们仍然使用一个例子来讲解生成器模式。假设我们正在开发一个餐厅 点餐系统,需要创建菜品对象。菜品对象具有多个 可选 属性,例如名称、描述、价格、热量等。当我们需要创建多个不同的菜品时,需要如何实现呢?
经过思考,我们首先想到了两种方法。一种方法是为多个菜品写一个庞大的菜品基类,然后基于这个基类去生产多个菜品子类,这种方法下你不得不去面对数量众多的菜品子类,可选参数越多时,结构越复杂。另一种方法是将所有不同类的参数都放在菜品类的构造函数中,或者实现一个庞大的单一构造函数,或者使用构造函数的重载来处理不同参数,最终构造出不同的菜品。显然第二种方法有可能导致构造函数相对复杂,尤其是当参数增加,有新可选参数的菜品时,都将导致代码的大量更改。
当菜品越来越复杂时,那么有什么办法规避这种复杂局面的出现呢?这时我们可以将菜品的构造过程划分为一组构建步骤,通过一个包含这一系列创建步骤的新对象去构建这个菜品对象。由于不同菜品对象用到了不同的可选参数,因此实际上当我们具体创建某一个菜品时只会用到有限的步骤。
通过这个例子,我们实际上讨论了一个构建复杂对象的方法,通过拆分将一个复杂的构建过程分解为若干个步骤,通过组合不同的步骤生成一个具体的对象。这个有一系列步骤方组成的专门用于生成一个对象的对象称之为生成器,相应的通过这种方式来创建对象的编程模式称之为生成器模式。下一节我们将具体分析该模式中的各个组成部分及其功能原理。
2. 什么是生成器模式
2.1 生成器模式原理
生成器模式 又称 建造者模式(Builder Pattern),它允许我们使用多个简单的对象逐步构建一个复杂的对象。该模式通过将对象的 构建 过程与其 表示 分离,可以灵活地创建不同类型的对象。其结构图如 图 1 所示:
builder
Builder
+buildPartA()
+buildPartB()
+getResult()
ConcreteBuilder
+buildPartA()
+buildPartB()
+getResult()
Director
-builder: Builder
+construct()
+setBuilder(builder: Builder)
Product
-partA
-partB
+setPartA(partA)
+setPartB(partB)
图 1 生成器模式结构图
图中包括实现生成器模式的四种角色,其中 构建者(Builder)和 具体构建者(ConcreteBuilder)负责构建产品的各个部分,产品(Product)表示正在构建的复杂对象,指导者(Director)协调构建者的工作并控制构建过程。它们协同工作来实现对象的构建和表示分离,以及灵活创建复杂对象的目标。各个角色在该模式中的功能的详细描述如表1所示:
角色 | 描述 |
构建者(Builder) | 定义了创建对象各个部分的抽象接口,包括构建不同部分的方法和获取最终产品的方法。 |
具体构建者(ConcreteBuilder) | 实现了构建者接口,负责实际构建产品的各个部分,并返回最终的产品对象。 |
产品(Product) | 表示正在构建的复杂对象,通常具有多个部分。具体构建者负责构建这些部分并定义产品的组装方式。 |
指导者(Director) | 负责使用构建者对象,按照一定的步骤来创建复杂对象。指导者可以隐藏产品的具体创建过程,简化客户端代码。 |
表 1 生成器模式中的角色描述
2.2 生成器模式的使用场景
要讨论生成器模式的使用场景还需要先了解使用该模式时我们能够获得的相关优势。归纳起来生成器模式的优点主要包括:
- 它分离了复杂对象的构建过程和其表示,使得构建过程更加灵活,易于扩展和修改。
- 可以控制对象的构建过程,使得构建过程可重复使用,并且可以逐步构建对象,方便构建复杂对象。
- 隐藏了产品的内部结构,使得产品的创建过程对客户端透明。
生成器模式适用于以下场景:
- 当需要创建的对象包含复杂的部分和创建步骤时,可以使用生成器模式来简化对象的构建过程。
- 当对象的构建过程需要根据不同的参数或配置选项而有所不同时,可以使用生成器模式来灵活地构建不同类型的对象。
- 当希望通过一系列步骤来创建对象,并希望将对象的创建过程与其表示分离时,生成器模式是一个有用的选择。
- 当需要在构建过程中对产品进行精细控制,或者需要在构建过程中进行特定操作或检查时,可以使用生成器模式。
3. 用 Dart 语言编写生成器模式代码
现在回到本文开头的例子上来,我们最终需要构建的目标是不同的 菜品(Dish),它也就相当于生成器模式中的产品(Product)。产品是由 指导者(Director)使用具体构建者(ConcreteBuilder)按照一定的步骤来创建复杂对象。而(抽象)构建者(Builder)中的抽象方法实际上是对构造过程做了具体的步骤划分。比如 buildName()
方法用于构造菜品的名字, buildDescription
方法用于构造菜品的描述,buildPrice
方法用于构造菜品的价格,等等。整体上,这个例子的结构图如下:
builder
Builder
+buildName(name: String)
+buildDescription(description: String)
+buildPrice(price: double)
+buildCalories(calories: int)
+getResult()
ConcreteBuilder
-dish: Dish
+buildName(name: String)
+buildDescription(description: String)
+buildPrice(price: double)
+buildCalories(calories: int)
+getResult()
Director
-builder: Builder
+setBuilder(builder: Builder)
+construct(name: String, description: String, price: double, calories: int)
Dish
-name: String
-description: String
-price: double
-calories: int
+display()
// Dish: 菜品类 class Dish { String name; // 名称 String description; // 描述 double price; // 价格 int calories; // 热量 Dish(this.name, this.description, this.price, this.calories); void display() { print( 'Dish: $name, Description: $description, Price: $price, Calories: $calories'); } } // Builder: 抽象建造者 abstract class Builder { void buildName(String name); // 构建名称 void buildDescription(String description); // 构建描述 void buildPrice(double price); // 构建价格 void buildCalories(int calories); // 构建热量 Dish getResult(); // 获取最终结果 } // ConcreteBuilder: 具体建造者 class ConcreteBuilder implements Builder { late Dish dish; // 菜品对象 ConcreteBuilder() { reset(); } void reset() { dish = Dish('', '', 0.0, 0); } @override void buildName(String name) { dish.name = name; } @override void buildDescription(String description) { dish.description = description; } @override void buildPrice(double price) { dish.price = price; } @override void buildCalories(int calories) { dish.calories = calories; } @override Dish getResult() { final createdDish = dish; reset(); return createdDish; } } // Director: 指导者 class Director { late Builder builder; // 建造者对象 void setBuilder(Builder builder) { this.builder = builder; } void construct(String name, String description, double price, int calories) { builder.buildName(name); builder.buildDescription(description); builder.buildPrice(price); builder.buildCalories(calories); } }
可以使用如下代码进行简单测试:
// Client 代码 void main() { final director = Director(); final builder = ConcreteBuilder(); director.setBuilder(builder); // 菜品1:小炒黄牛肉 director.construct('小炒黄牛肉', '特殊小炒菜品,黄牛肉鲜嫩可口', 68.99, 350); final dish1 = builder.getResult(); dish1.display(); // 菜品2:剁椒鱼头 director.construct('剁椒鱼头', '鱼头搭配剁椒酱烹饪', 58.99, 400); final dish2 = builder.getResult(); dish2.display(); // 菜品3:小炒猪肚 director.construct('小炒猪肚', '麻辣鲜香的湖南猪肚菜品', 48.99, 300); final dish3 = builder.getResult(); dish3.display(); // 菜品4:酥香大鲫鱼 director.construct('酥香大鲫鱼', '经酥香可口的大鲫鱼', 88.99, 450); final dish4 = builder.getResult(); dish4.display(); // 菜品5:酸辣大肠 director.construct('酸辣大肠', '酸辣可口的大肠烹饪', 38.99, 250); final dish5 = builder.getResult(); dish5.display(); // 菜品6:香辣猪蹄 director.construct('香辣猪蹄', '香辣多汁的猪蹄', 68.99, 400); final dish6 = builder.getResult(); dish6.display(); }
在上面的Client代码中,我们先创建了指导者的实例和具体构建者的实例,通过 setBuilder
方法将具体构建者的实例传给指导者。这样指导者的 construct
方法就可以使用该具体构建者实例。
当我们具体用于创建某一个菜品的实例时,实际上使用的是指导者的 construct
方法,该方法的多个参数对应了多个创建的步骤,最后返回一个实际的菜品。运行结果如下:
Dish: 小炒黄牛肉, Description: 特殊小炒菜品,黄牛肉鲜嫩可口, Price: 68.99, Calories: 350 Dish: 剁椒鱼头, Description: 鱼头搭配剁椒酱烹饪, Price: 58.99, Calories: 400 Dish: 小炒猪肚, Description: 麻辣鲜香的湖南猪肚菜品, Price: 48.99, Calories: 300 Dish: 酥香大鲫鱼, Description: 经酥香可口的大鲫鱼, Price: 88.99, Calories: 450 Dish: 酸辣大肠, Description: 酸辣可口的大肠烹饪, Price: 38.99, Calories: 250 Dish: 香辣猪蹄, Description: 香辣多汁的猪蹄, Price: 68.99, Calories: 400
通过使用生成器模式,当我们的菜品构造的过程越来越复杂时,只需要对构造的步骤方法进行改动,然后通过指导者对象使用新的具体构造者中的步骤方法去构造各种不同的菜品。