学习设计模式《二十一》——装饰模式

一、基础概念

装饰模式的本质是【动态组合】。

动态是手段,组合才是目的。这里的组合有两个含义:一个是动态功能的组合(即动态进行装饰器的组合);另外一个是指对象组合(通过对象组合来实现为被装饰对象透明地增加功能)。需要注意:装饰模式不仅可以增加功能,而且也可以控制功能的访问,完全实现新的功能,还可以控制装饰的功能是在被装饰功能之前还是之后来运行等。总之,装饰模式是通过把复杂功能简单化、分散化,然后在运行期间,根据需要来动态组合的这样一个模式。

**装饰模式的定义:**动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更为灵活。

|--------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 序号 | 认识装饰模式 | 说明 |
| 1 | 装饰模式的功能 | 装饰模式能够实现动态地为对象添加功能,是从一个对象外部来给对象增加功能,相当于是改变了对象的外观。 当装饰过后,从外部使用系统的角度看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。这样就能够灵活地改变一个对象的功能,只要动态组合的装饰器发生了改变,那么最终所得到的对象功能也就发生了改变。变相地还得到了另外一个好处,就是装饰器功能的复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。 |
| 2 | 对象组合 | 一个类功能的扩展方式【可以是继承】【也可以是功能更强大、更灵活的对象组合方式】。现在在面向对象的设计中,有一条基本规则就是【尽量使用对象组合】,而不是对象继承来扩展和复用功能。 |
| 3 | 装饰器 | 装饰器实现了对被装饰对象的某些装饰功能,可以在装饰器中调用被装饰对象的功能,获取相应的值,这其实是一种递归调用。在装饰器中不仅仅是可以给被装饰对象增加功能,还可以根据需要选择是否调用被装饰对象的功能,如果不调用被装饰对象的功能,那就变成完成重新实现,相当于动态修改了被装饰对象的功能。 各个装饰器中间最好是完全独立的功能,不要有依赖,这样在进行装饰组合的时候,才没有先后顺序的限制(即:先装饰谁和后装饰谁都应该是一样的)否则会极大降低装饰器组合的灵活性。 |
| 4 | 装饰器和组件类的关系 | 装饰器是用来装饰组件的,装饰器一定要实现和组件类一致的接口,保证它们是同一个类型,并具有同一个外观,这样组合完成的装饰才能够递归调用下去。 组件类是不知道装饰器的存在的,装饰器为组件添加功能是一种透明的包装,组件类毫不知情。需要改变的是外部使用组件类的地方,现在需要使用包装后的类,接口是一样的,但是具体的实现类发生了改变。 |
| 5 | 退化形式 | 如果仅仅只是想要添加一个功能,就没有必要再设计装饰器的抽象类了,直接在装饰器中实现跟组件一样的接口,然后实现相应的装饰功能就可以了。但是建议最好还是设计上装饰器的抽象类,这样有利于程序的扩展。 |
[认识装饰模式]

|--------|----------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| 序号 | 装饰模式的优点 | 装饰模式的缺点 |
| 1 | 比继承更灵活 从为对象添加功能的角度来看,装饰模式比继承更加灵活。继承是静态的,而且一旦继承所有子类都有一样的功能。而装饰模式采用把功能分离到每个装饰器当中,然后通过对象组合的方式,在运行时动态地组合功能,每个被装饰的对象最终有哪些功能,是由运行期动态组合的功能来决定的。 | 会产生很多细粒度对象 (即装饰模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样就会产生很多细粒度对象,且功能越复杂,需要的细粒度对象越多)。 |
| 2 | 更容易复用功能 装饰模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,使实现装饰器变得简单,更重要的是这样有利于装饰器功能的复用,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,从而实现复用装饰器的功能。 | 会产生很多细粒度对象 (即装饰模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样就会产生很多细粒度对象,且功能越复杂,需要的细粒度对象越多)。 |
| 3 | 简化高层定义 装饰模式可以通过组合装饰器的方式,为对象增添任意多的功能。因此在进行高层定义的时候,不用把所有的功能都定义出来,而是定义最基本的就可以了, 可以在需要使用的时候,组合相应的装饰器来完成所需的功能。 | 会产生很多细粒度对象 (即装饰模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样就会产生很多细粒度对象,且功能越复杂,需要的细粒度对象越多)。 |
[装饰模式的优缺点]

