Command模式应用实践

一、 需求分析

在我们开发的一个项目中,需要自己开发一个安装系统,便于部署特定环境下的产品系统。要求的安装界面如图19-1所示:
 SetupUI.gif
图19-1 安装系统界面
根据安装步骤的不同,所要执行的安装逻辑也相应不同,然而客户要求的是能够提供“上一步”和“下一步”的操作,以便于随时回退或者继续向前安装。这也是一般安装系统所应具备的功能。
根据对安装业务的分析,整个安装共分为七个步骤:
(1)检测本地计算机并初始化安装系统;
(2)选择要安装的压缩包;
(3)复制安装所需的临时文件;
(4)选定安装目录;
(5)准备安装;
(6)执行安装;
(7)安装成功,退出。
整个安装步骤中,每一步的安装界面都不相同,而这些界面都由特定的User Control体现,例如User Control中的提示信息、按钮、进度条等。

二、 坏的设计

既然我们为每一个安装步骤定义了不同的UserControl对象,那么在每次执行安装时,仅需要利用Pannel控件的添加子控件方法即可,例如定义如下的方法:
private void AddBodyUC(UserControl uc)
{
 panBody.Controls.Clear();
 panBody.Controls.Add(uc); 
 panBody.Refresh();
}
然后我们直接在主窗体中定义若干执行安装的方法,且根据安装的方向分类,例如:
private void PreviousStep1()
{}
private void NextStep1()
{}
我们接着定义一个计数器,用于记录安装步骤数:
private int step = 1;
同时,定义一个枚举类型,以指定安装的方向是“上一步”,还是“下一步”:
public enum OpDirection {Previous=0,Next}
为了调用更加简单,在执行安装的方法基础上,重又定义了一系列的安装方法,并接收OpDirection对象,根据其值以判断执行的方法:
private void Step1(OpDirection direction)
{
 switch (direction)
 {
  case OpDirection.Previous:
   PreviousStep1();
   break;
  case OpDirection.Next:
   NextStep1();
   break;
 }
}
现在,我们就可以直接在Button的Click事件中调用相关的方法了,例如:
private void btnNext_Click(object sender, System.EventArgs e)
{
  switch (step)
  {
   case 1: 
    Step1(OpDirection.Next);     
    break;
   case 2:
    Step2(OpDirection.Next);    
    break;
   case 3:
    Step3(OpDirection.Next);     
    break;
   case 4:
    Step4(OpDirection.Next);      
    break;
   case 5:
    Step5(OpDirection.Next);      
    break;
   case 6:
    Step6(OpDirection.Next);
    break;
   case 7:
    Step7(OpDirection.Next);
    break;
  } 
}
这样的设计不可谓不简单,然而也不可谓不拙劣了。实际上,这样的设计实乃面向过程设计之遗毒,而完全丢失了面向对象设计的精神实质。粗略列举一下,至少存在以下缺陷:
(1)层次混乱,职责不明,众多实现代码都集中在一个主窗体类中,导致一个类过于庞大;
(2)逻辑虽然简单,然而代码却极为繁杂,且不利于代码的复用;
(3)极度僵化,尤其不利于可能的扩展;
(4)思路混乱,结构模糊,不利于代码之阅读,从而影响其他人对代码可能会有的修改。
总之,这样的设计仿若一个拙劣的工匠胡乱敲打出来的一件半成品,非但谈不上设计之美,竟然连最基本的设计要素也不具备。勉强可说,够用而已。
那么,应该如何设计才能克服以上的缺陷呢?其实,从上述的设计中我们可以找到一些改善设计的端倪。最重要的设计原则,还是对象的职责划分。例如我们思考一下执行安装的方法,它们属于主窗体对象的职责范围吗?答案显然是否定的。既然这些方法不属于主窗体对象的职责,为什么我们还要将其定义在主窗体类中呢?
那么,这些方法究竟属于哪一个对象?是每一个安装步骤所定义的UserControl对象吗?似乎是,然而又不尽然。一个UserControl对象,它的职责应该是什么?虽然是为各个安装步骤定义的类,然而它的职责主要还在于安装界面的相关逻辑,例如相关信息在界面上的显示,进度条的进度显示,还包括Button响应Click事件所可能执行的方法。而执行安装,严格说来,却与这些对象无关。
既然找不到安装方法所属的对象,为什么不为其专门定义相关的对象呢?思路到了这里,就有了豁然开朗的喜悦了。

