笑说设计模式-小白逃课被点名

简介

工厂模式(Factory Pattern)是最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,而是通过使用一个共同的接口来指向新创建的对象。

分类

工厂模式可以分为三种,其中简单工厂一般不被认为是一种设计模式,可以将其看成是工厂方法的一种特殊。

  • 简单工厂
  • 工厂方法
  • 抽象工厂

场景分析

平凡枯燥的文字总是让人看得想睡觉,接下来我们用几个情景案例来进行分析

简单工厂

直接通过一个Factory【工厂类】类创建多个实体类的构造方式。

时间:2021年2月19日 地点:教室 人物:学生小白、老师、大佬黑皮


小白是一名大二的计算机系学生,懒惰不爱学习。今天早晨第一节课就因为睡懒觉迟到被老师逮个正着,这节课还正好是小白最头疼的上机课”C#设计模式”。这不,课堂上到一半老师就开始提问,小白“光荣”的成为了老师的点名对象。

老师笑着说道:“小白,请你解答一下屏幕上的问题。”

题目:请使用c#、java、python、php或其他任一面向对象编程语言实现输入俩个合法数字和一个合法符号,输出对应结果的功能。

小白一看,这算什么题目,这么简单,看我不手到擒来,伴随着双手噼里啪啦一顿敲击声音,屏幕上出现一串编码。

**Calculator操作类 **

    public class Calculator
    {
        public double GetResult(double A, double B, string operate)
        {
            double result = 0d;
            switch (operate)
            {
                case "+": 
                    result = A + B;
                    break;
                case "-":
                    result = A - B;
                    break;
                case "*":
                    result = A * B;
                    break;
                case "/":
                    result = A / B;
                    break;
                default: break;
            }
            return result;
        }
    }

客户端代码

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("请输入数字A");
            string a = Console.ReadLine();
            Console.WriteLine("请输入数字B");
            string b = Console.ReadLine();
            Console.WriteLine("请选择操作符号(+、-、*、/)");
            string operate = Console.ReadLine();

            Calculator box = new Calculator();
            double result = box.GetResult(Convert.ToDouble(a), Convert.ToDouble(b), operate);
            Console.WriteLine(result);
            
            Console.ReadKey();
        }
    }

小白:”老师,我写好了“

老师:”不错,写的很好。使用到了面向对象三大特性中的封装,将计算方法封装成了一个计算类,多个客户端可以复用这个类。但是这其中也有不少的问题,哪位同学来回答一下。”

黑皮:”小白同学写的代码有俩处问题。

1、如果输入A=10,B=0,程序就会报错,没有做输入的有效性验证

2、如果操作符号不按照规定的输入 ,也会导致报错“

老师:”黑皮同学回答的非常好,但是这都只是针对代码业务逻辑错误的描述。有没有谁可以看出更加深层次的内容。“

黑皮:”老师,你给点提示吧。“

老师:”这个可以会有点隐蔽,老师就给出一点提示。如果我们增加一个新的运算怎么办?“

小白立即回答到:”在switch中增加一个新的分支就可以了“。

老师:”这样当然是没有错误的,但是问题在于,我只是增加了一个新的运算符号,却需要让加减乘除所有的运算都参加编译,如果在修改的过程中不小心修改了其他的代码,例如把+号改成了-号,那不是很糟糕,这就违背了开闭原则【对扩展开放,对修改关闭】“

小白挠一挠头问道:”开闭原则,这是什么?“

老师:”这就是你不认真听课落下的内容,回去好好补习。黑皮同学,不知道你Get到了老师的点没有?“

黑皮:”我知道应该如何修改了。小白实现了面向对象三大特性之一的封装,其实就是将其他俩个特性一起使用就可以完成老师要的功能“

小白:”多态和继承?“