何时选用装饰模式?

《1》如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式,这计划就是装饰模式的主要功能。

《2》如果不适合使用子类来进程扩展的时候,可以使用装饰模式(因为装饰模式是使用的"对象组合"方式,所谓不适合用子类扩展的方式:如扩展功能需要的子类太多,造成子类数目呈爆炸性增长)。

二、装饰模式示例

业务需求:如何实现灵活的奖金计算。奖金计是相对复杂的功能,尤其是业务部门的奖金计算方式,非常复杂,除了业务功能复杂外,还有一个麻烦之处就是计算方式会经常变动,因为业务部门需要通过调整奖金的计算方式来激励士气。从业务的角度来看一下奖金计算方式的复杂性:

《1》奖金分类:对于个人分为个人当月业务奖金、个人累计奖金、个人业务增长奖金、及时回款奖金、限时成交加码奖金等;对业务主管或者业务经理,除了个人奖金外,还有团队累计奖金、团队业务增长奖金、团队盈利奖金等;

《2》计算奖金的金额:又分为销售额、销售毛利、实际回款、业务成本、奖金基数等。

《3》奖金的计算公式:针对不同的人、不同的奖金类别、不同的计算奖金的金额,计算公式也是不同的,即使是同一个公式,里面计算的比例参数也有可能不同。

我们这里简化一下奖金的计算体系:

《1》每个人当月的业务奖金=当月销售额x3%;

《2》每个人累计奖金=总的回款额x0.1%;

《3》团队奖金=团队总销售额x1%;

一个人的奖金分为多个部分,要实现奖金计算,主要是按照各个奖金计算的规则,把这个人可以获取的每部分奖金计算出来,然后计算一个总和,这就是这个人可以得到的奖金。

2.1、不使用模式的示例

2.1.1、准备测试数据

为了演示的简易性,我们这里准备测试数据在内存中模拟数据库。

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.NoPattern
{
    /// <summary>
    /// 在内存中模拟数据库,准备好测试数据,用于计算奖金
    /// </summary>
    internal class TempDB
    {
        //记录每个人的月度销售额
        public static Dictionary<string,double> monthSaleMoneyDic= new Dictionary<string,double>();

        /// <summary>
        /// 填充数据
        /// </summary>
        public static void FillDatas()
        {
            AddInfoToDic(ref monthSaleMoneyDic,"张三",10000.00);
            AddInfoToDic(ref monthSaleMoneyDic,"李四",20000.00);
            AddInfoToDic(ref monthSaleMoneyDic,"王五",30000.00);
        }

        /// <summary>
        /// 添加数据到字典中
        /// </summary>
        /// <typeparam name="T1">数据类型1</typeparam>
        /// <typeparam name="T2">数据类型2</typeparam>
        /// <param name="dic">字典容器</param>
        /// <param name="key">键</param>
        /// <param name="value">值</param>
        private static void AddInfoToDic<T1,T2>(ref Dictionary<T1,T2> dic,T1 key,T2 value)
        {
            if (dic == null || key==null ||"".Equals(key)) return;

            if (dic.ContainsKey(key))
            {
                dic[key] = value;
            }
            else
            {
                dic.Add(key, value);
            }
        }

    }//Class_end
}

