发布新日志

  • 组合模式

    2009-09-16 09:54:51

    意图

    将对象组合成树形结构以表示“部分-整体”的层次结构。Composite模式使得用户对单个对象和组合对象的使用具有一致性。[GOF 《设计模式》]

    我们以JUnit中的TestCase和TestSuit为例来说明组合模式,在JUnit中,TestCase是最小的一个测试用例,而TestSuit称为测试包,顾名思义,它可以包含其他的TestCase或TestSuit,但是,无论是TestCase还是TestSuit,都继承自TestCaseBase这个类,而且运行他们都是调用Run这个方法,我们看一下(我这里写的可能与JUnit不一样,但思路基本一样):

        public abstract class TestCaseBase
        {
            public abstract void Run();
        }

        public abstract class TestCase : TestCaseBase
        {
            public abstract void Init();
            public abstract void CleanUp();
            public abstract void TestMethod();

            public override void Run()
            {
                Init();
                TestMethod();
                CleanUp();
            }
        }

        public class TestSuit : TestCaseBase
        {
            private List<TestCaseBase> caseList = new List<TestCaseBase>();

            public void Add(TestCaseBase testCase)
            {
                caseList.Add(testCase);
            }

            public void Remove(TestCaseBase testCase)
            {
                caseList.Remove(testCase);
            }

            public override void Run()
            {
                foreach (TestCaseBase elem in caseList)
                    elem.Run();
            }
        }

    效果及实现要点

    1Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化“一对一”的关系,使得客户代码可以一致地处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。

    2.将“客户代码与复杂的对象容器结构”解耦是Composite模式的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的复内部实现结构——发生依赖关系,从而更能“应对变化”。

    3Composite模式中,是将“AddRemove等和对象容器相关的方法”定义在“表示抽象对象的Component类”中,还是将其定义在“表示对象容器的Composite类”中,是一个关乎“透明性”和“安全性”的两难问题,需要仔细权衡。这里有可能违背面向对象的“单一职责原则”,但是对于这种特殊结构,这又是必须付出的代价。ASP.NET控件的实现在这方面为我们提供了一个很好的示范。

    4Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。

    适用性

    以下情况下适用Composite模式:

    1.你想表示对象的部分-整体层次结构

    2.你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

    总结

    组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。

  • 观察者模式

    2009-09-15 10:39:46

    意图

    定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。[GOF 《设计模式》]

    下面以自动化测试为例来说明观察者模式的含义:

    做过自动化测试的都知道一个自动化的测试case主要分三个部分:Init-在一个case开始之前做一些初始化的工作,想读取测试数据阿,打开应用程序之类的, TestMethod-测试步骤都在这个测试方法中,最后一个是CleanUp-在一个case结束后作一些清理的工作,像关闭数据库,写日志之类的,当然可能不同的人名字叫的不一样,不过大体都是分这三部的。

    首先,我们有一个TestBase类:

        public abstract class TestBase
        {
            protected abstract void Init();
            protected abstract void TestMethod();
            protected abstract void CleanUp();

            public void Run()
            {
                Init();
                TestMethod();
                CleanUp();
            }
        }

    可以看出,基类TestCase的Run()方法其实就是跑一个case的入口,这里当然还用刀了模板方法模式,不过我们可以暂时不用管它。我们只要知道,在一个自动化case运行的时候(即Run方法被调用的时候),必须经过上述的三个阶段。

    现在我们想,在case开始的时候,我们可能会写一些记录,或告诉用户case已经开始了,在case运行过程中,我们可能会收集一些数据,在case结束的时候,我们可能会将最终的结果写入XML文件,或写入一个case结果管理工具中。这些其实都是一种“通知”,即在不同的阶段通知不同的对象,即便同一个阶段,在不同的case或不同的运行时间,我们也能通知不同的对象。这就用到了观察者模式。在这个例子中,其实就是在调用Init()的时候,我们要在其中作出CaseInit的通知,在调用TestMethod的时候我们要做出CaseRun的通知,在调用CleanUp()的时候要做出CaseCleanUp的通知,因为通知其实就是要调用一个函数,所以,观察者模式其实就是要在我们发通知的地方放一个“函数的占位符”。因为有三种不同类型的通知,所以我们有下面三种类:

        public interface InitObserver
        {
            void CaseStart(EventArgs arg);
        }

        public interface TestMethodObserver
        {
            void CaseRun(EventArgs arg);
        }

        public interface CleanUpObserver
        {
            void CaseCleanUp(EventArgs arg);
        }

    上面的EventArgs类型的参数是我临时用的,可以根据需要改成其他类型的参数来更好的获得需要的数据。

    然后修改TestBase来放我们所谓的“占位符”:

        public abstract class TestBase
        {
            public List<InitObserver> initObserverList = new List<InitObserver>();
            public List<TestMethodObserver> testMethodObserverList = new List<TestMethodObserver>();
            public List<CleanUpObserver> cleanUpObserverList = new List<CleanUpObserver>();

            private void Init()
            {
                foreach (InitObserver elem in initObserverList)
                    elem.CaseStart(new EventArgs());

                TestInitialize();
            }
            private void TestStep()
            {
                TestMethod();

                foreach (TestMethodObserver elem in testMethodObserverList)
                    elem.CaseRun(new EventArgs());
            }

            private void CleanUp()
            {
                foreach (CleanUpObserver elem in cleanUpObserverList)
                    elem.CaseCleanUp(new EventArgs());

                TestCleanUp();
            }

            protected abstract void TestInitialize();
            protected abstract void TestMethod();
            protected abstract void TestCleanUp();

            public void Run()
            {
                Init();
                TestStep();
                CleanUp();
            }
        }

    这样,在case开始之前,只要初始化好initObserverList,testMethodObserverList cleanUpObserverList,在相应的阶段就会对那些对象做出相应的通知。

    在.NET的环境中,观察者模式有了更简单的做法:

    首先将 InitObserver,TestMethodObserver,public interface CleanUpObserver这三个接口去掉,

    然后定义三个代理:

        public delegate void initObserverHandler(EventArgs arg);
        public delegate void testMethodObserverHandler(EventArgs arg);
        public delegate void cleanUpObserverHandler(EventArgs arg);

    修改TestBase:

        public abstract class TestBase
        {
            initObserverHandler initObserver;
            testMethodObserverHandler testMethodObserver;
            cleanUpObserverHandler cleanUpObserver;

            private void Init()
            {
                initObserver(new EventArgs());

                TestInitialize();
            }
            private void TestStep()
            {
                TestMethod();

                testMethodObserver(new EventArgs());
            }

            private void CleanUp()
            {
                cleanUpObserver(new EventArgs());

                TestCleanUp();
            }

            protected abstract void TestInitialize();
            protected abstract void TestMethod();
            protected abstract void TestCleanUp();

            public void Run()
            {
                Init();
                TestStep();
                CleanUp();
            }
        }

  • 命令模式

    2009-09-14 10:14:14

    我们以现实中的吃羊肉串为例,解释命令模式。假如我们要在客户端实现点2串羊肉串,1串鸡柳的行为,我们可以这么做:

    首先,我们要有一个烧烤者的接口,它包含子类可以实现的方法,一个是烤羊肉,一个是烤鸡肉:

        Public interface Barbecue
        {
            void BarMutton();

            void BarChicken();
        }

    然后我们定义两个烧烤者的具体类,他们都实现了烧烤接口的两个方法:

        class ConcreteBarbecue1 : Barbecue
        {
            public void BarMutton()
            {
                Console.WriteLine("ConcreteBarbecue1 is  BarMuttoning..");
            }

            public void BarChicken()
            {
                Console.WriteLine("ConcreteBarbecue1 is BarChicken..");
            }
        }

        class ConcreteBarbecue2 : Barbecue
        {
            public void BarMutton()
            {
                Console.WriteLine("ConcreteBarbecue2 is BarMuttoning..");
            }

            public void BarChicken()
            {
                Console.WriteLine("ConcreteBarbecue2 is BarChicken..");
            }
        }

    在客户端我们可以这样写:

       //指定谁为我们烧烤
       ConcreteBarbecue1 barbecue = new ConcreteBarbecue1();
       //烧两串羊肉串
       barbecue.BarMutton();
       barbecue.BarMutton();
       //烧一个鸡柳
       barbecue.BarChicken();

    可以看出,在这种实现方式中,命令的请求者(客户端)与命令的实现者(ConcreteBarbecue1类)之间紧耦合了,如果我们想撤销某个命令,或重做某个命令,或对命令进行记录,都是非常困难的。所以,我们必须将命令的实现者与命令的请求者进行解耦合。

    我们看一下命令模式的定义:

    将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。[GOF 《设计模式》]

    所谓请求就是在我们调用的ConcreteBarbecue1类的那些方法,命令模式的关键就是将命令实现者的方法抽象成类,把函数层面的功能提升到类的层面,下面我们分别实现烤羊肉串类和烤鸡柳类:

        public abstract class BarCommand
        {
            protected Barbecue bar;

            public void Execute();
        }

        public class BarMutton:BarCommand
        {
            public BarMutton(Barbecue bar)
            {
                this.bar = bar;
            }

            public void Execute()
            {
                bar.BarMutton();
            }
        }

        public class BarChicken:BarCommand
        {
            public BarChicken(Barbecue bar)
            {
                this.bar = bar;
            }

            public void Execute()
            {
                bar.BarChicken();
            }
        }

     

    我们拿BarChicken类说明一下,在构造一个BarChicken类实例的时候,传递给他一个烧烤者类的实例,然后调用BarChicken的Execute()方法的时候,其实就是调用了烧烤者实例的BarChicken()方法。

    这就是我们所谓的将请求封装成对象。

    然后,我们需要一个侍者类,来记录我们的命令:

        public class Waiter
        {
            List<BarCommand> barCommand;

            public Waiter()
            {
                barCommand = new List<BarCommand>();
            }

            public void SetOrder(BarCommand bar)
            {
                barCommand.Add(bar);
            }

            public void Action()
            {
                foreach (BarCommand bar in barCommand)
                {
                    bar.Execute();
                }
            }
        }

    我们只在waiter类里实现了添加命令和执行命令操作,其实还可以有插销、记录等方法。

    然后客户端代码如下:

            static void Main(string[] args)
            {
                Waiter waiter = new Waiter();
                BarCommand command = new BarMutton(new ConcreteBarbecue1());
                waiter.SetOrder(command);
                waiter.SetOrder(command);
                command = new BarChicken(new ConcreteBarbecue1());
                waiter.SetOrder(command);
                waiter.Action();
            }

  • 工厂方法模式

    2009-08-28 14:50:20

    工厂方法模式是设计模式中应用最为广泛的一种。

    在面向对象的编程中,对象的创建工作非常简单,对象的创建时机却非常重要。工厂方法模式要解决的就是对象的创建时机问题。

    我们可以想象一下现实中的例子:男人跟女人。他们都有吃饭、工作、睡觉这些动作,但是这些动作的具体实现肯定因性别不同而不同,比如女人吃饭一般都比较满,而男人会快一些。现在我们要在客户端生成一个人,然后执行这个人的Eat(),Work(),Sleep()这些动作,可能是在某些情况下我们会生成一个男人,其他情况下生成一个女人。怎么做呢,我们首先想到的可能是先创建两个男人、女人类:

        public class Man
        {
            void Eat()
            {
                Console.WriteLine("Man is eating!");
            }

            void Work()
            {
                Console.WriteLine("Man is working!");
            }

            void Sleep()
            {
                Console.WriteLine("Man is sleeping!");
            }
        }
        public class Woman
        {
            void Eat()
            {
                Console.WriteLine("Woman is eating!");
            }

            void Work()
            {
                Console.WriteLine("Woman is working!");
            }

            void Sleep()
            {
                Console.WriteLine("Woman is sleeping!");
            }
        }

    然后在客户端可以这样:

            static void Main(string[] args)
            {
                Man man= new Man();
                man.Eat();
            }

    但是,如果我们现在不想构造男人了,而是要构造一个女人,就要把上面所有这种代码都改成:

            static void Main(string[] args)
            {
                Woman womman = new Woman();
                womman.Eat();
            }

    这显然是不合适的,现在我们引入工厂方法模式,这个模式涉及到的角色有:

    1. 一个抽象的产品接口,工厂创建的所有产品都实现了这个接口,在本例子中我们称之为Human接口

        public interface Human
        {
            void Eat();
            void Work();
            void Sleep();
        }

    2. 具体的产品类,在本例子中,是Man跟Woman

        public class Man : Human
        {
            void Human.Eat()
            {
                Console.WriteLine("Man is eating!");
            }

            void Human.Work()
            {
                Console.WriteLine("Man is working!");
            }

            void Human.Sleep()
            {
                Console.WriteLine("Man is sleeping!");
            }
        }
        public class Woman : Human
        {
            void Human.Eat()
            {
                Console.WriteLine("Woman is eating!");
            }

            void Human.Work()
            {
                Console.WriteLine("Woman is working!");
            }

            void Human.Sleep()
            {
                Console.WriteLine("Woman is sleeping!");
            }
        }

    3. 抽象工厂接口,它定义了所有的具体工厂必须实现的接口,在本例子中,我们可以称之为PeopleCreator

        public interface PeopleCreator
        {
            Human Create();
        }

    4. 具体的工厂,它返回的是一个具体的产品类

        public class ManCreator:PeopleCreator
        {
            Human PeopleCreator.Create()
            {
                return new Man();
            }
        }

        public class WomanCreator : PeopleCreator
        {
            Human PeopleCreator.Create()
            {
                return new Woman();
            }
        }

    然后我们客户端可以这样写:

            static void Main(string[] args)
            {
                PeopleCreator creator = new ManCreator();
                Human people = creator.Create();
                people.Work();
            }

    如果要生成女人的话,我们只要改将PeopleCreator creator = new ManCreator();改为      PeopleCreator creator = new WomanCreator();就可以了。

    到这里,有的人可能疑问,如果这样的话,我们为什么不这样写呢:

            static void Main(string[] args)
            {
                Human people = new Man();
                people.Work();
            }

    这样在改为女人的时候,也是只要把Human people = new Man();改为Human people = new Womman();就可以了啊。

    其实我是这样认为的,有工厂方法模式主要的是没有在客户端牵涉到具体产品类的创建,而是将这些具体产品类的创建放在了相应的具体工厂里了,这样有一个好处,就是如果在实例化一个具体产品类的时候(在本例中,就是在调用Human people = creator.Create()的时候),我现在要根据另外一个类的实例的情况来做一些特殊处理,比如决定是否真的返回一个Human实例还是返回NULL,或为这个Human实例初始化一些属性等。这是后如果是用工厂方法模式的话,我们就可以将 PeopleCreator改一下:

        public interface PeopleCreator
        {
            Human Create(Context context);
        }


    再将具体工厂改一下:

        public class ManCreator:PeopleCreator
        {
            Human PeopleCreator.Create(Context context)
            {
                if (context怎么样)
                {
                    Man man = new Man();
                    为man做一些处理
                    return man;
                }
            }
        }

    这就是将产品对象的创建跟客户端代码解耦合的好处。

    上一篇我们看了抽象工厂模式,这两种模式都有四个参与对象:抽象工厂,具体工厂,抽象产品,具体产品。但不同的是抽象工厂模式中,工厂生产的是一个系列的产品,就是说一个具体工厂生产这个系列产品的某种类型,另一个具体工厂生产这个系列产品的另一种类型。而工厂方法模式则只是关于一个产品的多个类型的。

  • 抽象工厂模式

    2009-08-26 15:59:43

    虚拟案例

    中国企业需要一项简单的财务计算:每月月底,财务人员要计算员工的工资。

    员工的工资 = (基本工资 + 奖金 - 个人所得税)。这是一个放之四海皆准的运算法则。

    为了简化系统,我们假设员工基本工资总是4000美金。

    中国企业奖金和个人所得税的计算规则是:

                    奖金 = 基本工资(4000) * 10%

                    个人所得税 = (基本工资 + 奖金) * 40%

     

    于是我们新建一个文件ChineseSalary.cs,里面代码如下:

    namespace DesignPattern.AbstractFactory.ChineseSalary
    {
        public class Constant
        {
            public static double BASE_SALARY = 4000;
        }

        public class ChineseTax
        {
            public double Calculate()
            {
                return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4;
            }
        }

        public class ChineseBounus
        {
            public double Calculate()
            {
                return Constant.BASE_SALARY * 0.1;
            }
        }
    }

    然后我们可以在客户端这样调用:

    static void Main(string[] args)
            {
                ChineseBounus bounus = new ChineseBounus();
                ChineseTax tax = new ChineseTax();

                double salary = Constant.BASE_SALARY + bounus.Calculate() - tax.Calculate();

                Console.WriteLine(salary);
                Console.Read();
            }

     

    现在,这个系统如果要移植到美国,美国企业的工资计算同样是: 员工的工资 = 基本工资 + 奖金 - 个人所得税。

    但是他们的奖金和个人所得税的计算规则不同于中国企业:

    美国企业奖金和个人所得税的计算规则是:

            奖金 = 基本工资 * 15 %

            个人所得税 = (基本工资 * 5% + 奖金 * 25%)  

    我们发现不论是中国企业还是美国企业,他们的业务运规则都采用同样的计算接口。 于是很自然地想到建立两个业务接口类TaxBonus,然后让AmericanTaxAmericanBonusChineseTaxChineseBonus分别实现这两个接口

    我们首先定义这两个接口类

    namespace DesignPattern.AbstractFactory
    {
        public interface Tax
        {
            double Calculate();  -  这儿如果加上public是错误的,因为接口默认都是公共的
        }

        public interface Bounus
        {
            double Calculate();
        }
    }

     

    新建另一个文件,名字叫AmericanSalary.cs,代码如下

    namespace DesignPattern.AbstractFactory.AmericanSalary
    {
        public class Constant
        {
            public static double BASE_SALARY = 4000;
        }

        public class AmericanTax:Tax
        {
            #region Tax Members

            double Tax.Calculate()
            {
                return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4;
            }

            #endregion
        }

        public class AmericanBounus:Bounus
        {
            #region Tax Members

            double Bounus.Calculate()
            {
                return Constant.BASE_SALARY * 0.15;
            }

            #endregion
        }
    }

    同理,ChineseBounus等也需要修改

    然后在美国的客户端可以这样使用:

            static void Main(string[] args)
            {
                Bounus bounus = new AmericanBounus();
                Tax tax = new AmericanTax();

                double salary = Constant.BASE_SALARY + bounus.Calculate() - tax.Calculate();

                Console.WriteLine(salary);
                Console.Read();
            }

     

     

    我们可以看到,当我们将系统从中国移植到美国的时候(换句话,我认为可以说是一系列相关的类 - Bounus类跟Tax类在功能上没有变化,但实现方式多样化的时候),我们要增加一个新的Bounus类AmericianBounus类个一个新的Tax类AmericianTax类;并且,我们要在客户端修改所有new这些类的地方。我们首先要做的就是在客户端不要直接调用AmericanBounus等类,因为这样会与他们耦合性太紧密,所以我们新建一个类,用来返回AmericanBounus等类的实例:

    class AmericanFactory
        {
            public static Bounus CreateBounus()
            {
                return new AmericanBounus();
            }

            public static Tax CreateTax()
            {
                return new AmericanTax();
            }
        }

        public static class ChineseFactory
        {
            public static Bounus CreateBounus()
            {
                return new ChineseBounus();
            }

            public static Tax CreateTax()
            {
                return new ChineseTax();
            }
        }

    这样,如果在中国使用,我们可以在客户端这样写:

    static void Main(string[] args)
            {
                Bounus bounus = new ChineseFactory().CreateBounus();
                Tax tax =new  ChineseFactory().CreateTax();

                double salary = Constant.BASE_SALARY + bounus.Calculate() - tax.Calculate();

                Console.WriteLine(salary);
                Console.Read();
            }

    如果在美国使用,只要也调用相应的工厂类就可以了。

    现在考虑,如果要增加日本的计算类呢,我们需要新增JapaneseBounus类,以及相应的工厂方法类JapaneseFactory,也就是说我们还要修改客户端中调用ChineseFactory的地方,就是说,我们虽然与Bounus和Tax类的耦合性降低了,但仍然与创建他们的工厂类有耦合性,又因为在新建一个Bounus类和Tax类的时候,我们必须为其创建一个工厂类,所以就是说在每次增加一个新的Bounus和Tax类的时候,我们也要修改客户端对相应的工厂类的调用,所以我们考虑继续增加一个抽象的工厂,让其他的工厂都继承子这个抽象工厂:

    public abstract class AbstractFactory
        {
            public AbstractFactory GetInstance()
            {
                if(..)
                {
                    return new ChineseFactory();
                }
                else if(..)
                {
                    return new AmericanFactory();
                }
                ...
            }
            public abstract Bounus CreateBounus();  -  从这里也可以看出抽象工厂模式的使用情形,就是已经知道使用
            public abstract Tax CreateTax();                 - 哪些类了,只是这些类的实现方式会多样化。或者说,这个工厂
        }                                                                           - 生产的东西是知道的,只是每样东西样式不一样。

     

    这样的好处是,我们可以通过读配置文件来让抽象工厂的GetInstance()方法来决定到底返回哪种类型的具体工厂,当然就是读写配置文件,在新增一个工厂的时候也是要修改If那地方的语句,所以,另外一种方式就是我们可以读完配置文件等之后,用反射的方式来加载新的类,而不是用If..Else。

     

    优点

    l         分离了具体的类。抽象工厂模式帮助你控制一个应用创建的对象的类,因为一个工厂封装创建产品对象的责任和过程。它将客户和类的实现分离,客户通过他们的抽象接口操纵实例,产品的类名也在具体工厂的实现中被分离,它们不出现在客户代码中。

    l         它使得易于交换产品系列。一个具体工厂类在一个应用中仅出现一次——即在它初始化的时候。这使得改变一个应用的具体工厂变得很容易。它只需改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。

    l         它有利于产品的一致性。当一个系列的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要,而抽象工厂很容易实现这一点。

    缺点

    l         难以支持新种类的产品。难以扩展抽象工厂以生产新种类的产品。这是因为抽象工厂几口确定了可以被创建的产品集合,支持新种类的产品就需要扩展该工厂接口,这将涉及抽象工厂类及其所有子类的改变。

    适用性

    在以下情况下应当考虑使用抽象工厂模式:

    l         一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有形态的工厂模式都是重要的。

    l         这个系统有多于一个的产品族,而系统只消费其中某一产品族。

    l         同属于同一个产品族的产品是在一起使用的,这一约束必须在系统的设计中体现出来。

    l         系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于实现。

    应用场景

    l         支持多种观感标准的用户界面工具箱(Kit)。

    l         游戏开发中的多风格系列场景,比如道路,房屋,管道等。

    l         ……

    总结

    总之,抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,运用抽象工厂模式的关键点在于应对“多系列对象创建”的需求变化。一句话,学会了抽象工厂模式,你将理解OOP的精华:面向接口编程。

     

Open Toolbar