黑皮:”是的,等我改完你再看程序就应该有感觉了“

    public class Operate
    {
        public double NumberA { get; set; }
        public double NumberB { get; set; }

        public virtual double GetResult()
        {
            return 0;
        }
    }
    
    public class OperateAdd : Operate
    {
        public override double GetResult()
        {
            return this.NumberA +this.NumberB;
        }
    }
    
    public class OperateSub : Operate
    {
        public override double GetResult()
        {
            return this.NumberA - this.NumberB;
        }
    }

简单工厂

    public class OperateFactory
    {
        public static Operate GetOperateFactory(string operate)
        {
            Operate fac = null;
            switch (operate)
            {
                case "+":
                    fac = new OperateAdd();
                    break;
                case "-":
                    fac = new OperateSub();
                    break;
                case "*":
                    fac = new OperateMul();
                    break;
                case "/":
                    fac = new OperateDiv();
                    break;
                default:
                    break;
            }
            return fac;
        }
    }

黑皮:”首先是一个运算类,里面有俩个Number属性和一个虚方法GetResult()。加减乘除四个方法作为运算类的子类继承,继承后重写GetResult()方法,调用基类的A和B公有属性进行不同的数学运算。“

黑皮:”然后定义一个简单工厂,静态方法传入操作符参数得到实际的业务处理类,客户端得到处理类后对参数赋值。最后一步你应该知道怎么做了吧“

小白:”我懂了,那客户端这么调用就可以了“。

  static void Main(string[] args)
        {
            Console.WriteLine("请选择操作符号(+、-、*、/)");
            string operateStr = Console.ReadLine();
            Operate operate = OperateFactory.GetOperateFactory(operateStr);
            operate.NumberA = 10;
            operate.NumberB = 4;
            double result = operate.GetResult();
            Console.WriteLine(result);
            Console.ReadKey();
        }

老师:”看来俩位同学都已经掌握了简单工厂的使用,接下来我提问几个问题,便于大家更快的掌握这种设计模式“

老师:”如果我们要修改除方法的逻辑,增加被除数为0的逻辑应该怎么做“

小白:”直接修改OperateDiv类,这不会对其他造成影响“

老师:”如果我们要新增一个开根运算应该怎么做“

小白:”添加一个新的类,Operate开根类,里面描述开根的逻辑。在工厂方法中将新的操作符添加进去即可。新增的操作单独一个类,也不会对其他方法体造成影响“。

小白:”那客户端需要做什么改变吗?“

老师:”客户端要做什么改变,客户端只要处理好自己的事情就可以了!“

是的,客户端不关心工厂创建了什么,工厂是一个黑盒子。客户端只要传入参数,工厂负责将内容生产后【实例化类的过程】给客户端即可。

优/缺点

简单工厂模式的工厂类一般是使用静态方法,通过接收的参数不同来返回不同的对象实例。不修改代码的话,是无法扩展的
优点:客户端可以免除直接创建产品对象的责任,而仅仅是“消费”产品。简单工厂模式通过这种做法实现了对责任的分割
缺点:由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了

工厂方法

时间:2021年2月19日下午 地点:教室 人物:学生小白、老师、大佬黑皮

老师:”我们紧接着上午的设计模式继续,上午我们讲的是简单工厂,下午我们讲下一个内容工厂方法。工厂方法和简单工厂其实大同小异,唯一的区别就在于每一个实现抽象类的实例(也叫做产品,即上午定义的加减乘除四个子类)都有一个对应的工厂去创建。同学们了解一下老师说话的内容,然后寻找一个场景编码实现一下。最快完成的有课堂奖励”

….几分钟过去了…..

小白:“老师,我完成了。”

老师:“好的,那我们请小白同学说明一下场景和实现的方式。”

我设计的是以水果作为场景的模式。

1、定义一个抽象类Fruit.cs,这个类定义俩个抽象方法printfColor()printfName()

2、实现俩种不同的水果分别继承此抽象类并复写抽象方法。

3、定义一个工厂接口,定义接口方法createFruit()

4、实现俩个不同的工厂分别实现水果实例的创建。

