比较声明式编程和命令式编程






3.70/5 (15投票s)
2005年6月17日
10分钟阅读

73851
一个简单示例,比较/对比命令式编程和声明式编程的异同。
引言
本文是对命令式编程和声明式编程之间差异的轻量级介绍。我无意争论孰优孰劣,而是通过比较和对比命令式编程,提供一个简单示例来说明声明式编程的含义。有一点我要说——对我而言,声明式编程不仅仅是 UI 定义。如果你走这条路,你会发现编程方法似乎存在一些根本性差异。当以声明式方式“编程”(即,不仅仅是构建对象图)时,我发现自己创建了大量抽象的辅助类。这有利有弊。好处是我更多地从抽象和重用(还记得 80 年代的那些流行词吗?)的角度思考。我也倾向于更多地从可重用功能块的角度思考,尤其是在处理容器和工作流时。缺点是所有这些抽象都会对性能造成影响,并且应用程序分发的程序集也更多。声明式编程不一定是“精简高效”的。换句话说,它可能不适合快速粗糙的应用程序。然而,多年来从事声明式编程(不仅仅是 XML),从长远来看,我认为由声明式代码驱动的抽象层与特定于应用程序的命令式代码相结合,可以构建出非常易于维护和扩展的应用程序。
关于本示例
我有点不情愿随本文提供下载。我使用 MyXaml 作为声明式解析器,以及几个抽象层来处理事件、工作流、容器和数据绑定。本文的重点不是代码,而是比较命令式 C# 代码与特定声明式风格(即 MyXaml 解析的 XML)的示例。如果对代码有足够的兴趣,我可能会改变我的看法并发布它。请记住,在查看声明式代码时,首先,XML 只是众多声明式语法样式之一。在 XML 的上下文中,您将在这里看到的类属性/集合类对象图样式也只是一个变体。然后,在此之内,还有特定于 MyXaml 的语法元素。我试图通过本文示例传达的不是一成不变的声明式语法,而是在比较命令式编程和声明式编程时,编程风格的可能性和差异。
设计还是不设计
命令式编程比声明式编程有一个巨大优势:设计器!在这个例子中,我没有为命令式代码使用 Visual Studio 设计器,主要是因为我想要一些比设计器生成的更精简的东西。但这确实是声明式编程的祸根——缺乏设计器支持。更困难的是,声明式编程在抽象层实现方面相当开放。因此,如果你编写一个抽象层(例如,工作流),那么接下来你将需要编写一个设计器来支持你的抽象。对于命令式编程,对设计器的需求较少,因为应用程序不一定是在考虑到这种抽象级别的情况下构建的。稍后,当我们查看工作流示例时,你会明白我的意思。
另一个问题是,Infragistics 和 DevExpress 等第三方工具包很难以声明方式使用。我已在我的博客上讨论过这个问题,并强烈建议任何有兴趣使用第三方工具包的人,为工具包实现一个声明式友好的包装器。我关于停靠管理器的博客文章是一个很好的例子,说明了为什么你要为停靠管理器创建一个包装器。
示例
该示例是一个简单的对话框,我设计用于自动生成插件程序集。它演示了:
- UI 定义。
- 用于在 UI 和业务层容器之间进行接口的数据绑定。
- 事件。
- 工作流。
应用程序初始化
命令式代码
以命令式编码的表单初始化非常简单
using System;
using System.Windows.Forms;
namespace ImperativePlugInDialog
{
public class ImperativeDemo
{
protected PluginContainer pluginContainer;
protected Form form;
[STAThread]
static void Main()
{
ImperativeDemo d=new ImperativeDemo();
d.Init();
}
public void Init()
{
... // see below
}
}
}
声明式代码
声明式示例的初始化代码稍微复杂一些
using System;
using System.Windows.Forms;
using MyXaml.Core;
using MyXaml.MxHelpers;
namespace DeclarativePlugInDialog
{
public class DeclarativeDemo
{
protected Parser parser;
[MyXamlAutoInitialize] protected MxContainer newPluginContainer;
[MyXamlAutoInitialize] protected Form newPluginDlg;
[STAThread]
static void Main()
{
DeclarativeDemo d=new DeclarativeDemo();
d.Init();
}
public void Init()
{
Parser.AddExtender("MyXaml.WinForms", "MyXaml.WinForms",
"WinFormExtender");
parser=new Parser();
parser.AddReference("App", this);
Form form=(Form)parser.Instantiate("pluginDlg.myxaml", "*");
parser.InitializeFields(this);
form.ShowDialog();
}
}
}
因为它涉及初始化解析器、添加扩展器,并添加“App”作为解析器可以用来解析事件连接的引用(稍后会详细介绍事件)。MyXaml 的一个特定功能是能够使用在解析期间创建的引用字典中的值自动初始化指定实例中的字段。
用户界面
命令式代码
在命令式世界中,用户界面由代码创建(通常通过设计器!)并定义表单、子控件和事件处理程序(这是上面提到的 Init
方法的内容)
form=new Form();
form.SuspendLayout();
#region FormControls
Label lbl=new Label();
lbl.Location=new Point(10, 10);
lbl.Size=new Size(80, 20);
lbl.Text="Plug-in name:";
lbl.TextAlign=ContentAlignment.MiddleRight;
form.Controls.Add(lbl);
TextBox tbPluginName=new TextBox();
tbPluginName.Location=new Point(95, 10);
tbPluginName.Size=new Size(150, 20);
form.Controls.Add(tbPluginName);
lbl=new Label();
lbl.Location=new Point(10, 35);
lbl.Size=new Size(80, 20);
lbl.Text="Namespace:";
lbl.TextAlign=ContentAlignment.MiddleRight;
form.Controls.Add(lbl);
TextBox tbNamespaceName=new TextBox();
tbNamespaceName.Location=new Point(95, 35);
tbNamespaceName.Size=new Size(150, 20);
form.Controls.Add(tbNamespaceName);
lbl=new Label();
lbl.Location=new Point(10, 60);
lbl.Size=new Size(80, 20);
lbl.Text="Class name:";
lbl.TextAlign=ContentAlignment.MiddleRight;
form.Controls.Add(lbl);
TextBox tbClassName=new TextBox();
tbClassName.Location=new Point(95, 60);
tbClassName.Size=new Size(150, 20);
form.Controls.Add(tbClassName);
lbl=new Label();
lbl.Location=new Point(10, 85);
lbl.Size=new Size(80, 20);
lbl.Text="Create in:";
lbl.TextAlign=ContentAlignment.MiddleRight;
form.Controls.Add(lbl);
TextBox tbPath=new TextBox();
tbPath.Location=new Point(95, 85);
tbPath.Size=new Size(215, 20);
form.Controls.Add(tbPath);
Button btnPath=new Button();
btnPath.Location=new Point(315, 85);
btnPath.Size=new Size(30, 20);
btnPath.Text="...";
btnPath.FlatStyle=FlatStyle.System;
btnPath.Click+=new EventHandler(OnGetPath);
form.Controls.Add(btnPath);
Button btnOK=new Button();
btnOK.Location=new Point(270, 10);
btnOK.Size=new Size(80, 25);
btnOK.FlatStyle=FlatStyle.System;
btnOK.Text="OK";
btnOK.Click+=new EventHandler(OnOK);
form.Controls.Add(btnOK);
Button btnCancel=new Button();
btnCancel.Location=new Point(270, 35);
btnCancel.Size=new Size(80, 25);
btnCancel.FlatStyle=FlatStyle.System;
btnCancel.Text="Cancel";
form.Controls.Add(btnCancel);
form.Text="New Plug-in (Imperative Demo)";
form.StartPosition=FormStartPosition.CenterScreen;
form.ClientSize=new Size(360, 120);
form.AcceptButton=btnOK;
form.CancelButton=btnCancel;
form.ResumeLayout();
#endregion
声明式代码
声明式代码需要命名空间映射
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) 2005 MyXaml All Rights Reserved -->
<MyXaml xmlns="System.Windows.Forms,
System.Windows.Forms,
Version=1.0.5000.0,
Culture=neutral,
PublicKeyToken=b77a5c561934e089"
xmlns:myxaml="MyXaml.Core"
xmlns:mouse="Clifton.Tools.Events"
xmlns:mxh="MyXaml.MxHelpers"
xmlns:flow="MyXaml.MxFlowPanel"
xmlns:ev="MyXaml.MxEventVector"
xmlns:wf="MyXaml.MxWorkflow"
xmlns:def="Definition"
xmlns:ref="Reference">
这类似于向您的命令式程序添加必要的引用。命名空间映射的格式是 MyXaml 特有的,并且与您在 Microsoft 的 XAML 中看到的略有不同。您可以在其他地方阅读更多相关信息。
声明式定义的用户界面构建了一个 Form 及其子控件的对象图
<Form def:Name="newPluginDlg"
Text="New Plug-in (Declarative Demo)"
StartPosition="CenterScreen"
FormBorderStyle="FixedDialog"
ClientSize="360, 120"
AcceptButton="{btnOk}"
CancelButton="{btnCancel}">
<Controls>
<flow:TableLayoutPanel Location="10, 10"
Size="250, 70"
ColumnWidths="80, 150"
RowHeights="20, 20, 20"
ColumnStyles="Fixed, Fixed"
RowStyles="Fixed, Fixed, Fixed"
VerticalSpacing="5"
HorizontalSpacing="5">
<Controls>
<Label Text="Plug-in name:" TextAlign="MiddleRight"/>
<TextBox def:Name="tbPluginName"/>
<Label Text="Namespace:" TextAlign="MiddleRight"/>
<TextBox def:Name="tbNamespaceName"/>
<Label Text="Class name:" TextAlign="MiddleRight"/>
<TextBox def:Name="tbClassName"/>
</Controls>
</flow:TableLayoutPanel>
<Panel Location="10, 85" Size="335, 20">
<Controls>
<Label Location="0, 0" Size="80, 20" Text="Create in:"
TextAlign="MiddleRight"/>
<TextBox def:Name="tbPath" Location="85, 0" Size="215, 20"/>
<Button Location="305, 0" Size="30, 20" Text="..." FlatStyle="System"
MxEventVector="Click; workflowProc.Execute(SelectPluginFolder)"/>
</Controls>
</Panel>
<Button def:Name="btnOk" Location="270, 10" Text="OK" Size="80, 25"
FlatStyle="System" Click="{App.CreatePlugin}"/>
<Button def:Name="btnCancel" Location="270, 35" Text="Cancel"
Size="80, 25" FlatStyle="System"/>
</Controls>
</Form>
你会注意到我在命令式代码中没有使用 `TableLayoutPanel`。不是我不能用,只是除非你使用 .NET 2.0 或者在设计器中有可用的组件,否则你可能不会用。对于声明式代码,这是一个更多地思考问题(在本例中是定义所有位置和大小)的例子。对于命令式代码,你根本不会去想它,因为设计器太容易使用了。正如我之前提到的,声明式方法让你更多地思考这些事情,即使仅仅是为了节省一些输入。
这里有一些 MyXaml 特有的语法元素。例如,“`def:`”前缀告诉解析器通过其 `Name` 属性值记住实例。花括号 {} 告诉解析器正在引用该名称的对象,或者,在上述情况下,正在连接一个事件。我稍后会简要讨论 `MxEventVector` 属性。
业务层容器
我使用带有数据绑定的业务层容器,这比直接从控件属性中提取信息是一种更好的编程实践。对于像这样的简单对话框,这有点不必要,但让我们稍微超越这个例子思考一下。
命令式代码
在命令式代码中,我必须创建一个具有属性和事件的类,以便绑定机制可以挂钩,从而实现双向数据绑定
#region Containers
public class PluginContainer
{
protected string pluginName;
protected string pluginNamespaceName;
protected string pluginClassName;
protected string pluginPath;
public event EventHandler PluginNameChanged;
public event EventHandler PluginNamespaceNameChanged;
public event EventHandler PluginClassNameChanged;
public event EventHandler PluginPathChanged;
public string PluginName
{
get {return pluginName;}
set
{
pluginName=value;
if (PluginNameChanged != null)
{
PluginNameChanged(this, EventArgs.Empty);
}
}
}
public string PluginNamespaceName
{
get {return pluginNamespaceName;}
set
{
pluginNamespaceName=value;
if (PluginNamespaceNameChanged != null)
{
PluginNamespaceNameChanged(this, EventArgs.Empty);
}
}
}
public string PluginClassName
{
get {return pluginClassName;}
set
{
pluginClassName=value;
if (PluginClassNameChanged != null)
{
PluginClassNameChanged(this, EventArgs.Empty);
}
}
}
public string PluginPath
{
get {return pluginPath;}
set
{
pluginPath=value;
if (PluginPathChanged != null)
{
PluginPathChanged(this, EventArgs.Empty);
}
}
}
public PluginContainer()
{
pluginName=String.Empty;
pluginNamespaceName=String.Empty;
pluginClassName=String.Empty;
pluginPath=String.Empty;
}
}
#endregion
至此,我们完成了该类,可以在下一节“数据绑定”中使用它。
声明式代码
在声明式代码中,我当然可以直接利用命令式类,实例化它,并构建一个数据绑定对象图。但为什么要这么做呢?我想以声明方式定义容器
<mxh:MxContainer def:Name="newPluginContainer">
<mxh:MxObjects>
<mxh:MxObject def:Name="pluginName" Type="System.String"/>
<mxh:MxObject def:Name="pluginNamespaceName" Type="System.String"/>
<mxh:MxObject def:Name="pluginClassName" Type="System.String"/>
<mxh:MxObject def:Name="pluginPath" Type="System.String"/>
</mxh:MxObjects>
</mxh:MxContainer>
所以,这里我使用了几个抽象层——`MxContainer` 和 `MxObject`。容器管理对象的集合,而 `MxObject` 类实现了各种转换方法和事件处理程序。
数据绑定
下一步是在实例化容器之后,将容器的属性绑定到控件属性。
命令式代码
pluginContainer=new PluginContainer();
tbPluginName.DataBindings.Add("Text", pluginContainer, "PluginName");
tbNamespaceName.DataBindings.Add("Text", pluginContainer,
"PluginNamespaceName");
tbClassName.DataBindings.Add("Text", pluginContainer, "PluginClassName");
tbPath.DataBindings.Add("Text", pluginContainer, "PluginPath");
声明式代码
<mxh:MxBinder def:Name="newPluginBinding">
<mxh:MxBindings>
<mxh:MxBinding Target="{tbPluginName}" PropertyName="Text"
DataSource="{pluginName}" DataMember="AsString"/>
<mxh:MxBinding Target="{tbNamespaceName}" PropertyName="Text"
DataSource="{pluginNamespaceName}" DataMember="AsString"/>
<mxh:MxBinding Target="{tbClassName}" PropertyName="Text"
DataSource="{pluginClassName}" DataMember="AsString"/>
<mxh:MxBinding Target="{tbPath}" PropertyName="Text"
DataSource="{pluginPath}" DataMember="AsString"/>
</mxh:MxBindings>
</mxh:MxBinder>
这里我使用了另一个助手,这样我就可以将绑定与 UI 定义分离。我更喜欢这种方法进行声明式编码,而不是将数据绑定图直接放入 UI 图中。而且,由于 .NET 1.1 的 `Binding` 类没有无参数构造函数,无论如何我都需要使用一个助手类。
事件
我将演示两个事件——点击“确定”按钮和点击“...”按钮,它会调出 FolderBrowserDialog。
命令式代码
在命令式代码中连接事件非常简单
btnPath.Click+=new EventHandler(OnGetPath);
btnOK.Click+=new EventHandler(OnOK);
事件处理程序也足够直接
private void OnOK(object sender, EventArgs e)
{
MessageBox.Show(pluginContainer.PluginName, "Creating plugin:");
form.Close();
}
private void OnGetPath(object sender, EventArgs e)
{
FolderBrowserDialog pluginFolderDlg=new FolderBrowserDialog();
pluginFolderDlg.Description="Select folder for plug-in:";
pluginFolderDlg.SelectedPath=pluginContainer.PluginPath;
pluginFolderDlg.ShowDialog();
pluginContainer.PluginPath=pluginFolderDlg.SelectedPath;
}
声明式代码
在声明式代码示例中,我想让你体验两件事——将事件定向到任何方法,而不仅仅是事件处理程序,以及使用工作流而不是命令式代码。
我将首先处理简单事件——连接“确定”按钮的 `Click` 事件
<Button def:Name="btnOk"
Location="270, 10"
Text="OK"
Size="80, 25"
FlatStyle="System"
Click="{App.CreatePlugin}"/>
这足够简单。应用程序实例提供了一个 `CreatePlugin` 方法,其功能与命令式代码完全相同,唯一的区别在于容器的引用方式(稍后讨论)
public void CreatePlugin(object sender, EventArgs e)
{
MessageBox.Show(newPluginContainer["pluginName"].AsString,
"Creating plugin:");
newPluginDlg.Close();
}
工作流事件处理程序稍微复杂一些。
MxEventVector="Click; workflowProc.Execute(SelectPluginFolder)
在声明式代码中,我希望能够将事件直接连接到另一个实例中的方法,而无需编写命令式事件处理程序将事件传递给实例方法。在这种特定情况下,我希望将 `Click` 事件定向到工作流处理器的 `Execute` 方法,并让它执行特定的工作流。`MxEventVector` 属性是一个“扩展”属性,解析器允许它,因为在声明式代码中,我已经实例化了一个 `MxEventVector` 实例,它告诉解析器“我知道如何处理这个属性当我看到它时”
<ev:MxEventVector def:Name="eventVector"/>
这是 MyXaml 特有的功能——能够扩展其他类的属性。但它也展示了声明式编程可以实现的一些有趣功能。所有这些的底层是一些功能,它们创建具有适当委托签名的事件处理程序,绑定事件,并将事件定向到指定的实例方法。所有这些的细节超出了本文的范围。
工作流程
工作流通常不属于命令式编程的概念。它们通常是一系列脚本化的指令,其中每条指令都引用一个已通过命令式编码的方法。工作流的理念是将通用功能和应用程序特定功能串联起来,以创建特定于应用程序/用户的操作。工作流使得可以轻松地根据不同的需求调整正在完成的工作,描述元级别逻辑控制,并解耦离散的功能块。例如,您可以使用工作流来跨越插件,而无需应用程序在构建时链接不同的模块。如果做得好,工作流会增加非常少的处理开销,因为它们只是充当一个相当愚蠢的指令队列,可能带有一些用于测试、分支和循环的基本逻辑。实际工作是在命令式代码中完成的。
我选择了一种工作流风格来演示如何以声明方式编写命令式事件处理程序 `OnGetPath`
<FolderBrowserDialog def:Name="pluginFolderDlg"
Description="Select folder for plug-in:"/>
<wf:MxWorkflowProcessor def:Name="workflowProc" DataContainer="{MyXamlDefs}">
<wf:Workflows>
<wf:Workflow Name="SelectPluginFolder">
<wf:Statements>
<wf:Set Target="{pluginFolderDlg}" Property="SelectedPath"
Value="*pluginPath.AsString"/>
<wf:Call Target="{pluginFolderDlg}" Method="ShowDialog"/>
<wf:Set Target="{pluginPath}" Property="AsString"
Value="*pluginFolderDlg.SelectedPath"/>
</wf:Statements>
</wf:Workflow>
</wf:Workflows>
</wf:MxWorkflowProcessor>
XML 的第一行实例化了 `FolderBrowserDialog` 类——这在创建插件对话框时完成。工作流演示了上面命令式代码示例的最后三行现在是如何在 XML 中编码的。所以,我们在这里跨越了界限——使用声明式语法来描述通常是命令式代码。这并不是最好的例子,但我希望它能说明使用工作流的想法。
在上面的声明式示例中,你再次看到了 MyXaml 特有的风格:使用星号表示一个引用(与它在 C++ 中的含义完全相反),该引用在语句执行时解析,而不是在描述工作流的对象图实例化时解析(这是花括号的作用)。
引用容器属性
引用命令式容器属性非常简单。它提供了对拼写和属性转换的编译时检查。引用声明式容器属性更耗时,并且容易出现拼写和属性转换错误,这些错误直到运行时才能检测到。
命令式代码
MessageBox.Show(pluginContainer.PluginName, "Creating plugin:");
简单,对吧?只需使用容器所需的属性。
声明式代码
MessageBox.Show(newPluginContainer["pluginName"].AsString, "Creating plugin:");
这并不那么简单,因为我们使用了一个索引器,其值是要返回的“属性”的名称,而且我们还必须声明所需的类型。部分原因在于底层的 `MxObject` 实现,部分原因在于声明式容器施加的抽象层。
结论
我读到很多关于声明式编程如何如何好,因为例如,UI 定义占用的代码行更少。嗯,不要陷入这种思维陷阱。毕竟,有数百行解析器代码支持声明式 UI 定义。相反,思考如何利用声明式编程,不仅在对象图定义方面,还在编程的其他方面——容器、事件、工作流、插件架构等。您希望将功能抽象到何种程度以获得维护和/或扩展的灵活性?您希望如何管理配置信息?哪些事物可以以声明方式比命令式方式更好地描述?声明式编程的好处在于您可以选择如何使用它。