Builder模式应用实践-阿里云开发者社区

开发者社区> 技术小美> 正文

Builder模式应用实践

简介:
+关注继续查看

本文为《软件设计精要与模式》第十七章,略有修改

在GOF 所著的《设计模式》一书中,描述了Builder模式的意图:“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”按照封装变化的原理,Builder模式实则是封装对象创建的变化,但它与Factory Method模式、Abstract Factory模式不同的是,所谓对象的创建,主要是指对象内部构件的创建。形象地说,Builder模式就好似生产线的装配工人,可以接收多种方式与顺序组装各种零部件。本章,将给出我参与设计与开发的CIMS系统中的一个需求,详细讲解Builder模式的应用。

17.1  需求分析——装配设备对象

在 CIMS(Computer Integrated Manufacture System)项目中,有这样一个需求。系统中需要创建Equipment对象,这些对象由Machine对象和多个Port对象组成。Port对象包含两种类型:Input与Output。在Machine对象中,定义了PortType属性,它的值与Port对象的类型相对应。从目前的需求来看,Equipment对象,存在如下三种组成情况:
— 一个Machine对象,一个Input类型的Port对象;
— 一个Machine对象,一个Output类型的Port对象;
— 一个Machine对象,一个Input类型的Port对象,一个Output类型的Port对象。

客户方不排除将来有增加新的Equipment组合的可能。

需求清晰而简单,我们可以非常容易地识别出Port、Machine、Equipment对象。Port类的定义如下:

C#语言:
public abstract class Port
{
    public abstract void Transfer();
}
public class InputPort:Port
{
    public override void Transfer()
    {
        Console.WriteLine("Input");
    }
}
public class OutputPort:Port
{
    public override void Transfer()
    {
        Console.WriteLine("Output");
    }
}

Port对象由于类型的不同,方法Transfer会有不同的实现,因此,我们定义了一个抽象Port类,然后定义其子类InputPort和OutputPort,分别代表两种不同类型的Port。

Machine类的定义相对比较简单,如下所示。

C#语言
public class Machine
{
    public Machine() { }
    public Machine(string name)
    {
        Name = name;
    }
    
    public string Name
    {
        get; set;
    }
    public string PortType
    {
        get;set;
    }
    public void Run()
    {
        Console.WriteLine("The machine {0} is running!", Name);
    }
}

由于Equipment对象可能会包含多个Port,因此Equipment类引入集合对象来保存Port对象,并利用泛型限制集合元素的类型,通过强类型的方式避免强制转换时可能出现的异常或错误。

C#语言:
public class Equipment
{
    public Equipment()
    {            
        PortsList = new List<Port>();
    } 
    public Machine Machine
    {
        getset
    }
    public List<Port> PortsList
    {
        get
    } 
    public string Name
    {
        get; set;
    }            

    public void AddPort(Port port)
    {
        m_list.Add(port);
    }
    public void RemovePort(Port port)
    {
        m_list.Remove(port);
    }
    public void Run()
    {
        Console.WriteLine("The Equipment {0} is running as below...",m_name);
        foreach (Port port in m_list)
        {
            port.Transfer();
        }
        m_machine.Run();
    }
}

Equipment的Run方法会遍历List<Port>中的元素,并执行Port类型的Transfer方法,然后再执行Machine对象的Run方法。

17.2  糟糕的设计

单以现有的需求而论,上面的设计已经基本能够满足客户的要求。例如我们创建一个Equipment对象,它包含一个Machine对象,以及两个Port对象,一个为Input类型,一个为Output类型:

C#语言:
Machine machine = new Machine("InputOutputMachine");

Port inputPort = new InputPort();
Port outputPort = new OutputPort();
Equipment eqp = new Equipment();

eqp.Name = machine.Name;
eqp.PortType = "InputOutput";

eqp.AddPort(inputPort);
eqp.AddPort(outputPort);

然而,这样的实现存在以下三个明显的缺陷:
(1)创建过程过于繁杂,不便于调用方调用;
(2)Port对象的添加过程无法控制;
(3)过于僵化,不利于程序的扩展;
事实上,每当我们创建一个Equipment对象时,都需要执行与上面实现相似的代码,那么为何不将这样的创建职责进行封装,以便于程序的重用呢?我们首先想到的是工厂模式。既然是创建三种Equipment对象,利用简单工厂模式即可,例如定义一个类SimpleLCDFactory:

C#语言
public static class SimpleLCDFactory
{
    public static Equipment CreateInputEQP(string eqpName)
    {
        Machine machine = new Machine("InputMachine");
        Port inputPort = new InputPort();

        Equipment eqp = new Equipment();
        eqp.Name = machine.Name;
        eqp.PortType = "Input";

        eqp.AddPort(inputPort);
    }
    public static Equipment CreateOutputEQP(string eqpName)
    {
        Machine machine = new Machine("OutputMachine");
        Port outputPort = new OutputPort();

        Equipment eqp = new Equipment();
        eqp.Name = machine.Name;
        eqp.PortType = "Output";

        eqp.AddPort(outputPort);
    }
    public static Equipment CreateIOPutEQP(string eqpName)
    {
        Machine machine = new Machine("InputOutputMachine");
        Port inputPort = new InputPort();
        Port outputPort = new OutputPort();
        Equipment eqp = new Equipment();
        eqp.Name = machine.Name;
        eqp.PortType = "InputOutput";

        eqp.AddPort(inputPort);
        eqp.AddPort(outputPort);
    }
}

这样一来,我们要创建一个Equipment对象就相对容易多了:

C#语言:
Equipment eqp = SimpleLCDFactory.CreateIOPutEQP("InputOutputMachine");

由于提供了专门的方法来创建Equipment对象,之前提到的前两个缺陷就被有效地克服了。然而,由于采用的简单工厂模式并没有将可能存在的创建变化进行抽象,导致这样的结构仍然僵化,不易于扩展。例如,在增加一个新的Equipment对象组合时,就需要在SimpleLCDFactory类中添加一个新的方法,来创建这个对象。

要支持未来需求可能的变化,我们也可以引入Factory Method模式,将创建Equipment对象的方法进行抽象,具体的实现留待各自的实现子类来完成。坦白说,这未尝不是一个好的实现方案。然而,考虑到这里要创建的Equipment对象,是由Port对象和Machine对象组成的,我们关注的创建行为,归根结底,是如何将Port对象和Machine对象创建好,并使之组合成我们需要的整体对象Equipment,因此,采用Builder模式也许是更好的选择。

注意:实际上Factory Method模式与Builder模式并没有泾渭分明的区别,尤其针对Builder模式,我们都可以将其转换为Factory Method模式,反之则不然。也就是说,Factory Method模式的应用更加广泛。如果一定要作出区分,那么可以说,二者同时生成产品,Factory Method模式主要用于生成一类完整的产品,而Builder模式则更关注于产品内部各种零件的组合,并最终组装为整体的产品。由于Builder模式对产品内部的创建进行了细分,因此对于那些内部具有一定结构的目标对象,如本例的Equipment,最佳选择仍然是Builder模式。

17.3  引入Builder模式

就Builder模式来说,最重要的当然是Build行为的抽象。以本例而言,要创建一个Equipment对象,需要Build的是Port和Machine对象,所以我们可以定义两个Build方法的抽象:

C#语言:
void BuildPort();
void BuildMachine(string machineName);

按照这样一个原则,我们可以建立如图17-1所示的类图:

 

图17-1  Builder类的继承体系

实现代码如下:

C#语言
public abstract class EQPBuilder
{        
    protected Equipment m_equipment;
    protected Machine m_machine;

    public EQPBuilder()
    {
        m_equipment = new Equipment();
    }

    public abstract void BuildPort();
    public virtual void BuildMachine(string name)
    {
        m_machine = new Machine(name);            
        m_equipment.Name = name;
        m_equipment.Machine = m_machine;
    }
    public virtual Equipment GetEQP()
    {
        return m_equipment;
    }
}

public class InputEQPBuilder : EQPBuilder
{        
    public override void BuildPort()
    {
        Port port = new InputPort();
        m_equipment.AddPort(port);
    }
    public override void BuildMachine(string name)
    {
        base.BuildMachine(name);
        m_machine.PortType = "Input";            
    }
}

public class OutputEQPBuilder : EQPBuilder
{        
    public override void BuildPort()
    {
        Port port = new OutputPort();
        m_equipment.AddPort(port);
    }
    public override void BuildMachine(string name)
    {
        base.BuildMachine(name);
        m_machine.PortType = "Output";            
    }
}

public class IOPutEQPBuilder : EQPBuilder
{        
    public override void BuildPort()
    {
        Port inputPort = new InputPort();
        Port outputPort = new OutputPort();

        m_equipment.AddPort(inputPort);
        m_equipment.AddPort(outputPort);
    }
    public override void BuildMachine(string name)
    {
        base.BuildMachine(name);
        m_machine.PortType = "InputOutput";            
    }
}

