《Delphi模式编程》——利用模板模式实现建造者模式,希望大家指正 ( 积分: 200 )

  • 主题发起人 jn_betterfly
  • 开始时间
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
在建造者模式中,我们生成一个Product实际上是通过一个Director按照确定的步骤一步一步生成
的。如模式编程里的例子,生成一个DataSet,
function TfrmMain.BuildDataSet(aBuilder:TDBBuilder;aOwner:TWinControl): TDataSet;
begin
with aBuilderdo
begin
BuildConnection(aOwner);
BuildDataSet(aOwner);
result:=GetDataSet;
end;
end;
它是按照BuildConnection,然后再BuildDataSet,最后通过GetDataSet这三个确定的步骤完成的。根据
生成的具体的DataSet不同,BuildConnection和BuildDataSet的具体内容是不同的。
仔细考虑一下,这一点和模板模式有很大的相似之处,模板模式里抽象基类里的模板方法给我们规定了一个完成某个功能的骨干或框架,但在派生类中的具体实现时会根据实际情况在实现细节上会有不同。
所以,现在再考虑一下建造者模式,只要我们要创建的Product的创建过程是固定的,不会出现建造
步骤上的顺序的不同,那么我们完全可以把建造的过程用一个和模板模式中模板方法封装起来。这样建造的过程就不用Director来完成了,直接由模板方法来实现就可以了,或者说用模板方法把Director方法替换掉。
下面写一下主要代码:
type
TDBBuilder = class(TObject)
protected
FDataSet: TDataSet;
procedure BuildConnection(AOwner:TComponent);
virtual;
abstract;//将建造的过程方法设定protected,只能由模板方法调用
procedure BuildDataSet(AOwner:TComponent);
virtual;
abstract;
public
function GetDataSet(AOwner:TComponent): TDataSet;//由GetDataSet方法来代替Director方法,也就是模板方法
end;

TADOBuilder = class(TDBBuilder)
private
FADOConnection: TADOConnection;
protected
procedure BuildConnection(AOwner:TComponent);
override;
procedure BuildDataSet(AOwner:TComponent);
override;
end;

TBEDBuilder = class(TDBBuilder)
private
FDatabase: TDatabase;
protected
procedure BuildConnection(AOwner:TComponent);
override;
procedure BuildDataSet(AOwner:TComponent);
override;
end;


{
********************************** TDBBuilder **********************************
}
function TDBBuilder.GetDataSet(AOwner:TComponent): TDataSet;//由GetDataSet来封装建造的过程,也就是定义了模板方法的框架
begin
BuildConnection(AOwner);
BuildDataSet(AOwner);
Result:=FDataSet;
end;

这样在客户端就可以直接调用GetDataSet方法,而不用再在客户端定义Director方法,即TClient.BuildDataSet.
以生成Table为例,用建造者模式在客户端代码如下:
if RadioGroup1.Buttons[0].Checked then
begin
if (FBDEBuilder=nil) then
FBDEBuilder:=TBDEBuilder.create;
FDataSet:=BuildDataSet(FBDEBuilder,self);由BuildDataSet这个Director方法负责创建过程
DBGrid1.color:=clMoneyGreen;
end;

如果用模板模式的话,代码如下:
if RadioGroup1.Buttons[0].Checked then
begin
if (FBDEBuilder=nil) then
FBDEBuilder:=TBDEBuilder.create;
FDataSet:=FBDEBuilder.GetDataSet(self);//直接调用模板方法
DBGrid1.color:=clMoneyGreen;
end;


