C#设计模式 之 生成器模式

陈言必行 发表于 2021/08/22 15:25:07 2021/08/22
【摘要】 生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

别名:建造者模式、Builder

一,意图

  将一个复杂的对象的构建与其表示相分离,使得同样的构建过程可以创建不同的标识。

  生成器模式使我们能够分步骤创建复杂对象。 此模式允许你使用相同的创建代码生成不同类型和形式的对象。


二,动机

   在程序设计中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象 用一定的算法构成;由于需求的变化,这个复杂对象的 各个部分面临着剧烈的变化,,但是它们组合在一起的算法却是相对稳定的。

问题来了:
   如何应对这种变化?如何提供一种“封装机制”来隔离出 “复杂对象的各个部分” 的变化,从而保持系统中的 “稳定构建算法” 不随这需求的改变而改变?

举例理解:
   绝地求生中有很多房子,最简单的房子(厕所)由一个门,四面墙,一套窗户和一个地板一个屋顶构成。那么复杂一点的呢,有的屋子里面有小屋,有的屋子是两层的里面还要有楼梯,屋顶的形状啊;门、墙使用的材质啊,这些都是基于一个屋子扩展处理的,那么怎么办呢?

尝试解决:
   1. 最简单的方法就是拓展 “房屋“ 基类,然后创建一系列涵盖所有参数组合的子类。这么做你会发现各种类型的屋子和各种类型的组合会有特别多。而且以后只要增加一个参数(新加一个材质的门)都会使这个结构更加复杂。这将导致:如果为每种可能的对象都创建一个子类, 这可能会导致程序变得过于复杂。

   2. 另一种方法则无需生成子类,你可以在 “房屋“ 基类中创建一个包括所有可能参数的超级构造函数,并用它来控制房屋对象的生成。这种方法确实避免了生成子类,但是它会使得通常情况下,大多数参数都是没有用的。缺陷在于:拥有大量输入参数的构造函数也有缺陷: 这些参数也不是每次都要全部用上的。

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

   此模式会将对象的构造划分为一组步骤,比如 “创建门” 和 “创建墙壁” 等。每次创建对象时,你都需要通过对生成器对象执行一系列步骤。重点在于,你无需调用所有步骤,你只需要调用创建特定对象的配置所需要的那些步骤即可。

  当你需要创建不同形式的产品时,其中的一些构造步骤可能都不同的实现。(比如:木头厕所的是木头墙壁,石头厕所的是石头墙壁)

  这种情况下,你可以创建多个不同的生成器,用不同方式实现一组相同的创建步骤。然后你就可以在创建过程中使用这些生成器(如:先生成石头地板,再生成四面石头墙… 这种顺序调用构造)来生成不同类型的对象(房屋);

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


三,结构

结构

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

四,优缺点

优点:

  • 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
  • 使用生成器模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。(各种参数的重载构造函数)
  • 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
  • 生成不同形式的产品时, 你可以复用相同的制造代码。

缺点:

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

五,应用场景

适用性:

  • 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的组成方式时。
  • 当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时。
  • 如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异。

对照:

  • 抽象工厂模式解决 “系列对象” 的需求变化。
  • 生成器模式解决 “对象部分” 的需求变化。

六,代码实现

实现方式:

  1. 清晰地定义通用步骤, 确保它们可以制造所有形式的产品。 否则你将无法进一步使用此模式。
  2. 在基本生成器接口中声明这些步骤。
  3. 为每个形式的产品创建具体生成器类, 并实现其构造步骤。
    不要忘记实现获取构造结果对象的方法。 你不能在生成器接口中声明该方法, 因为不同生成器构造的产品可能没有公共接口, 因此你就不知道该方法返回的对象类型。 但是, 如果所有产品都位于单一类层次中, 你就可以安全地在基本接口中添加获取生成对象的方法。
  4. 考虑创建主管类。 它可以使用同一生成器对象来封装多种构造产品的方式。
  5. 客户端代码会同时创建生成器和主管对象。
    构造开始前, 客户端必须将生成器对象传递给主管对象。 通常情况下, 客户端只需调用主管类构造函数一次即可。 主管类使用生成器对象完成后续所有制造任务。 还有另一种方式, 那就是客户端可以将生成器对象直接传递给主管类的制造方法。
  6. 只有在所有产品都遵循相同接口的情况下, 构造结果可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。

