设计模式之组合模式(十四)

一、引出模式

在软件开发中,我们经常会遇到树型目录的功能,比如:管理商品的目录 

如果让你来实现这个功能,你会怎么做呢?

我们先来分析分析:商品类别树上的节点有三类,根节点、树枝节点和叶子节点,在进一步根节点和树枝节点都是可以包含其他节点的,我们就叫它容器节点。这样,商品类别树就分为了容器节点和叶子节点,我们将它们分别实现成为对象。

代码示例:

class Program
    {
        static void Main(string[] args)
        {
            //定义所有的组合对象
            Composite root = new Composite("服装");
            Composite c1 = new Composite("男装");
            Composite c2 = new Composite("女装");
            //定义所有的叶子对象
            Leaf leaf1 = new Leaf("衬衣");
            Leaf leaf2 = new Leaf("夹克");
            Leaf leaf3 = new Leaf("裙子");
            Leaf leaf4 = new Leaf("套装");
            //按照树的结构来组合组合对象和叶子对象
            root.AddComposite(c1);
            root.AddComposite(c2);
            
            c1.AddLeaf(leaf1);
            c1.AddLeaf(leaf2);

            c2.AddLeaf(leaf3);
            c2.AddLeaf(leaf4);

            //调用根对象的输出功能来输出整棵树
            root.PrintStruct("");

            Console.ReadKey();
        }
    }

    /// <summary>
    /// 叶子对象
    /// </summary>
    public class Leaf
    {
        /// <summary>
        /// 叶子对象的名字
        /// </summary>
        private string name = null;

        /// <summary>
        /// 构造方法,传入叶子对象的名字
        /// </summary>
        /// <param name="name">叶子对象的名字</param>
        public Leaf(string name)
        {
            this.name = name;
        }

        /// <summary>
        /// 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
        /// </summary>
        /// <param name="preStr">前缀,主要是按照层级拼接的空格,实现向后缩进</param>
        public void PrintStruct(string preStr)
        {
            Console.WriteLine(preStr + "-" + name);
        }
    }

    /// <summary>
    /// 组合对象,可以包含其它组合对象或者叶子对象
    /// </summary>
    public class Composite
    {
        /// <summary>
        /// 用来记录包含的其它组合对象
        /// </summary>
        private List<Composite> childComposite = new List<Composite>();

        /// <summary>
        /// 用来记录包含的其它叶子对象
        /// </summary>
        private List<Leaf> childLeaf = new List<Leaf>();

        /// <summary>
        /// 组合对象的名字
        /// </summary>
        private string name = null;

        /// <summary>
        /// 构造方法,传入组合对象的名字
        /// </summary>
        /// <param name="name"></param>
        public Composite(string name)
        {
            this.name = name;
        }

        /// <summary>
        /// 向组合对象加入被它包含的其它组合对象
        /// </summary>
        /// <param name="c"></param>
        public void AddComposite(Composite c)
        {
            this.childComposite.Add(c);
        }

        /// <summary>
        /// 向组合对象加入被它包含的叶子对象
        /// </summary>
        /// <param name="leaf"></param>
        public void AddLeaf(Leaf leaf)
        {
            this.childLeaf.Add(leaf);
        }

        /// <summary>
        /// 输出组合对象自身的结构
        /// </summary>
        /// <param name="preStr"></param>
        public void PrintStruct(String preStr)
        {
            //先把自己输出去
            Console.WriteLine(preStr + "+" + this.name);
            //然后添加一个空格,表示向后缩进一个空格,输出自己包含的叶子对象
            preStr += " ";
            foreach (Leaf leaf in childLeaf)
            {
                leaf.PrintStruct(preStr);
            }

            //输出当前对象的子对象了
            foreach (Composite c in childComposite)
            {
                ////递归输出每个子对象
                c.PrintStruct(preStr);
            }
        }
    }

功能上已经实现好了,但有何问题呢?

区分了组合对象和叶子对象,并进行有区别的对待,比如在CompositeClient里面,都需要区别对待这两种对象,这就是个问题。

对于这种具有整体与部分关系,并能组合成树型结构的对象结构,如何才能够以一个统一的方式来进行操作呢?

二、认识模式

1.模式定义

将对象组合成为属性结构以表示“整体-部分”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

2.解决思路

上述例子中,要区分组合对象和叶子对象,就是因为没有把组合对象和叶子对象统一起来。

组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来,用户使用时,始终是在操作组件对象,而不用再区分是在操作组合对象还是叶子对象。

3.模式图示

 

Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。

Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其它的子节点对象。

Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。

Client:客户端,通过组件接口来操作组合结构里面的组件对象。

4.模式原型示例代码