总结,建造者模式是一个创建型模式,目的是创建一个Product,而模板模式是一个行为模式,目的是提供解决同一问题的不同方法,二者的出发点是不同的,但是如果从算法的构造上看,建造者模式和模板模式有很大的相似之处。二者都是遵循一定的步骤来完成各自的操作的,这样如果建造模式中的建造过程是固定的,那我们完全可以利用模板模式把建造过程封装起来,这样我们就没有必要再去在客户端写Director方法,如果在客户端写Director方法的话就要求我们对建造过程相当熟悉,实际上加重了我们在客户端编码的工作,如果用模板方法实现的话,我们可以不必关心他的建造过程,直接调用模板方法即可,这样就可以简化我们的客户端程序。
当然,建造者模式复用的方式实际上是由Director实现的,也就是说我们可以定义不同的Director方法来使用建造者模式中的建造过程,这时,如果不同的Director方法调用建造过程方法的顺序不同,那我们就不能利用模板方法实现了,因为模板方法中的步骤必须是固定的。但是我现在还没有想起什么很好的具体例子来说明这个问题,也就是对于同一Product,它的建造过程顺序可以不同。

以上仅是我个人的一些观点和看法,不知道对不对,希望大家批评指正
上面的例子用模板方法已经实现,如果需要源码可以e_mail给我:zhaocheng28@163.com
 
A

angellover

Unregistered / Unconfirmed
GUEST, unregistred user!
感谢LZ,发代码我研究研究,2003981088@163.com!
 
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
邮件已经发出,希望可以继续讨论!
 

无欲则刚

Unregistered / Unconfirmed
GUEST, unregistred user!
模式编程对大型的软件帮助很大 维护起来方便
而小软件没有必要耗费太多精力在模式上
 
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
D

duhai_lee

Unregistered / Unconfirmed
GUEST, unregistred user!
我的理解, 在Builder模式中, 您的代码中的。
function TfrmMain.BuildDataSet(aBuilder:TDBBuilder;aOwner:TWinControl): TDataSet;
begin
with aBuilderdo
begin
BuildConnection(aOwner);
BuildDataSet(aOwner);
result:=GetDataSet;
end;
end;
有点象装饰者模式。 个人认为这样不够赶紧利索, 就是存在一定的耦合, 例如, 我要生成的是一个DateSet, 而这个DataSet具体是Query还是Adoquery,或者table什么的,应该是它的主要职责, 否则就违背了Srp, 构造Connection应该交由其他方法或者类去完成,(最好是构造连接类去完成)。 因为BDE的连接和ADO的连接方式是不一样的。
之后的代码示例作为模板方法的,我认为比较妥当, 至少是职责分明。 若有更多的扩展还是需要定义新的接口来实现。 例如连接问题。
builder模式我用的比较少,据我了解,它是主要用于构造不同的一组对象, 一个Builder器本身需要扩展(可能这种看法欠妥),例如, 当这个需要产生DataSource时, Builder的内容并不封闭, 而是需要改动的。 当然Builder的目的是容纳一组稳定的方法,来让具体类的具体实现。
模板方法相对比较简单, 也是最常用的方法之一。 跟大宝一样, 用了都说好。
个人意见, 不正之处请斧正。
 
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
首先感谢您的参与与意见,其中
function TfrmMain.BuildDataSet(aBuilder:TDBBuilder;aOwner:TWinControl): TDataSet;
begin
with aBuilderdo
begin
BuildConnection(aOwner);
BuildDataSet(aOwner);
result:=GetDataSet;
end;
end;
这段代码是刘艺《Delphi 模式编程》里在讲解建造者模式的时候使用的代码,我也正是看到这段代码后才感觉到用模板模式可以同样实现这个功能,这样,整个建造过程就没有必要在放到客户端去写了,直接在基类里面定义好整个建造的框架,然后在派生类里具体实现建造的过程,这样就减轻了客户端代码的复杂度。
个人意见,希望大家能够继续讨论。
 
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
K

kidneyball