2.1.2、实现奖金计算规则

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.NoPattern
{
    /// <summary>
    /// 奖金规则
    /// </summary>
    internal class PrizeRule
    {
        public PrizeRule()
        {
            //先填充数据
            TempDB.FillDatas();
        }


        /// <summary>
        /// 计算某人在某段时间内的奖金(有些参数在这里的演示中不会使用,但是在实际业务中会用到;
        /// 为表示这个具体的业务方法,因此这些参数被保留了)
        /// </summary>
        /// <param name="user">人员名称</param>
        /// <param name="start">开始时间</param>
        /// <param name="end">结束时间</param>
        /// <returns>返回某人在某段时间内的奖金</returns>
        public double CaculatePrize(string user,DateTime start,DateTime end)
        {
            double prize = 0.00;
            if (string.IsNullOrEmpty(user) || start>end)
            {
                return prize;
            }

            //计算当月业务奖金(所有人)
            prize = MonthPrize(user,start,end);

            //计算累计奖金
            prize += SumPrize(user,start,end);

            //需要判断该人员是普通人员还是业务经理(团队奖金只有业务经理才有)
            if (IsManager(user))
            {
                prize += GroupPrize(user,start,end);
            }

            return prize;
        }

        //计算某人的当月业务奖金
        private double MonthPrize(string user,DateTime start,DateTime end)
        {
            double prize = 0.00;

            //根据人员获取当月的业务额度,然后乘以3%
            prize = TempDB.monthSaleMoneyDic[user]*0.03;
            Console.WriteLine($"【{user}】当月的业务奖金是【{prize}】");

            return prize;
        }

        //计算某人的累计奖金
        private double SumPrize(string user, DateTime start, DateTime end)
        {
            //计算累计奖金(正常来说我们应该安装人员获取累计业务额度,然后再乘以0.1%)
            //我们这里只做演示(就都假定每个人员的业务额度都是一百万1000000)
            double prize = 0.00;
            prize = 1000000 * 0.001;
            Console.WriteLine($"【{user}】的累计奖金是【{prize}】");
            return prize;
        }

        //判断人员是普通人员还是业务经理
        private bool IsManager(string user)
        {
            //实际业务应该从数据库中获取人员对应的职务进行判定
            //我们这里为了掩饰方便,就直接指定王五为经理,其余都是普通员工
            if ("王五".Equals(user))
            {
                return true;
            }
            return false;
        }

        //计算当月团队业务将
        private double GroupPrize(string user,DateTime start,DateTime end)
        {
            //计算当月团队的业务奖金(先算出团队业务总额,然后在乘以1%)
            double groupPize = 0.00;
            foreach (var item in TempDB.monthSaleMoneyDic.Values)
            {
                groupPize += item;
            }
            groupPize = groupPize * 0.01;
            Console.WriteLine($"【{user}】所属团队当月的业务奖金是【{groupPize}】");
            return groupPize;
        }

    }//Class_end
}

2.1.3、客户端测试

cs 复制代码
namespace DecoratorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            NoPattern();

            Console.ReadLine();
        }

        /// <summary>
        /// 不使用模式的测试
        /// </summary>
        private static void NoPattern()
        {
            Console.WriteLine("------不使用模式的测试------");

            //创建奖金规则对象
            NoPattern.PrizeRule prizeRule = new NoPattern.PrizeRule();

            DateTime start = Convert.ToDateTime("2025-07-01");
            DateTime end = Convert.ToDateTime("2025-07-31");

            double prize_zs = prizeRule.CaculatePrize("张三",start,end);
            Console.WriteLine($"======张三应得的奖金是【{prize_zs}】\n");
            double prize_ls = prizeRule.CaculatePrize("李四",start,end);
            Console.WriteLine($"======李四应得的奖金是【{prize_ls}】\n");
            double prize_ww = prizeRule.CaculatePrize("王五",start,end);
            Console.WriteLine($"======王五经理应得的奖金是【{prize_ww}】\n");
        }

    }//Class_end
}

2.1.4、运行结果

这是示例是已经实现了我们现有的功能要求,只是计算的方式麻烦一些,每个规则都要实现。现在的问题是:这个奖金的计算方式经常发生变动,几乎是每个季度都会有小调整,每年都有大调整,这就要求软件的实现要足够灵活,要能够很快进行相应的调整和修改,否则就不能满足实际的业务需要。比如:现在需要增加一个"环比增长奖金"(即本月的销售额比上个月有增加,且达到一定的比例,就可以获取奖金,增长的比例越高,奖金比例也越大);又经过几个月,业务的奖励策略发生变化,不在需要这个奖金或者修改为另一种奖金方式,这就需要对软件进行调整,实现新的功能,这需要怎么实现呢?

可通过继承的方式来扩展功能;或者是计算奖金的对象里面添加或者删除新的功能,并在计算奖金的时候,调用新的功能或是不调用某些去掉的功能,这种方案会严重违反开闭原则。

还有一个问题是,在运行期间,不同任意参与的奖金计算凡事也是不同的(如若是主管或业务经理,除了参与个人计算部分外,还需要参加团队奖金计算,这就意味着需要再运行期间动态地来组合需要计算的部分,会出现一堆的if-else)。

总结一下:奖金计算面临如下问题:

《1》计算逻辑复杂;

《2》需要有足够的灵活性(可以方便地增加或减少功能);

《3》需要动态地组合计算方式,不同的人参与的计算不同。

