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

happlyfox 等级 362 1 0

关于我

文章首发 | 我的博客 | 欢迎关注

简介

工厂模式(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();

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

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

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

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

下课铃声又响起了

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

优/缺点

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

推荐阅读

Redis工具收费后新的开源已出现

GitHub上Star最高的工程师技能图谱

中国程序员最容易发错的单词

推荐!!! Markdown图标索引网站

END

欢迎关注公众号 程序员工具集 👍👍 致力于分享优秀的开源项目、学习资源 、常用工具

回复关键词“关注礼包”,送你一份最全的程序员技能图谱。

回复关键词"wx"添加个人微信,勾搭作者,欢迎来聊^-^。

收藏
评论区

相关推荐

我是Redis,MySQL大哥被我害惨了!
本文转自 轩辕之风 ,链接如下 https://mp.weixin.qq.com/s?__bizMzIyNjMxOTY0NA&mid2247486528&idx1&sn3f7b09eb21969fdb16f5b0805ff69fed&scene21wechat_redirect 我是Redis 你好,我是Redis,一个叫Antirez的
容易被忽略的5个HTML技巧
对于所有 Web 开发人员来说,无论你选择的是哪种框架或后端语言,都需要大量使用 HTML(超文本标记语言)。 各种框架和编程语言可能会此消彼长,但 HTML 永不会过时。只是,就算 HTML 的应用如此广泛,这种语言中还是有不少多数开发人员都不了解的标签和属性。 而且,尽管市面上有各种模板引擎(例如 Pug)可用,但你仍然需要对 HTML 和 CSS
LRU算法四种实现方式介绍
LRU全称是Least Recently Used,即最近最久未使用的意思。 LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
被“词云”包围的冰冰会更好看吗?安排
(https://imghelloworld.osscnbeijing.aliyuncs.com/b299933deefc692934e8cc6141ab3894.png) 大家好,我是小五🐶 昨天「凹凸数据」发了一篇张同学投稿的文章《用Python爬取王冰冰vlog弹幕并制作词云(https://mp.weixin.qq.com/
Android 内存泄露:详解 Handler 内存泄露的原因与解决方案
前言 在Android开发中,内存泄露 十分常见 1.内存泄露的定义:本该被回收的对象不能被回收而停留在堆内存中 2.内存泄露出现的原因:当一个对象已经不再被使用时,本该被回收但却因为有另外一个正在使用的对象持有它的引用从而导致它不能被回收。这就导致了内存泄漏。 本文将详细讲解内存泄露的其中一种情况:在Handler中发生的内
python百题大冲关-Fizz Buzz 经典问题
挑战介绍 给定一个整数 num,从 1 到 num 按照下面的规则返回每个数: 如果这个数被 3 整除,返回 'Fizz'。 如果这个数被 5 整除,返回 'Buzz'。 如果这个数能同时被 3 和 5 整除,返回 'FizzBuzz'。 如果这个数既不能被 3 也不能被 5 整除,返回这个数字的字符串格式。 挑战内容
mac端口被占用问题
目录1. 查找被占用的端口2. 干掉占用的服务或应用正文本文主要介绍一下mac系统,端口被占用的一般处理方法。 1. 查找被占用的端口本次遇到的是8081端口被占用了,系统一直提示端口被占用,于是用如下命令查看: lsof i:8081这个命令感觉比docker(linux)要简要一些,如果需要查看docker端口占用的处理方法,可以点击参考链接。
笑说设计模式-小白逃课被点名
关于我 简介工厂模式(Factory Pattern)是最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,而是通过使用一个共同的接口来指向新创建的对象。 分类工厂模式可以分为三种,其中简单工厂一般不被认为是一种设计模式,可以将其看成是工厂方法的一种特殊
垃圾回收机制
GC标记算法 对象被判定为垃圾的标准:没有被其他对象引用 引用计数算法: 判断对象的引用数量: 通过判断对象的引用数量来决定对象是否可以被回收 每个对象实例都有一个引用计数器,被引用则1,完成引用则1 任何引用计数为0的对象实例可以被当做垃圾收集 优点:执行效率高,程序执行受影响较小。
简析限流算法
简析限流算法1.简介限流顾名思义是限制流量,限制流量的目的是为了保障服务稳定运行,避免服务被流量冲垮。当流量超出服务处理能力时,部分请求将会被限流组件拦截。被拦截的请求可能会被丢弃,如果是 C 端请求,那么这个请求可能会被导向指定的错误页上,而不是生硬的拒绝。这里我们丢
svn提交文件提示文件被别人锁定
笔记类,加深印象 提示被别人锁定文件时idea中操作文件中操作
Android包管理机制(五)APK是如何被解析的
Android框架层 Android包管理机制 Android框架层本文首发于微信公众号「刘望舒」 前言在本系列的前面文章中,我介绍了PackageInstaller的初始化和安装APK过程、PMS处理APK的安装和PMS的创建过程,这些文章中经常会涉及到一个类,那就是PackageParser,它用来在APK的安装过程中解析APK,那么APK是如何被解析的
面试字节我被String类的问题给问死了!
"+" 是怎么连接字符串的?(JDK1.7及以上)总结:使用 "+" 连接字符串时,实际上是使用临时创建的StringBuilder对象来辅助完成的。对于编译时常量,在编译后直接计算出字符串的值,而不会在运行时创建临时的StringBuilder对象来完成字符串连接。在循环中对String对象进行连接,应该直接使用StringBuilder代替 "+",这样
关于Vue在面试中常常被提到的几点
❝ 现在Vue几乎公司里都用,所以掌握Vue至关重要,这里我总结了几点,希望对大家有用 ❞1、Vue项目中为什么要在列表组件中写key,作用是什么?我们在业务组件中,会经常使用循环列表,当时用vfor命令时,会在后面写上:key,那么为什么建议写呢?key的作用是更新组件时判断两个节点是否相同。相同则复用,不相同就删除旧的创建新的。正是因为带唯一key时
我丢,去面试初级Java开发岗位,被问到泛型?
1、泛型的基础概念 1.1 为什么需要泛型 c List list new ArrayList();//默认类型是Object list.add("A123"); list.add("B234"); list.add("C345"); System.out.println(list);