由于Builder子类对于Port对象的创建完全不同,因此我们将父类EQPBuilder中的CreatePort方法定义为抽象方法;而创建Machine对象的CreateMachine方法,则因为具有一些共同的逻辑,可以在父类中提供实现,所以被定义为虚方法,并将相同的逻辑抽象到父类中。EQPBuilder的子类需要重写父类的CreateMachine方法,并在调用父类的CreateMachine方法后,根据创建的Port对象的不同,分别设置Machine对象的PortType值。

此外,在抽象类EQPBuilder中,我还定义了一个虚方法GetEQP,该方法用于返回一个Equipment对象,这个对象实则就是EQPBuilder类型对象所创建的产品。由于各个Builder子类返回Equipment对象的实现逻辑完全一样,所以在子类中就不必改写该方法了。

在Builder模式的实现中,我们已经有了Product(产品)角色Equipment类对象;有了Builder(建造者)角色EQPBuilder类对象,以及它的派生子类。现在还缺乏一个Director(指导者)角色,用以引入具体建造者角色,指导完成产品的创建。该角色类似于工厂模式中的工厂对象。因此,我将其定义为LCDFactory,便于调用者理解其职能:

C#语言:
public static class LCDFactory
{
    public static Equipment CreateEQP(EQPBuilder buider, string name)
    {
        buider.BuildPort();
        buider.BuildMachine(name);
        return buider.GetEQP();
    }
}

由于LCDFactory类的静态方法CreateEQP接受的参数是抽象类EQPBuilder,所以指导者角色与建造者角色之间仅存在弱依赖关系,这保证了Builder的扩展不会影响产品的创建,类图如17-2所示:

 

图17-2 Builder模式的实现类图

与标准的Builder模式不同,为了调用方便,我将EQPBuilder类型中的GetEQP方法也封装到了LCDFactory类中,因此客户端的调用方式应该是这样:

C#语言
class Program
{
    static void Main(string[] args)
    {
        EQPBuilder builder = new InputEQPBuilder();
        Equipment eqp = LCDFactory.CreateEQP(builder, "InputMachine");
        eqp.Run();

        builder = new IOPutEQPBuilder();
        eqp = LCDFactory.CreateEQP(builder, "InputOutputMachine");
        eqp.Run();

        Console.Read();
    }
}

17.4  从容应对扩展

即使创建Equipment的需求发生了变化,应用了Builder模式的设计也能够从容应对。例如要求创建的Equipment包含一个Machine对象,一个Input类型的Port,两个Output类型的Port,那么我们可以在不修改原有程序集的前提下,新定义一个IO2PutEQPBuilder类,并继承自抽象类EQPBuilder:

C#语言
public class IO2PutEQPBuilder: EQPBuilder
{        
    public override void BuildPort()
    {
        Port inputPort = new InputPort();
        Port outputPort1 = new OutputPort();
        Port outputPort2 = new OutputPort();

        m_equipment.AddPort(inputPort);
        m_equipment.AddPort(outputPort1);
        m_equipment.AddPort(outputPort2);
    }
    public override void BuildMachine(string name)
    {
        base.BuildMachine(name);
        m_machine.PortType = "InputOutput2";            
    }
}

此时,客户端可以根据传递进来的EQPBuilder类型对象,生产出新的Equipment产品。例如:

C#语言:
class Program
{
    static void Main(string[] args)
    {
        EQPBuilder builder = new IO2PutEQPBuilder();
        Equipment eqp = LCDFactory.CreateEQP(builder, " InputOutput2Machine");
        eqp.Run();

        Console.Read();
    }
}

我们甚至可以引入配置文件,利用反射技术动态创建具体的EQPBuilder对象,从而完全达到松散耦合开放扩展的目的。本例是项目开发的真实实践,通过对Builder模式的引入,给程序结构带来了有利的变化。这足以证明,如果能够合理地运用设计模式,就足以弥补好的设计与坏的设计之间巨大的鸿沟。









本文转自wayfarer51CTO博客,原文链接:http://blog.51cto.com/wayfarer/280151,如需转载请自行联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析(五)
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析
33 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
8798 0
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析(四)
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析
35 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10614 0
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析(四)
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析
44 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
12455 0
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析(九)
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析
19 0
Hbuilder应用离线本地自定义打包
Hbuilder离校本地Android应用打包
13660 0
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析(十)
带你读《云原生应用开发 Operator原理与实践》第三章 Kubebuilder 原理3.3 Controller-runtime 模块分析
48 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
6757 0
+关注
6906
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载