也就是说;假如我们设有一个计算奖金的对象,现在需要能够灵活地给它增加或减少功能,还需要能够动态地组合功能,每个功能相当于在计算奖金的某个部分。

2.2、使用装饰模式示例1

需要解决给一个对象增加功能,且实现功能的动态组合;还需要不能让对象知道(即:不能修改这个对象)通常我们会想到用继承,但是若还需要减少或修改功能呢?继承就不行了。那就只能使用【对象组合了】。在装饰模式的实现中,为了能够实现和原来使用被装饰对象的代码无缝结合,是通过定一个抽象类,让这个类实现与被装饰对象相同的接口,然后在具体的实现类中,转调被装饰的对象,在调转的前后添加新的功能,这就实现了给被装饰对象增加功能,这个思路与【对象组合】非常相似。在转调的时候,如果觉得被装饰对象的功能不再需要,还可以直接替换掉,不用在转调,而是在装饰对象中完成新的全新实现。

2.2.1、定义奖金规则接口和一个基本实现

实现一个抽象类定义奖金计算的方法:

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    /// <summary>
    /// 奖金规则
    /// </summary>
    abstract internal class PrizeRule
    {
        /// <summary>
        /// 计算某人在某段时间内的奖金(有些参数在这里的演示中不会使用,但是在实际业务中会用到;
        /// 为表示这个具体的业务方法,因此这些参数被保留了)
        /// </summary>
        /// <param name="user">人员名称</param>
        /// <param name="start">开始时间</param>
        /// <param name="end">结束时间</param>
        /// <returns>返回某人在某段时间内的奖金</returns>
        public abstract double CaculatePrize(string user, DateTime start, DateTime end);

    }//Class_end
}

实现抽象类的定义奖金方法的具体实现

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    /// <summary>
    /// 具体实现奖金的类(也是被装饰器装饰的对象)
    /// </summary>
    internal class PrizeRuleOne : PrizeRule
    {
        public override double CaculatePrize(string user, DateTime start, DateTime end)
        {
            //默认实现(没有任何奖金)
            return 0.00;
        }

    }//Class_end
}

在内存模拟数据充当数据库

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    /// <summary>
    /// 在内存中模拟数据库,准备好测试数据,用于计算奖金
    /// </summary>
    internal class TempDB
    {
        //记录每个人的月度销售额
        public static Dictionary<string, double> monthSaleMoneyDic = new Dictionary<string, double>();

        /// <summary>
        /// 填充数据
        /// </summary>
        public static void FillDatas()
        {
            AddInfoToDic(ref monthSaleMoneyDic, "张三", 10000.00);
            AddInfoToDic(ref monthSaleMoneyDic, "李四", 20000.00);
            AddInfoToDic(ref monthSaleMoneyDic, "王五", 30000.00);
        }

        /// <summary>
        /// 添加数据到字典中
        /// </summary>
        /// <typeparam name="T1">数据类型1</typeparam>
        /// <typeparam name="T2">数据类型2</typeparam>
        /// <param name="dic">字典容器</param>
        /// <param name="key">键</param>
        /// <param name="value">值</param>
        private static void AddInfoToDic<T1, T2>(ref Dictionary<T1, T2> dic, T1 key, T2 value)
        {
            if (dic == null || key == null || "".Equals(key)) return;

            if (dic.ContainsKey(key))
            {
                dic[key] = value;
            }
            else
            {
                dic.Add(key, value);
            }
        }

    }//Class_end
}

2.2.2、定义抽象的装饰器

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    internal class Decorator : PrizeRule
    {
        //被装饰的对象
        protected PrizeRule prizeRule;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="prizeRule">被装饰的对象</param>
        public Decorator(PrizeRule prizeRule)
        {
            this.prizeRule = prizeRule;
        }

        public override double CaculatePrize(string user, DateTime start, DateTime end)
        {
            //转调被装饰对象的方法
            return prizeRule.CaculatePrize(user,start,end);
        }

    }//Class_end
}

2.2.3、定义一系列的装饰器对象

在这里实现多个具体的装饰器对象来实现具体的奖金计算规则,每个具体的装饰器对象只实现一个规则:

《1》实现计算奖金的规则装饰器

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    /// <summary>
    /// 计算当月业务奖金(装饰器对象)
    /// </summary>
    internal class MonthPrizeDecorator : Decorator
    {
        public MonthPrizeDecorator(PrizeRule prizeRule) : base(prizeRule)
        {
        }

        public override double CaculatePrize(string user, DateTime start, DateTime end)
        {
            //1-获取前面运算出来的奖金
            double money = base.CaculatePrize(user,start,end);

            //2-计算当月业务奖金(按人员和时间去获取当月业务额度,然后乘以3%)
            double prize = TempDB.monthSaleMoneyDic[user] * 0.03;
            Console.WriteLine($"【{user}】当月业务奖金是【{prize}】");
            return money+prize;
        }

    }//Class_end
}

《2》实现计算累计奖金计算的装饰器

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    /// <summary>
    /// 计算累计奖金(装饰器对象)
    /// </summary>
    internal class SumPrizeDecorator : Decorator
    {
        public SumPrizeDecorator(PrizeRule prizeRule) : base(prizeRule)
        {
        }

        public override double CaculatePrize(string user, DateTime start, DateTime end)
        {
            //1-先获取前面运算出来的奖金
            double money = base.CaculatePrize(user, start, end);

            //2-计算累计奖金,实际情况应该按照人员去获取其对应的累计业务额度,然后再乘以0.1%
            //我们这里为了演示简单,就假定大家的累计业务额都是一百万1000000元
            double prize = 1000000 * 0.001;
            Console.WriteLine($"【{user}】的累计奖金是【{prize}】");
            return money+prize;
        }

    }//Class_end
}

《3》实现计算当月团队业务奖金的装饰器

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoOne
{
    /// <summary>
    /// 计算当月团队业务奖金(装饰器对象)
    /// </summary>
    internal class GroupPrizeDecorator : Decorator
    {
        public GroupPrizeDecorator(PrizeRule prizeRule) : base(prizeRule)
        {
        }

        public override double CaculatePrize(string user, DateTime start, DateTime end)
        {
            //1-先获取前面运算出来的奖金
            double money = base.CaculatePrize(user, start, end);

            //2-计算当月团队业务奖金,先计算出团队总的业务额度,然后再乘以1%
            //假设都是一个团队的
            double group = 0.00;
            foreach (var item in TempDB.monthSaleMoneyDic.Values)
            {
                group += item;
            }

            group = group * 0.01;
            Console.WriteLine($"【{user}】所在团队当月业务奖金是【{group}】");
            return money+group;
        }

    }//Class_end
}

2.2.4、客户端测试

cs 复制代码
namespace DecoratorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TestDecoratorDemoOne();

            Console.ReadLine();
        }

        /// <summary>
        /// 装饰器模式示例1测试
        /// </summary>
        private static void TestDecoratorDemoOne()
        {
            Console.WriteLine("------装饰器模式示例1测试------");
            //先获取数据库内容
            DecoratorDemoOne.TempDB.FillDatas();

            //1-创建计算基本奖金的类(这也是被装饰的对象)
            DecoratorDemoOne.PrizeRuleOne prizeRuleOne=new DecoratorDemoOne.PrizeRuleOne();

            //2-计算基本奖金装饰(这里需要组合各个装饰:各个装饰者之间最好是不要有先后顺序的限制)

            //组合普通业务人员的奖金计算
            DecoratorDemoOne.Decorator d1 = new DecoratorDemoOne.MonthPrizeDecorator(prizeRuleOne);
            DecoratorDemoOne.Decorator d2=new DecoratorDemoOne.SumPrizeDecorator(d1);

            //注意:这里只需使用最后组合好的对象调用业务方法即可,会依次调用回去
            DateTime start = Convert.ToDateTime("2025-08-01");
            DateTime end = Convert.ToDateTime("2025-08-31");

            double prize_zs = d2.CaculatePrize("张三",start,end);
            Console.WriteLine($"======张三应得奖金【{prize_zs}】\n");
            double prize_ls = d2.CaculatePrize("李四",start,end);
            Console.WriteLine($"======李四应得奖金【{prize_ls}】\n");

            //若是业务经理,还需要计算团队奖金
            DecoratorDemoOne.Decorator d3 =new DecoratorDemoOne.GroupPrizeDecorator(d2);
            double prize_ww = d3.CaculatePrize("王五",start,end);
            Console.WriteLine($"======王五经理应得奖金【{prize_ww}】\n");
        }

    }//Class_end
}

2.2.5、运行结果