class Program
    {
        static void Main(string[] args)
        {
            //定义多个Composite对象
            Component root = new Composite();
            Component c1 = new Composite();
            Component c2 = new Composite();
            //定义多个叶子对象
            Component leaf1 = new Leaf();
            Component leaf2 = new Leaf();
            Component leaf3 = new Leaf();

            //组和成为树形的对象结构
            root.AddChild(c1);
            root.AddChild(c2);
            root.AddChild(leaf1);

            c1.AddChild(leaf2);
            c2.AddChild(leaf3);

            //操作Component对象
            Component o = root.GetChildren(1);
            Console.WriteLine(o);
        }

        /// <summary>
        /// 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
        /// </summary>
        public abstract class Component
        {
            /// <summary>
            /// 示意方法,子组件对象可能有的功能方法
            /// </summary>
            public abstract void SomeOperation();

            /// <summary>
            /// 向组合对象中加入组件对象 
            /// </summary>
            /// <param name="component"></param>
            public virtual void AddChild(Component component)
            {
            }

            /// <summary>
            /// 从组合对象中移出某个组件对象
            /// </summary>
            /// <param name="component"></param>
            public virtual void RemoveChild(Component component)
            {

            }

            /// <summary>
            /// 返回某个索引对应的组件对象
            /// </summary>
            /// <param name="index"></param>
            /// <returns></returns>
            public virtual Component GetChildren(int index)
            {
               
            }
        }

        /// <summary>
        /// 叶子对象,叶子对象不再包含其它子对象
        /// </summary>
        public class Leaf : Component
        {
            /// <summary>
            /// 示意方法,叶子对象可能有自己的功能方法
            /// </summary>
            public override void SomeOperation()
            {
                // do something
            }
        }

        /// <summary>
        /// 组合对象,通常需要存储子对象,定义有子部件的部件行为,
        /// 并实现在Component里面定义的与子部件有关的操作
        /// </summary>
        public class Composite : Component
        {
            /// <summary>
            /// 用来存储组合对象中包含的子组件对象
            /// </summary>
            private List<Component> childComponents = null;

            /// <summary>
            /// 示意方法,通常在里面需要实现递归的调用
            /// </summary>
            public override void SomeOperation()
            {
                if (childComponents != null)
                {
                    foreach (Component c in childComponents)
                    {
                        //递归的进行子组件相应方法的调用
                        c.SomeOperation();
                    }
                }
            }

            public override void AddChild(Component component)
            {
                //延迟初始化
                if (childComponents == null)
                {
                    childComponents = new List<Component>();
                }
                childComponents.Add(component);
            }

            public override void RemoveChild(Component component)
            {
                if (childComponents != null)
                {
                    childComponents.Remove(component);
                }
            }

            public override Component GetChildren(int index)
            {
                if (childComponents != null)
                {
                    if (index >= 0 && index < childComponents.Count)
                    {
                        return childComponents[index];
                    }
                }
                return null;
            }
        }
    }

 

5.商品分类目录实例代码

class Program
    {
        static void Main(string[] args)
        {
            //定义所有的组合对象
            Component root = new Composite("服装");
            Component c1 = new Composite("男装");
            Component c2 = new Composite("女装");
            //定义所有的叶子对象
            Component leaf1 = new Leaf("衬衣");
            Component leaf2 = new Leaf("夹克");
            Component leaf3 = new Leaf("裙子");
            Component leaf4 = new Leaf("套装");
            //按照树的结构来组合组合对象和叶子对象
            root.AddChild(c1);
            root.AddChild(c2);


            c1.AddChild(leaf1);
            c1.AddChild(leaf2);

            c2.AddChild(leaf3);
            c2.AddChild(leaf4);

            //调用根对象的输出功能来输出整棵树
            root.PrintStruct("");

            Console.ReadKey();
        }

        public abstract class Component
        {
            public abstract void PrintStruct(string preStr);

            public virtual void AddChild(Component component)
            {
            }

            public virtual void RemoveChild(Component component)
            {
            }

            public virtual Component GetChildren(int index)
            {
                return null;
            }
        }

        public class Leaf : Component
        {

            private string name = null;

            public Leaf(string name)
            {
                this.name = name;
            }

            public override void PrintStruct(string preStr)
            {
                Console.WriteLine(preStr + "-" + name);
            }
        }

        public class Composite : Component
        {
            private string name = null;
            private List<Component> childComponents = null;

            public Composite(string name)
            {
                this.name = name;
            }

            public override void PrintStruct(string preStr)
            {
                Console.WriteLine(preStr + "+" + name);
                if (childComponents != null)
                {
                   preStr += "  ";
                    foreach (var c in childComponents)
                    { 
                        c.PrintStruct(preStr);
                    }
                }
            }

            public override void AddChild(Component component)
            {
                if (childComponents == null)
                {
                    childComponents = new List<Component>();
                }
                childComponents.Add(component);

            }

            public override void RemoveChild(Component component)
            {
                if (childComponents == null)
                {
                    childComponents = new List<Component>();
                }
                childComponents.Remove(component);
            }

            public override Component GetChildren(int index)
            {
                if (childComponents != null)
                {
                    if (index > 0 && index < childComponents.Count)
                    {
                        return childComponents[index];
                    }
                }
                return null;
            }
        }
    }

 