示例代码:

  1. 创建一个生成器
  2. 创建房屋基类, 地板,墙,门,窗户,屋顶等构造步骤
 /// <summary>
 /// 接口部分 --> 基类生成器
 /// </summary>
 public abstract class Builder
 {
     // 创建 地板,墙,门,窗户,屋顶
     public abstract void BuildFloor();
     public abstract void BuildWall();
     public abstract void BuildDoor();
     public abstract void BuildWindows();
     public abstract void BuildHouseCeiling();

     // 创建房屋
     public abstract House GetHouse();
 }


 /// <summary>
 /// 房屋基类 --> 实际使用按需求修改(不一定是抽象类或者接口)
 /// </summary>
 public abstract class House
 {
 	// todo...按需添加具体实现
 }

 // 模拟各个子部分抽象
 public abstract class Floor { }
 public abstract class Wall { }
 public abstract class Door { }
 public abstract class Windows { }
 public abstract class HouseCeiling { }

  1. 一种形式的具体类生成器 – 以海岛地图上的房屋具体实现为例(生成海岛的专属门,窗,墙等各部分子对象
    ps:集合抽象工厂模式 -->后续拓展沙漠、雪地地图可按照下面代码逻辑进行复制修改拓展即可
/// <summary>
/// 海岛房屋构造器
/// </summary>
public class IslandHouseBuilder : Builder
{
    IslandHouse islandHouse;

    // 通过构造,新建一个海岛的房子
    public IslandHouseBuilder()
    {
        islandHouse = new IslandHouse();
    }
 	// todo..按需进行具体实现
    public override void BuildDoor()
    {
        // 模拟给指定的房屋创建了一个海岛的门
        // 后续在 海岛门IslandDoor 这个类中进行具体实现
        // 通过调用指定方法返回一个指定的门
        islandHouse.IslandDoor = new IslandDoor();
        Console.WriteLine("--- 创建了一个海岛地图的地板 ---");
    }

    public override void BuildFloor()
    {
        islandHouse.IslandFloor = new IslandFloor();
        Console.WriteLine("--- 创建了一个海岛地图的屋门 ---");
    }

    public override void BuildHouseCeiling()
    {
        islandHouse.IslandHouseCeiling = new IslandHouseCeiling();
        Console.WriteLine("--- 创建了一个海岛地图的屋顶 ---");
    }

    public override void BuildWall()
    {
        islandHouse.IslandWall = new IslandWall();
        Console.WriteLine("--- 创建了一个海岛地图的墙体 ---");
    }

    public override void BuildWindows()
    {
        islandHouse.IslandWindows = new IslandWindows();
        Console.WriteLine("--- 创建了一个海岛地图的窗户 ---");
    }

    public override House GetHouse()
    {
        // 将得到的房子进行一些特定的操作

        Console.WriteLine("--- 可以按照需求进行随意组合 ---");
        Console.WriteLine("--- 得到了一个海岛地图的房子 ---");

        return new IslandHouse();
    }
}


/// <summary>
/// 海岛房屋
/// </summary>
public class IslandHouse : House
{
	// 进行一个简单的关系模拟
    public Door IslandDoor;
    public Floor IslandFloor;
    public Wall IslandWall;
    public Windows IslandWindows;
    public HouseCeiling IslandHouseCeiling;
}

// todo... 具体实现海岛地图上的门,窗,墙,地板,屋顶
public class IslandDoor : Door { }
public class IslandFloor : Floor { }
public class IslandWall : Wall { }
public class IslandWindows : Windows { }
public class IslandHouseCeiling : HouseCeiling { }

4.模拟游戏主管类,

public class GameManager
{
	// todo...按需随意拓展
    public static House CreateHouse(Builder builder)
    {
        // 创建一个地板
        builder.BuildFloor();
        // 四面墙
        builder.BuildWall();
        builder.BuildWall();
        builder.BuildWall();
        builder.BuildWall();
        //门,屋顶
        builder.BuildDoor();
        builder.BuildHouseCeiling();

        return builder.GetHouse();
    }
}

  1. 模拟客户程序调用
// 模拟客户程序
class Program
{
    static void Main(string[] args)
    {
        // 创建了一个海岛地图上的房子
        House islandHouse = GameManager.CreateHouse(new IslandHouseBuilder());

        Console.ReadKey();
    }
}


测试结果:
测试结果


设计模式系列博文示例代码工程:链接



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。