Unregistered / Unconfirmed
GUEST, unregistred user!
To jn_betterfly
谈谈我的理解。您的问题本质其实不在于如何用模板模式实现建造者模式,而在于建造者模式中的建造方法放在客户类中还是Builder类中。如果决定了把建造过程放在Builder端,而Builder类本身又是建造者模式中的变化点之一,那么很自然就会在Builder端使用模版模式。(注意,按照您的论述,这里的提法应该是“建造方法”而不是“Director”。因为实际应用中“Director”有可能是一个独立的类,而不是客户类中的一个方法)
那么建造过程究竟应该放在哪里呢?我的理解是,既然您决定了用Builder方法,那么建造过程就不应该放在Builder上。原因是,建造者模式最根本的出发点,就是要分离建造过程逻辑与建造的细节逻辑。现在您又把它们合并回去了。
这样导致两个问题:
(首先请假定您的类已经用二进制形式发布出去了,或者您自己本人已经忘了具体实现细节,手上只有公共接口文档。总之对已有的类只能继承不能修改。)
1. 如您所说,无法解决建造过程发生变化的情况。比如说,现在您已经为Builder类根据不同的建造细节逻辑继承了4个子类。这时如果建造过程发生哪怕一丁点变化,您就需要为每个细节子类分别再继承出1个覆盖了建造过程的子类,一下就多了4个类。如果分离了建造过程,只需要为Director继承一个子类,然后在根据需要组合就行了。
2. 这其实不是建造者模式。画个类图就很清楚了,修改之后,因为BuildConnection,BuildDataSet都变成了protected,Builder类的公共接口就只有一个GetDataSet。而且由于本来就不打算解决构建过程发生变化的情况,这个GetDataSet连virtual关键字都不用加。从整体来看,您仅仅是把一个负责初始化的类方法提取到了一个单独的类中,并且使用模版模式来重用这个类方法。这个结构其实有点象策略模式,但策略模式是为了解决算法变化的,而这里的算法灵活性却被加入了人为的限制(构建过程不能变,一变就会引起类泛滥)。
总的来说,如果只是为了避免客户端需要编写构建方法的问题,有以下几种方法:
1. 如果您不太在意面向对象规则,象您的例子中那样在Builder里写个GetDataSet也可以。但至少应该公开其他的细节方法,让客户端可以选择。这种做法有点职责不明,因为Builder本来就不应该去管构建过程的(实际开发中,写Builder的时候很可能手上只有Builder接口的需求说明,根本就不知道实际创建过程逻辑)。
2. 如果客户端有公共基类,可以在客户端用模版模式。
3. 可以把构建过程放在独立的Director类中,Director根据传入参数或当前系统状态选用恰当的Builder子类(或直接由客户端负责装配Director与Builder的具体子类),然后客户端调用Director实例中的构建方法
4. 如果真的肯定建造过程绝对不会变,用上面的做法也可以。但不应该在文档或注释中说这里用了建造者模式,通常别人看到“建造者模式”五字就会假定这里需要考虑建造过程与建造细节分别演化的问题。
一家之言,也请指正 [:)]
 
I

Iancurtis