在这里运行的时候按照装饰器的组合顺序,依次调用相应的装饰器来执行业务功能,相当于递归调用方法,如下是关于业务经理王五的奖金调用示例图:

如图所示:对于基本的计算奖金的对象来说,由于计算奖金的逻辑很复杂,且需要在不同的情况下运行不同的运算,为了灵活性,把多种计算奖金的方式分散到不同的装饰器对象中,采用动态组合的方式,来给基本的计算奖金的对象添加计算奖金的功能,每个装饰器相当于计算奖金的一个部分。

这种方式明显比不使用模式的计算奖金的对象增加子类更灵活,因为装饰模式的起源点是采用对象组合的方式,然后在组合的时候顺便增加些功能,达到了一层层组合的效果,装饰模式还要求装饰器要实现与被装饰对象相同的业务接口,这样才能以同一种方式依次组合下去。灵活性还体现在动态上,若是继承的方式,那么所有类的实例都有这个功能了;但是采用装饰模式,可以动态地为某几个对象实例添加功能,而不是对整个类添加功能(如:我们这个装饰模式的示例1中,客户端在对张三、李四只是组合了两个功能,对王五就组合了三个功能,但原始的计算奖金类都是一样的,只是动态地为它增加的功能不同而已)。

2.2.6、对象组合

一个类的功能扩展方式可以是【继承】,【也可以是功能更强大、更灵活的对象组合方式】。什么是对象组合?(比如你有一个对象A,实现了一个A1方法,而B对象想要扩展A的功能,给它增加一个B2的方法)

《1》那么一个方案就是继承,如下所示:

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.ObjectCompositon
{
    internal class A
    {
        public void A1()
        {
            Console.WriteLine($"我是{this.GetType().Name}对象/A1()");
        }

    }//Class_end
}

B对象继承A对象后,可以直接调用A对象的方法,还可以自行扩展B2方法:

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.ObjectCompositon
{
    /// <summary>
    /// 【使用继承方式】实现对A对象A1方法的扩展
    /// </summary>
    internal class B:A
    {
        public void B1()
        {
            Console.WriteLine($"我是继承自A的B对象");
            base.A1();
            Console.WriteLine($"{this.GetType().Name}/B1()");
        }

        public void B2()
        {
            Console.WriteLine($"{this.GetType().Name}/这是扩展的功能B2");
        }

    }//Class_end
}

《2》另一个方案就是对象组合(即:在B11对象中不继承A对象,而是直接使用A对象的实例,根据需要调用A对象实例的方法)如下所示:

cs 复制代码
/***
*	Title:"设计模式" 项目
*		主题:组合对象
*	Description:
*	    组合对象的优势
*	        《1》可以有选择的复用功能(即持有其他对象实例的功能可以根据需要使用需要到的,不使用的不调用即可);
*	        《2》在调用持有的其他对象实例功能前后,可以实现一些功能处理(比如:该类持有A对象实例,该类对于A对象是透明的(即:A对象并不知道它自己的A1方法在被调用前后被追加了什么功能))。
*	        《3》可以组合拥有多个对象的功能(比如:在这个类中除了持有A对象的实例还可以持有C对象的实例,可以分别组合调用A对象实例与B对象实例的方法)
*	    
*	    何时创建被组合对象的实例:
*	        《1》方式一:在属性上直接定义并创建需要组合对象的实例;
*	        《2》方式二:在属性上定义一个变量,来表示持有被组合对象的实例,具体的实例从外部传入,也可以通过IOC/DI容器来注入。
*	        	    
*	Date:2025
*	Version:0.1版本
*	Author:Coffee
*	Modify Recoder:
 ***/


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.ObjectCompositon
{
    /// <summary>
    /// 【使用对象组合方式】实现对A对象A1方法的扩展(即:直接创建A对象的实例进行操作)
    /// </summary>
    internal class B11
    {
        //持有A对象实例【这就是属于在属性上创建需要组合的对象】
        A a = new A();
        //持有C对象实例【这就是属于在属性上创建需要组合的对象】
        C c = new C();

        //通过外部传入需要组合的对象
        private D d = null;
        public void SetD(D d)
        {
            this.d = d;
        }

        public void B1()
        {
            Console.WriteLine($"我是B11对象");
            //转调A对象的功能
            a.A1();
            Console.WriteLine($"{this.GetType().Name}/B1()");
            Console.WriteLine("\n");
        }

        public void B2()
        {
            Console.WriteLine($"我是B11对象");
            Console.WriteLine($"{this.GetType().Name}/这是扩展的功能B2");
            //转调C对象的功能
            c.C1();
            Console.WriteLine("\n");
        }

        public void B3()
        {
            Console.WriteLine($"我是B11对象");
            //转调D对象的功能
            d.D1();
            Console.WriteLine("\n");
        }

    }//Class_end
}