三、 引入Command模式

如果说一个典型的对象应该包括属性和行为,那么仅仅包含行为的对象,毋庸置疑最佳的定义类型就是接口了。在上一节的设计方案中,定义了一系列安装方法,虽然步骤不同,执行的逻辑也不相同,然而由于具有相同的方法签名,因此完全可以统一为一个接口,例如ISetupCommand:
public interface ISetupCommand
{
     void ExecuteSetup();
}
既然是多个安装步骤都具有该Setup()方法,自然就可以定义相关的类,并使其实现该接口:
public class Step1SetupCommand:ISetupCommand
{
     public void ExecuteSetup()
     {
     //实现略;
     }
}
如此一来,修改后的设计其类图如图19-2所示:
 comm02.gif
图19-2 ISetupCommand类型的类图
熟悉设计模式的读者应该可以看出,上图所示就是一个标准的Command模式实现。确实如此,我们将安装方法看作是一个用户的请求,或者说是命令。由于该命令逻辑对于系统而言是变化的,因此抽象该命令逻辑,使其与其他调用者之间的耦合度松散,是解决这类问题的最佳方案。
注意:如果比较类图结构,我们会发现Command模式、Strategy模式和State模式是完全一样的。事实正是如此,由于它们的设计思想都是对易于变化的部分进行抽象,或为接口,或为抽象类。唯一的区别,就是所抽象的行为职责不同而已,这一点从各自的名字就可以看出。本例中,由于安装方法更近似于用户的请求或命令,所以称其为Command模式更加恰当。
引入Command模式确乎使我们的程序结构更加合理了,然而,我们使用设计模式,并不是要生搬硬套,而应该遵循其设计的基本原则。以本例而言,实则我们没有必要定义诸如Step1SetupCommand的类,来实现ISetupCommand接口。我们只需要修改原来为各个安装步骤定义的UserControl类,令其实现ISetupCommand接口即可。
如此说来,安装行为仍然属于UserControl的职责吗?这岂不是与前面的分析自相矛盾?其实不然,两者之间有着迥然的区别。如果只是将安装方法简单地放到UserControl对象中,则该职责是与具体的UserControl类型相绑定的,例如Step1BodyUC类对象,它们的关系是一种强依赖关系。由于UserControl类类型是.Net Framework已经定义好的,虽然各个具体的UserControl对象有一个共同的基类UserControl,然而安装方法却没有被共同抽象出来,也即是说,各个UserControl对象的安装方法是各自为政的,因此如下的代码就是错误的:
UserControl uc = new Step1BodyUC();
uc.ExecuteSetup();
除非将对象uc进行显示转换为Step1BodyUC类型,然而这样的处理就完全悖离了面向对象思想中的多态性。
如果是各个UserControl对象均实现ISetupCommand接口,情况就完全不同了:
public class Step1BodyUC:UserControl,ISetupCommand
{
 //实现略;
}
以下代码是合理的:
ISetupCommand uc = new Step1BodyUC();
uc.ExecuteSetup();
修改后的设计方案类图应该如图19-3所示:
 comm03.gif
图19-3 修改后的设计类图
相比最初的设计,我们仅仅新增加了一个ISetupCommand接口,同时将原来在主窗口类中的安装方法,转移到了各个UserControl对象中,作为ISetupCommand接口方法的实现。

四、 进一步完善