Unregistered / Unconfirmed
GUEST, unregistred user!
刚吃早饭看到这篇文章,来不及细看,收藏,到了公司继续拜读~~`
 
J

jn_betterfly

Unregistered / Unconfirmed
GUEST, unregistred user!
首先感谢kidneyball的指正,我想还有这么几个问题可以再讨论一下:
1.正如你所说,Director类应该是一个单独的类,不一定放到界面单元中去定义,可以为它单独创建一个单元,这确实是我犯的一个错误。
2.把BuildeConnection和BuildDataset设计成protected而不是public,主要还是考虑到封装的问题,因为设置成public之后,这两个方法(对于其他的情况,创建的过程可能会更多)实际上就可以被随便调用了,而不必非要通过Diretor,这时建造的过程就可以被随便设定,这样造成的问题可能会更严重。
3.本帖实际上就是考虑的建造过程相对稳定的时候,把建造过程固定到一个框架程序中,这样既能实现良好的封装,又可以在编程时不用过多的去考虑建造的过程。对于建造过程不一定是稳定的情况,本帖的方法可能就不是很适用了,这也是你主要考虑到的。
4.虽然本帖一直在强调建造者模式,但如果从算法的角度看待建造过程,实际上完全可以把它设计成模板模式。只不过,如果从模式的分类来看,对于这样一个问题,从创建型模式看应该把它叫做建造者模式,主要应该考虑到把建造的过程和逻辑进行抽象,如果从行为型模式看,完全可以把建造的过程看成是建造算法,不同的产品它的建造的算法不同。如果对于建造过程相对稳定的情况,就可以把它封装到一个模板方法中。总之,同一问题,从不同的角度看,解决的方法也就不一样,这也就是模式的灵活性吧。
希望大家继续讨论,如有不正确之处,还望不吝赐教,谢谢!
 
K

kidneyball

Unregistered / Unconfirmed
GUEST, unregistred user!
To jn_betterfly
前几天比较忙,一直没上来。既然您有兴趣,我们可以再深入讨论一下。:)
首先我想明确一点,您这种写法在建造过程稳定,且需要限制客户类随意控制建造过程的场景中,是非常合适的。但您在贴子提到的一些观点,跟我的原本的理解不太一致,在这里提出来,大家参详参详:
1. 我觉得您这种结构的主要应用场景,还是在不希望客户类控制建造过程的情况。如果仅仅是为了方便客户类编写代码,那么使用独立的Director可以达到同样目的。虽然这样一来稍为增加了Builder端的代码量,但既然出发点是方便客户类,再考虑到独立Director提供的良好可扩展性,这点代价还是值得的。
2. 关于Builder对建造过程的控制强度问题。我不是说应该把零零碎碎的建造过程方法公开出来(除非这些方法可以无序调用而且有需要单独调用的场景),但是只要使用了独立Director,编写客户类的程序就可以通过派生出Director的子类来改变建造过程(当然,事实上可以在通过妥善设计Director进行一定程度的控制)。现在问题是在前景不明朗的情况下,究竟应不应该假定让编写客户类的程序员去修改建造过程?或者换个说法:在没有明确的限制客户类参与建造过程的业务需求时,应该为客户类控制建造过程提供支持,还是加以限制?
个人觉得应该是支持,至少应该留下可供扩展的位置。。不妨从远一点说起,我们知道使用模式的一个主要优点是,用类之间的关系取代了关联的分支关系。例如:
if c = 0 then
begin
//do something for condition 0
end
else
if c = 1 then
begin
//do something for condition 1
end;
//do lots of things for both 0 and 1
if c = 0 then
begin
//do something for 0
end
else
if c = 1 then
begin
//do something for 1
end;
显然,如果分支结构很多,每个分支结构支路很多,各个分支结构相隔很远(位于不同方法中),这样的写法极其丑陋,容易出错且不易扩展。我们就应该使用适当的模式,把这种分支结构体现到类之间的依赖关系中。这时,这些分支结构虽然被合并起来了,但总要在某个地方落脚,这个地方就是类实例关系的创建过程。可以说,面向对象编程的一个重要特点就是在创建阶段通过一定逻辑(各种分支结构等)对类实例(各种实现子类)进行组合,使得后面的处理过程以更为直观的方式进行。你写程序的时候,只要在创建时组装了正确的子类,后面只要看着父类的virtual方法就行了,而不用去管什么分支。我觉得也许正是因为创建过程如此重要,GOF才会为创建过程单独分出一类模式来。也是JAVA社区里控制反转(依赖注入)如此热门的原因:良好的框架,只需要改变创建期的类实例依赖关系,就能正确地改变整个系统的运作,因此需要令建立依赖关系的过程更加灵活。 基于上面的原因,在没有明确的业务需求来支持的情况下,我认为还是应该优先保证创建过程的灵活性。
再详细一点,保证创建过程灵活性最根本的一条惯例是,一个类A尽量不要在自身内部硬编码自身的依赖关系,而应该由外部组装。这个“外部”,通常来说,就是客户类、或A的子类,或对象工厂。一般,当创建过程与客户类的当前状态相关时,客户类是应该了解并且负责它所使用的类的组装过程的(若创建过程与客户类状态无关,则通常使用独立的对象工厂)。当然,这是“尽量”、“惯例”而已,实际场景中应以需求为准。
 

Similar threads

回复
0
查看
490
不得闲
D
回复
0
查看
2K
DelphiTeacher的专栏
D
D
回复
0
查看
2K
DelphiTeacher的专栏
D
顶部