水果抽象类

    public abstract class Fruit
    {
        public abstract void PrintfColor();
        public abstract void PrintfName();
    }
    
  public class Apple : Fruit
    {
        public override void PrintfColor()
        {
            Console.WriteLine("红色");
        }

        public override void PrintfName()
        {
            Console.WriteLine("苹果");
        }
    }

工厂接口

   public interface IFruitFactory
    {
        Fruit CreateFruit();
    }

    public class AppleFactory : IFruitFactory
    {
        public Fruit CreateFruit()
        {
            return new Apple();
        }
    }

客户端实现

           //苹果工厂
            IFruitFactory appleFac = new AppleFactory();
            Fruit apple = appleFac.CreateFruit();
            apple.PrintfColor();
            apple.PrintfName
            
            //橘子工厂
            IFruitFactory orangeFac = new OrangeFactory();
            Fruit orage = orangeFac.CreateFruit();
            orage.PrintfColor();
            orage.PrintfName();

老师:“看来小白同学已经对上午的内容有了一个充分的了解,果然好好上课才能够学习到新的知识。逃课是没有益处的”

老师:“只是这样的案例太过简单,可能其他同学不是很能理解为什么要这样 ,我来举一个实际场景的案例方便大家理解。在实际的工作过程中我们总会用到日志组件,例如Nlog,Log4net这种第三方组件,这种组件都支持可配置化的多源输出。当我们在配置文件(json/xml)中增加一个“输出到控制台的参数”,程序 就会将内容输出到控制台,当配置一个输入到文件的参数,程序就会将内容输出到指定的文件。这种场景的实现其实就是一种典型的工厂方法。下面我来分析一下过程”

1、读取配置文件(json/xml)

2、获取所有的配置方式,循环遍历

3、判断配置类型,如果是输入到文件的配置。new一个文件日志工厂,将配置信息作为参数传递,便于后期方法调用;如果是输入到控制台的配置。new一个日志工厂也是做同样的操作

4、每一个工厂只管理自己的事情,但是应该都拥有日志输出这个接口。

5、当上层调用打印方法时候,循环遍历所有的工厂,调用接口的日志输出输出方法

优/缺点

工厂方法是针对每一种产品提供一个工厂类。通过不同的工厂实例来创建不同的产品实例。在同一等级结构中,支持增加任意产品
优点:允许系统在不修改具体工厂角色的情况下引进新产品
缺点:由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量

抽象工厂

抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。

时间:2021年2月20日上午 地点:教室 人物:学生小白、老师、黑皮

新的一天又开始了,“设计模式”课程在小白的眼中好像没有那么复杂了,今天小白早早地就到了教室,准备迎接老师新的鞭策。

老师:”同学们早上好,今天我们继续昨日的课程。昨天讲的是工厂方法,今天我们在此基础上做一点改进,看看又有什么新的变化。小白同学学习热情很高嘛,现在都知道坐在第一排了。不错不错,值得鼓励”

小白:”嘻嘻“

老师:“好,那开始今天的课程。今天要讲的模式是抽象工厂模式。通过和工厂模式做比较,同学们可以比较清晰的发现这俩都之间的区别。我们用昨天小白同学的例子继续开拓。”

此时有苹果和橘子俩个产品,分别对应苹果工厂和橘子工厂。这是工厂方法的体现。可是如果有3个不同的工厂,他们分别都生产苹果和橘子呢。

小白:“恩…那就多建立几个工厂。每个产品分别对应不同的工厂,应该是这样的一个结构,每一个工厂分别对应生产产品的类”

A

  • A_苹果工厂.cs
  • A_橘子工厂.cs

B

  • B_苹果工厂.cs
  • B_橘子工厂.cs

C

  • C_苹果工厂.cs
  • C_橘子工厂.cs

老师:“这样的方式当然是可以的,可以如果我有10个工厂呢,难道我们要建立10*2=20个类吗,这样程序的复杂度就是直线上升,不利于维护。”