虽然程序结构在引入Command模式后有了很大的改观,然而现有的各个ISetupCommand对象并不能很好地被主窗体对象所调用。在为btnNext和btnPrevious按钮的Click事件实现安装行为时,安装步骤必须是顺次执行的,而如今的设计,并不能体现这样一个顺序关系。唯一的办法,是将这些ISetupCommand对象依次放入一个集合对象中,并提供Next()和Previous()方法,返回正确的对象:
 public class SetupUCChain
   {
        private List<ISetupCommand> m_list;
        private int m_step;
        private static SetupUCChain m_chain;
        private SetupUCChain()
        {
            m_list = new List<ISetupCommand>();
            m_step = 0;
            Init();
        }
        public static SetupUCChain CreateSetupUCChain()
        {
            if (m_chain == null)
            {
               return new SetupUCChain();
            }
            else
            {
                return m_chain;
            }
        }
        private void Init()
        {
            m_list.Add(new Step1BodyUC());
            m_list.Add(new Step2BodyUC());
            m_list.Add(new Step3BodyUC());
            m_list.Add(new Step4BodyUC());
            m_list.Add(new Step5BodyUC());
            m_list.Add(new Step6BodyUC());
            m_list.Add(new Step7BodyUC());
        }
        public ISetupCommand Next()
        {
            ++m_step;
            if (m_step < m_list.Count)
            {
                return (ISetupCommand)m_list[m_step];
            }
            else
            {
                throw new IndexOutOfRangeException("Setup is completed.");
            }
        }

        public ISetupCommand Previous()
        {
            --m_step;
            if (m_step >= 0)
            {
                return (ISetupCommand)m_list[m_step];
            }
            else
            {
                throw new IndexOutOfRangeException("No previous step.");
            }
        }
  }
考虑到SetupUCChain对象最多只能实例化一次,因此我在此引入了Singleton模式。
最后,由于在各自的安装方法中,需要将UserControl本身添加到主窗体Pannel控件的子控件中,我们还需要修改ISetupCommand的接口方法,从参数传入Pannel控件对象:
public interface ISetupCommand
{
     void ExecuteSetup(Pannel pannel);
}
那么在各个UserControl对象中,需要在原有的安装方法实现中添加如下的代码:
panel.Controls.Add(this);
现在,对于btnPrevious和btnNext按钮的Click事件而言,逻辑就非常简单了:
public class SetupMainForm:System.Windows.Forms.Form
{
     private System.Windows.Forms.Panel panBody;
    private SetupUCChain m_chain = SetupUCChain.CreateSetupUCChain();
    //中间代码略;
private void btnNext_Click(object sender, System.EventArgs e)
{
  try
  {
  m_chain.Next().ExecuteSetup(panBody);
  }
  catch (IndexOutOfRangeException ex)
  {
  MessageBox.Show(ex.Message);
  }
     }
private void btnPrevious_Click(object sender, System.EventArgs e)
{
  try
  {
  m_chain.Previous().ExecuteSetup(panBody);
  }
  catch (IndexOutOfRangeException ex)
  {
  MessageBox.Show(ex.Message);
  }
     }
}
通过引入设计模式,运用职责分离的原理,我们将与安装有关的逻辑剥离出主窗体类定义,使得整个结构清晰简要,职责分明,且因为对可能存在的变化进行了封装,同时也具备了可扩展性,形成了结构之间的松散耦合。这样的完善设计的整个过程虽然会耗费我们项目开发的时间,然而付出的努力并没有付诸东流,改善后的结构才是健壮的、逻辑清楚的,同样也是优雅的设计。

posted on 2006-09-18 13:24 张逸 阅读(5018) 评论(20)  编辑 收藏 所属分类: Design & Pattern

评论

#1楼  2006-09-18 14:07 雨恨云愁 [未注册用户]

虽然没有拜师
也不知道你是否会答应
在我心里
你已经是我的老师了
多谢了   回复  引用    

#2楼  2006-09-18 15:00 CrazyCoder      

Whta if we add the two properties

ISetupCommand PreviousCmd
{ get;set;}
ISetupCommand NextCmd
{ get;set;}

to the ISetupCommand interface, will it be better?   回复  引用  查看    

#3楼 [楼主] 2006-09-18 16:30 Bruce Zhang(wayfarer)      