三、理解模式

1.组合模式的目的

组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作

实现这个目标的关键之处,是设计一个抽象的组件类,让它可以代表组合对象和叶子对象。

2.对象树

组合模式会组合出树型结构,组成这个树型结构所使用的多个组件对象,就自然形成的对象树。

所有可以使用对象树来描述或操作的功能,都可以考虑组合模式。比如读取XML,或对语句进行语法解析等。

3.组合模式中的递归

组合模式中的递归,是对象本身的递归,是对象的组合方式,从设计上来讲是递归关联,是对象关联关系的一种。

4.安全性和透明性

在组合模式中,把组件对象分为两种:一种是可以包含子组件Composite对象;另一种不能包含其他组件对象的叶子对象。

Composite对象就像是一个容器,可以包含其他的Composite对或叶子对象。有了容器,就要对容器进行维护和管理。

这就产生了这样一个问题:在组合模式的类层次结构中,到底哪一些类里面定义这些管理子组件的操作,是应该在Component中声明这些操作呢,还是在Composite中声明这些操作?

这就需要仔细思考,在不同的实现中,进行安全性和透明性的权衡选择。

这里所说的安全性是指:从客户使用组合模式上看是否更安全。如果是安全的,那么不会有发生误操作的可能,能访问的方法都是被支持的功能。

这里所说的透明性是指:从客户使用组合模式上,是否需要区分到底是组合对象还是叶子对象。如果是透明的,那就是不再区分,对于客户而言,都是组件对象,具体的类型对于客户而言是透明的,是客户无需要关心的。

透明性的实现

如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无需关心具体的组件类型,这种实现方式就是透明性的实现。事实上,前面示例的实现方式都是这种实现方式。

但是透明性的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的,比如:增加、删除子组件对象。而客户不知道这些区别,对客户是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的。

组合模式的透明性实现,通常的方式是:在Component中声明管理子组件的操作,并在Component中为这些方法提供缺省的实现,如果是有子对象不支持的功能,缺省的实现可以是抛出一个例外,来表示不支持这个功能。

安全性的实现

如果把管理子组件的操作定义在Composite中,那么客户在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了,因为压根就没有这样的功能,这种实现方式是安全的。

但是这样一来,客户端在使用的时候,就必须区分到底使用的是Composite对象,还是叶子对象,不同对象的功能是不一样的。也就是说,这种实现方式,对客户而言就不是透明的了。

5.组合模式的优缺点

定义了包含基本对象和组合对象的类层次结构
    在组合模式中,基本对象可以被组合成更复杂的组合对象,而组合对象又可以组合成更复杂的组合对象,可以不断地递归组合下去,从而构成一个统一的组合对象的类层次结构

统一了组合对象和叶子对象
    在组合模式中,可以把叶子对象当作特殊的组合对象看待,为它们定义统一的父类,从而把组合对象和叶子对象的行为统一起来

简化了客户端调用
    组合模式通过统一组合对象和叶子对象,使得客户端在使用它们的时候,就不需要再去区分它们,客户不关心使用的到底是什么类型的对象,这就大大简化了客户端的使用

更容易扩展
    由于客户端是统一的面对Component来操作,因此,新定义的CompositeLeaf子类能够很容易的与已有的结构一起工作,而客户端不需要为增添了新的组件类而改变

很难限制组合中的组件类型
    容易增加新的组件也会带来一些问题,比如很难限制组合中的组件类型。这在需要检测组件类型的时候,使得我们不能依靠编译期的类型约束来完成,必须在运行期间动态检测。

6.何时选用组合模式

建议在如下情况中,选用组合模式:

如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也简单

如果你希望统一的使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能

7.组合模式的本质

  组合模式的本质:统一叶子对象和组合对象。

  组合模式通过把叶子对象当成特殊的组合对象看待,从而对叶子对象和组合对象一视同仁,统统当成了Component对象,有机的统一了叶子对象和组合对象。

  正是因为统一了叶子对象和组合对象,在将对象构建成树形结构的时候,才不需要做区分,反正是组件对象里面包含其它的组件对象,如此递归下去;也才使得对于树形结构的操作变得简单,不管对象类型,统一操作。

posted @ 2013-12-20 20:00  烧点饭  阅读(795)  评论(0编辑  收藏  举报