小白:“那怎么办呢,用老师你说的那种抽象工厂吗?如果用,又应该怎么做呢”

老师:“是的,在这样的场景下,抽象工厂是最能匹配的设计模式。其实做法非常简单,对昨天的代码进行一些修改即可”

水果抽象类

新增一个Name属性,方便后期打印不同的工厂。

    public abstract class Fruit
    {
        public string Name { get; set; }
        public abstract void PrintfColor();
        public abstract void PrintfName();
    }
    
    public class Apple : Fruit
    {
        public Apple(string name)
        {
            this.Name = name;
        }

        public override void PrintfColor()
        {
            Console.WriteLine(this.Name + "红色");
        }

        public override void PrintfName()
        {
            Console.WriteLine(this.Name + "苹果");
        }
    }

工厂接口

老师:“这一处的改动就比较明显。原来的接口中方法输出唯一的产品——因为之前每一个工厂只生产一件产品。现在输出俩个产品,即继承工厂接口的类必须实现生产苹果和橘子的方法。这样的好处在于,每一个工厂负责管理自己产品的实现,避免了每一个产品都需要创建一个工厂的操作。“

解释:

工厂生产苹果和橘子。当有多个工厂的时候,每一个工厂都实现生产苹果和橘子。而不是生产A厂苹果需要一个工厂实现类,生产B厂苹果又需要一个。如下所示

旧模式

A

  • A_苹果工厂.
  • A_橘子工厂

B

  • B_苹果工厂
  • B_橘子工厂

C

  • C_苹果工厂

新模式

A 工厂

  • 苹果/橘子

B 工厂

  • 苹果/橘子

C 工厂

  • 苹果/橘子

老师:“这样复杂度由原来的6变成了3。”

小白:”我明白了,又学习到了新的东西。”

    public interface IFruitFactory
    {
        Fruit CreateApple(string name);
        Fruit CreateOrange(string name);
    }

    public class AFactory : IFruitFactory
    {
        public Fruit CreateApple(string name)
        {
            return new Apple(name);
        }

        public Fruit CreateOrange(string name)
        {
            return new Orange(name);
        }
    }

客户端实现

            IFruitFactory fac = new AFactory();
            Fruit a_Apple = fac.CreateApple("a工厂");
            Fruit a_Orange = fac.CreateOrange("a工厂");
            a_Apple.PrintfName();
            a_Orange.PrintfName();

            IFruitFactory b_fac = new BFactory();
            Fruit b_Apple = b_fac.CreateApple("b工厂");
            Fruit b_Orange = b_fac.CreateOrange("b工厂");
            b_Apple.PrintfName();
            b_Orange.PrintfName();

小白:“可是在什么样的场景下用这种模式呢,我好像一下子想不到”

老师:“抽象工厂的使用相对来说比较少,但也不是没有。我举一个例子,在后端开始中我们有各种的组件【按钮,抽屉,导航栏】等等,这些组件有对应的皮肤,对皮肤的开发就是抽象工厂的实现。工厂接口是对每个组件的定义,每个皮肤就是一个工厂的实现。如果要切换皮肤,只需要实例化不同的工厂即可。”

小白:“哦。就和游戏中的皮肤切换一样吗?”

老师:“你也可以这样理解,设计模式只是一种通用解决方案,可以应用在不同的场景下,大家可以挑最适应自己,最好理解的场景下手。”

下课铃声又响起了

老师:“好了,这节课就到这里。下节课我们讲其他的设计模式,希望大家准时听讲。”

优/缺点

抽象工厂是应对产品族概念的。应对产品族概念而生,增加新的产品线很容易,但是无法增加新的产品。比如,每个汽车公司可能要同时生产轿车、货车、客车,那么每一个工厂都要有创建轿车、货车和客车的方法
优点:向客户端提供一个接口,使得客户端在不必指定产品具体类型的情况下,创建多个产品族中的产品对象
缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性