@雨恨云愁
呵呵,不敢当,互相学习。

@CrazyCoder
这样增加两个属性,对于设计而言没有太大必要,反而增加了复杂性。   回复  引用  查看    

#4楼  2006-09-18 16:36 Michael-_-Young      

我最近有一个想法,正如本文中提到的:command模式中UserControl对象要继承自ISetupCommand接口。能不能在设计上做的更“过分”一点,另外针对该UserControl所要表现的Command逻辑额外写一个类,例如Setup1CommandLogic,让它继承自ISetupCommand,让UserControl“一心一意”去作UI上的表现?
这样一来,可能文件更多了,但是在逻辑上我觉得更清晰了。
正像vs2005的做法,窗体类Form1.cs附带了一个Form1.Designer.cs。
期待Wayfarer的解答:)   回复  引用  查看    

#5楼  2006-09-20 09:22 wyrover [未注册用户]

有点像java或php下的mvc,前端控制器(SetupUCChain)->业务控制Action(ISetupCommand)->(调用视图)void ExecuteSetup(Pannel pannel);   回复  引用    

#6楼  2006-09-22 11:17 ww[匿名] [未注册用户]

222222   回复  引用    

#7楼  2006-09-22 11:33 ww[匿名] [未注册用户]

dddddddddddddd   回复  引用    

#8楼  2006-09-26 19:01 liu zhuang      

@CrazyCoder

The property will give user ability to view the sequence as well as change the sequence. You may or may or need it depending on your purpose. It seems Bruce wants to seal all these as internal behaviors and not to expose it to client(main form), so he does not suggest to include property.
  回复  引用  查看    

#9楼 [楼主] 2006-09-27 00:13 Bruce Zhang(wayfarer)      

@Michael-_-Young
我始终强调利用模式或者利用面向对象思想,都不要过分设计。以本例来看,UserControl担负起SetupCommand的职责是完全可以的,够用就好,这是我的一个原则。

当然你说的也有一定道理,这种实现方式在我文中也有提及。如果UserControl的UI逻辑比较复杂的话,采用你所说的方式,就比较好了。

@liu zhuang
U are right.
  回复  引用  查看    

#10楼  2006-09-27 15:42 过客[匿名] [未注册用户]

文中提到的仅仅是单流程。如果向导窗体中有选项的功能,使程序逻辑能够按照多种流程执行,那就比较复杂了。不知道有好的解决方案么?   回复  引用    

#11楼  2006-11-15 13:12 小峰      

command + singleton 学习了。谢谢。   回复  引用  查看    

#12楼  2007-03-29 20:50 YanziMyWife      

StepXBodyUC()是不是等到了需要的时候来创建呢???如果步骤多的话这样效率太底了   回复  引用  查看    

#13楼  2007-05-14 10:23 bluestone [未注册用户]

I am studying it, thanks.`~~~~   回复  引用    

#14楼  2007-05-27 23:46 gaotian [未注册用户]

不愧为master,多谢!   回复  引用    

#15楼  2007-05-30 17:37 ?????????? [未注册用户]

这样的完善设计的整个过程虽然会耗费我们项目开发的时间,然而付出的努力并没有付诸东流,改善后的结构才是健壮的、逻辑清楚的,同样也是优雅的设计。

???????????


对本文作者表示反对!   回复  引用    

#16楼  2007-09-26 10:03 dreamsfeng [未注册用户]

我们实验室里没有像wayfarer这样能够对技术研究这么透彻,并能用浅显易懂的语言写出来。虚心向张老师学习,从张老师的第一篇博客文章看起,一天看三篇张老师的博客文章,计划是十月分全部看完。   回复  引用    

导航

公告

logo.gif
我的著作与译作

《软件设计精要与模式》

《WCF服务编程》

MVP_Horizontal_BlueOnly.png

From 03-03-2006
Counter: site stats

与我联系

搜索

 

常用链接

我参加的小组

我参与的团队

随笔分类(243)

随笔档案(235)

最新随笔

积分与排名

最新评论

阅读排行榜

评论排行榜