2.3、使用装饰模式示例2

装饰模式和AOP在思想上有共同之处:

《1》什么是AOP(面向方面编程)?

AOP是一种编程范式,提供从另一个角度来考虑程序结构以完善面向对象编程(OOP)。在面向对象开发中,考虑系统的角度通常是纵向的(即:从上到下,上层依赖下层【如:MVC架构】),如下图所示:

但是,一些系统架构中,有越来越多的人发现,各个模块之间存在着一些共性的功能(如:日志管理、安全检查、事务管理等)如下图所示:

这个时候,在思考这些共性功能的时候,是从横向来思考问题的,与通常面向对象的纵向思考角度不同,此时就需要有新的方案来解决就是AOP。

AOP为开发者提供了一种描述横切关注点的机制,并能够自动将横切关注点织入到面向对象的软件系统中,从而实现横切关注点的模块化。AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(如:日志管理、权限控制、事务管理等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并利于未来的可操作性和可维护性。

AOP之所以强大,是因为它能够自动把横切关注点的功能模块,自动织入软件系统中(即:在常规的面向对象系统中,对这种共性的功能如何处理?一般都是把这些功能提炼出来,然后在需要用到的地方进行调用【即应用主动去调用公共模块,也就是说应用模块需要很清楚公共模块的功能以及具体的调用方法才行,应用模块是依赖于公共模块的,是耦合的,这样一来,要想修改公共模块就会很困难,牵一发而动全身】如下图所示:)。

AOP针对这些共性的功能是如何处理的呢?(AOP是将各个公共模块主动织入到应用系统中,这样一来应用系统就不需要知道公共功能模块,也就是应用系统和公共模块解耦了)公共模块会在合适的时候,由外部织入到应用系统中 ,至于谁来实现这样的模块,以及如何实现不在我们此次的讨论范围中,我们主要是关注AOP的实现思路。如果按照装饰模式对于AOP的这个思路,业务功能对象就可以看作是被装饰的对象,而各个功能模块可以看作是装饰器(可以透明地给业务功能对象增加功能)。从某个侧面来说:装饰模式和AOP要实现的功能是类似的,只不过AOP的实现方法不同,会更加灵活,更加可以配置,另外AOP的一个更重要变化是思想上【即:主从换位】让原本主动调用的功能模块变成了被动等待,甚至在毫不知情的情况下被织入了很多新功能

如下就是使用装饰模式做出类似AOP效果的商品销售示例:

2.3.1、定义接口

这里定义接口而不是抽象类的原因是(抽象类是需要为子类提供公共功能,若不需要为子类提供公共功能可以直接实现为接口):

《1》先定义数据对象模型

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoTwo
{
    /// <summary>
    /// 销售单数据模型
    /// </summary>
    internal class SaleModel
    {
        //销售商品
        public string? Goods { get; set; }
        //销售数量
        public int SaleNumber { get; set; }

        public override string ToString()
        {
            string str = $"商品名称是【{Goods}】购买的数量是【{SaleNumber}】";
            return str ;
        }

    }//Class_end
}

《2》定义接口

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoTwo
{
    /// <summary>
    /// 商品销售管理的业务接口
    /// </summary>
    internal interface IGoodsSaleEbi
    {
        /// <summary>
        /// 保存销售信息(这里为了演示就简化了【本来销售数据是多条的】)
        /// </summary>
        /// <param name="user">操作人员</param>
        /// <param name="customer">客户</param>
        /// <param name="saleModel">销售数据</param>
        /// <returns>是否成功 true表示成功</returns>
        bool Sale(string user,string customer,SaleModel saleModel);

    }//Interface_end
}

2.3.2、定义基本的业务实现对象

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoTwo
{
    /// <summary>
    /// 具体的业务实现
    /// </summary>
    internal class GoodsSaleEbo : IGoodsSaleEbi
    {
        public bool Sale(string user, string customer, SaleModel saleModel)
        {
            Console.WriteLine($"【{user}】保存了【{customer}】购买【{saleModel}】的销售数据");
            return true;
        }
    }//Class_end
}

2.3.3、实现公共功能并将其作为装饰器

如下实现的就是公共功能,并把这些公共功能实现为装饰器,因此需要给它们定义一个抽象的父类:

《1》实现被装饰对象的抽象父类

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoTwo
{
    /// <summary>
    /// 需要和被装饰的对象实现同样的接口(装饰器接口)
    /// </summary>
    abstract internal class Decorator1 : IGoodsSaleEbi
    {
        //被装饰的对象
        protected IGoodsSaleEbi ebi;

        /// <summary>
        /// 构造方法
        /// </summary>
        /// <param name="ebi">被装饰的对象</param>
        public Decorator1(IGoodsSaleEbi ebi)
        {
            this.ebi = ebi;
        }

        public virtual bool Sale(string user, string customer, SaleModel saleModel)
        {
            return ebi.Sale(user, customer, saleModel);
        }
    }//Class_end
}

《2》实现权限控制装饰器

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoTwo
{
    /// <summary>
    /// 实现权限控制的装饰器
    /// </summary>
    internal class CheckDecorator : Decorator1
    {
        public CheckDecorator(IGoodsSaleEbi ebi) : base(ebi)
        {
        }

        public override bool Sale(string user, string customer, SaleModel saleModel)
        {
            //这里演示就简单点,只让张三执行这个功能
            if ("张三".Equals(user))
            {
                return base.Sale(user, customer, saleModel);
            }
            else
            {
                Console.WriteLine($"对不起【{user}】你没有保存销售单的权限!");
                return false;
            }
        }

    }//Class_end
}

《3》实现日志记录的装饰器

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern.DecoratorDemoTwo
{
    /// <summary>
    /// 实现日志记录的装饰器
    /// </summary>
    internal class LogDecorator : Decorator1
    {
        public LogDecorator(IGoodsSaleEbi ebi) : base(ebi)
        {
        }

        public override bool Sale(string user, string customer, SaleModel saleModel)
        {
            //执行业务功能
            bool res = base.Sale(user, customer, saleModel);
            //执行业务功能后记录日志
            string curTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            Console.WriteLine($"日志记录【{user}】于【{curTime}】保存了一条销售记录," +
                $"客户是【{customer}】购买记录是【{saleModel}】");
            return res;
        }

    }//Class_end
}

2.3.4、客户端组合使用装饰器

在组合这些装饰器的时候,权限控制应该是最新被执行的,所以把它组合在最外面,日志记录的装饰器会先调用原始的业务对象,所以把日志记录的装饰器组合在中间(装饰器直接最好不用有顺序限制,但是在实际的应用中,可以根据具体的功能要求有顺序的限制,但是应该尽量避免这种情况):

cs 复制代码
namespace DecoratorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TestDecoratorDemoTwo();

            Console.ReadLine();
        }

        /// <summary>
        /// 装饰器模式示例2测试
        /// </summary>
        private static void TestDecoratorDemoTwo()
        {
            Console.WriteLine("------装饰器模式示例2测试------");

            //得到业务接口,组合装饰器
            DecoratorDemoTwo.IGoodsSaleEbi ebi = new DecoratorDemoTwo.CheckDecorator(
                new DecoratorDemoTwo.LogDecorator(new DecoratorDemoTwo.GoodsSaleEbo()));

            //准备测试
            DecoratorDemoTwo.SaleModel saleModel= new DecoratorDemoTwo.SaleModel();
            saleModel.Goods = "华为P80Pro";
            saleModel.SaleNumber = 6;

            //调用业务功能
            ebi.Sale("张三","周茜",saleModel);
            ebi.Sale("李四","李思",saleModel);

        }

    }//Class_end
}

2.3.5、运行结果

我们实现完成后,看一仔细看一下,就是在没有惊动原始业务的情况下,给它织入了新的功能(即:原始业务在不知情的情况下,给原始业务对象透明地增加了新功能,从而模拟实现了AOP的功能)。我们完全可以将这种做法应用在实际的项目开发中,这样在后期为项目的业务对象添加数据检查、权限控制、日志记录等功能,就不在需要去原有业务对象做处理了,业务对象可以更加专注于具体业务处理。

三、项目源码工程

kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern