C#面向对象设计模式4:生成器(Builder)

生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。它又叫建造者模式。

一、情景

假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 这些代码散落在客户端代码的多个位置。

如果为每种可能的对象都创建一个子类, 这可能会导致程序变得过于复杂。

 

例子:开了个小餐馆,提供了“手撕鸡”、“黑椒牛柳”、“酸菜鱼”这些菜,现在需要完善后厨流程,与流程相关的操作可能有冷热处理方式、洗菜、烧菜、蒸、炖、装盘、辣度、打包等。

针对这样一个稍微有一丁点小复杂的对象,比方说我们要创建一道菜品“手撕鸡”的实例对象(就是new一下),客人提出了要打包、加辣、放酱油、胡椒粉等奇葩要求,那么你的构造函数可能就有点不够用了,如下图,你是不是开始冒汗了?如果客人再提一些要求,比如要微波炉加热,你不会咚咚马上跑去改代码吧?不改的话,没有相应的构造函数啊。

好的,下面我们来思考如何创建一个菜品对象。 要制作出一道菜如“手撕鸡”,可能需要洗净、焯水、切菜、煮制、拼盘等流程,但这只是手撕鸡的流程,如果是酸菜鱼,你还需要增加去骨(但它不需要拼盘流程),如果是黑椒牛柳,你还需要增加煎炸流程(但它不需要煮制流程), 那又该怎么办呢?

最简单的方法是扩展菜品基类, 然后创建一系列涵盖所有参数组合的子类。但最终你将面对相当数量的子类。任何新增的参数都会让这个层次结构更加复杂。而且菜品是会不断新增的,你永远都涵盖不完你的参数组合。

另一种方法则无需生成子类。 你可以在菜品基类中创建一个包括所有可能参数的超级构造函数,并用它来控制菜品对象。这种方法确实可以避免生成子类, 但它却会造成另外一个问题。如上图手撕鸡是否放辣、是否打包的一样。

 

拥有大量输入参数的构造函数也有缺陷: 这些参数也不是每次都要全部用上的。而且如果需要新增参数,你加都加不完。

通常情况下,这种大量的构造函数参数都没有使用,这使得对于构造函数的调用十分不简洁。 例如,只有特定的菜品如酸菜鱼才需要去骨流程,而对于黑椒牛柳来说,这种参数是毫无用处的。

 

二、解决方案

生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。

生成器模式让你能够分步骤创建复杂对象。 生成器不允许其它对象访问正在创建中的产品。

该模式会将对象构造过程划分为一组步骤, 比如“洗净”、“焯水”等,这些是手撕鸡的生成器步骤,对于酸菜鱼来说,可能会有“去骨”的步骤,但没有”拼盘“的步骤。 每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 另外,一些构造步骤可能略有差别,比如上面说的手撕鸡和酸菜鱼,在制作产品的步骤上来看,是稍有差异的。

在这种情况下, 你可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤(当然,我们的手撕鸡和酸菜鱼,明显步骤有些不同。但是炒大白菜和炒土豆丝,它们的步骤可能是很相似的)。 然后你就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。不同生成器以不同方式执行相同的任务。

 

三、指挥

你可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的指挥类。 指挥类可定义创建步骤的执行顺序, 而生成器则提供这些步骤的实现。

指挥知道需要哪些创建步骤才能获得可正常使用的产品。

严格来说, 你的程序中并不一定需要指挥类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 指挥类中非常适合放入各种例行构造流程, 以便在程序中反复使用。

此外, 对于客户端代码来说,指挥类完全隐藏了产品构造细节。 客户端只需要将一个生成器与指挥类关联, 然后使用指挥类来构造产品, 就能从生成器处获得构造结果了。由于指挥类仍然需求根据特定顺序调用创建步骤,所以,您可以在此基础上再抽象一层工厂方法以实现这些功能,然后指挥类再调用工厂方法进行构建。

指挥相当于将您的客户端代码再包装了一层,当然您本来是可以直接在客户端代码中编写的。指挥并不是您必需的部分。

 

四、生成器模式的类图结构

  1. 生成器 (Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
  2. 具体生成器 (Concrete Builders) 提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
  3. 产品 (Products) 是最终生成的对象。 由不同生成器构造的产品无需属于同一类层次结构或接口。
  4. 指挥 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
  5. 客户端 (Client) 必须将某个生成器对象与指挥类关联。 一般情况下, 你只需通过指挥类构造函数的参数进行一次性关联即可。 此后指挥类就能使用生成器对象完成后续所有的构造任务。 但在客户端将生成器对象传递给指挥类制造方法时还有另一种方式。 在这种情况下, 你在使用指挥类生产产品时每次都可以使用不同的生成器。

五、代码模拟生成

class Program
{
    static void Main(string[] args)
    {
        酸菜鱼生成器 builder = new 酸菜鱼生成器();
        指挥 director = new 指挥(builder);
        director.取菜品();
    }
}

public interface 本店产品
{
    void 制作();
}

public class 酸菜鱼 : 本店产品
{
    public void 制作()
    {
        Console.WriteLine("酸菜鱼制作完毕");
    }
}

public class 手撕鸡 : 本店产品
{
    public void 制作()
    {
        Console.WriteLine("手撕鸡制作完毕");
    }
}

public class 指挥
{
    private 菜品生成器 _builder;

    public 指挥(菜品生成器 生成器)
    {
        this._builder = 生成器;
    }

    public 本店产品 取菜品()
    {
        //这里只是一个演示,表示类型为酸菜鱼生成器时。实际业务编写中,您可以使用枚举、直接传类型参数等方式。
        //当然,您也可以不需要指挥类,直接将步骤写在您的客户端代码中
        if (_builder is 酸菜鱼生成器 suancaiyuBuilder)
        {
            suancaiyuBuilder.洗净();
            suancaiyuBuilder.煮制();
        }
        else if (_builder is 手撕鸡生成器 shousijiBuilder)
        {
            shousijiBuilder.洗净();
            shousijiBuilder.切菜();
            shousijiBuilder.焯水();
            shousijiBuilder.煮制();
            shousijiBuilder.拼盘();
        }
        return this._builder.获得成品();
    }
}

public interface 菜品生成器
{
    本店产品 获得成品();
}

public class 酸菜鱼生成器 : 菜品生成器
{
    private 本店产品 _酸菜鱼 = new 酸菜鱼();
    public void 洗净()
    {
        Console.WriteLine("把鱼洗干净一下");
    }
    public void 煮制()
    {
        Console.WriteLine("把酸菜鱼煮制");
    }

    public 本店产品 获得成品()
    {
        this._酸菜鱼.制作();
        Console.WriteLine("酸菜鱼做好啦,客观请慢用");
        return this._酸菜鱼;
    }
}

public class 手撕鸡生成器 : 菜品生成器
{
    private 本店产品 _手撕鸡 = new 手撕鸡();
    public void 洗净()
    {
        Console.WriteLine("把鸡洗干净一下");
    }
    public void 焯水()
    {
        Console.WriteLine("把鸡焯水过滤一下");
    }
    public void 切菜()
    {
        Console.WriteLine("把鸡切好");
    }
    public void 煮制()
    {
        Console.WriteLine("把鸡用卤水煮好");
    }
    public void 拼盘()
    {
        Console.WriteLine("把鸡拼盘准备好");
    }

    public 本店产品 获得成品()
    {
        this._手撕鸡.制作();
        Console.WriteLine("手撕鸡做好啦,客观请慢用");
        return this._手撕鸡;
    }
}

程序运行结果如下:

六、生成器模式适合应用场景

1.在C#或者Java语言中,构造函数是可以重载的,如果你的产品很复杂,需要多次重载,那么使用生成器模式可避免 “重载构造函数” 的出现。

2.生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。这些步骤是您在生成器中排列好的,调用者(客户端)不知道你的顺序,也无法更改你的顺序。比如:

  •  当你希望使用代码创建不同形式的产品时,可使用生成器模式。
  •  如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。

比如组装一台汽车,比如创建一套网页模板。当然您也可以使用其它模式实现,多种模式实际是可以相互转化的。

3.基本生成器接口中定义了所有可能的制造步骤, 具体生成器将根据这些步骤来制造特定形式的产品。 同时, 指挥类将负责管理您需要制造的产品。

  • 使用生成器构造组合树或其他复杂对象。
  •  生成器模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。
  • 生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。

七、代码编写步骤

  1. 清晰地定义通用步骤, 确保它们可以制造所有形式的产品。 这里的关键地方在于这些产品的制作是有步骤顺序的。

  2. 在基本生成器接口中声明这些步骤。

  3. 为每个形式的产品创建具体生成器类, 并实现其构造步骤。

    不要忘记实现获取构造结果对象的方法。 你不能在生成器接口中声明该方法, 因为不同生成器构造的产品可能没有公共接口, 因此你就不知道该方法返回的对象类型。 但是, 如果所有产品都位于单一类层次中, 你就可以安全地在基本接口中添加获取生成对象的方法。我们建议您尽可能让产品属于同一类,这样您就可以抽象出产品并添加一个获取产品的方法。

  4. 考虑创建指挥类。 它可以使用同一生成器对象来封装多种构造产品的方式。

  5. 客户端代码会同时创建生成器和指挥对象。 构造开始前, 客户端必须将生成器对象传递给指挥对象。 通常情况下, 客户端只需调用指挥类构造函数一次即可。 指挥类使用生成器对象完成后续所有制造任务。 还有另一种方式, 那就是客户端可以将生成器对象直接传递给指挥类的制造方法。

  6. 只有在所有产品都遵循相同接口的情况下, 构造结果才可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。这与第3点有关联。

八、生成器模式优缺点

优点:

  • 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
  •  生成不同形式的产品时, 你可以复用相同的制造代码。
  •  单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。

缺点:

  •  由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。

九、与其他模式的关系

  • 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式原型模式或生成器模式 (更灵活但更加复杂)。

  • 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。

  • 你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。

  • 你可以结合使用生成器桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。

  • 抽象工厂生成器原型都可以用单例模式来实现。

 

面向对象的武功招式是一个艰难的学习和训练过程,它将耗费你3年以上的实践时间,请给自己多点耐心。即使您认为自己是爱因斯坦式的神级人物,您可能也要不少于1年以上的实践时间。

 

源码下载:

 

祝您用餐愉快。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值