一起学习设计模式--03.工厂方法模式

简单工厂模式虽然简单,但是存在一个很严重的问题:由于静态工厂方法是根据传入的参数不同来创建不同的产品的,所以当系统中需要引入新产品时,就需要修改工厂类的源代码,这将违背开闭原则。为了实现增加新产品而不修改原有代码,工厂方法模式应运而生。

一、日志记录器的设计

A科技公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,例如通过文件或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,A科技公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是A科技公司开发人员面临的一个难题。

开发人员对需求进行分析,发现该日志记录器有如下两个设计要点:

  1. 需要封装日志记录器的初始化过程,这些初始化工作比较复杂。比如:需要初始化其它相关的类,还有可能需要配置工作环境(如连接数据库或创建文件),导致代码较长,如果将他们都写在构造函数中,会导致构造函数庞大,不利于代码的修改和维护。

  2. 用户可能需要更换日志记录方式,在客户端代码中需要提供一种灵活的方式来选择日志记录器,尽量在不修改源代码的基础上更换或增加日志记录方式。

开发人员最开始使用简单工厂模式对日志记录器进行了设计,结构图如下:LoggerFactory 充当创建日志记录器的工厂,CreateLogger() 负责创建日志记录日,ILogger 是抽象日志记录器的接口,FileLogger 和 DatabaseLogger 是具体的日志记录器。工厂类 LoggerFactory 的代码如下:

    public class LoggerFactory{public static ILogger CreateLogger(string args){ILogger logger = null;if (args.Equals("db", StringComparison.OrdinalIgnoreCase)){//连接数据库,代码省略//创建数据库日志记录器对象logger = new DatabaseLogger();//初始化数据库日志记录器,代码省略}else if (args.Equals("file", StringComparison.OrdinalIgnoreCase)){//创建日志文件,代码省略//创建文件日志记录器队形logger = new FileLogger();//初始化文件日志记录器,代码省略}return logger;}}

虽然从上边的代码可以看出简单工厂模式实现了对象的创建和使用分离,但是仍然存在两个问题:

  1. 工厂类过于庞大,而且包含了大量的 if...else... 代码,导致维护和测试难度增大。

  2. 系统的扩展不灵活,如果要增加新的日志记录器,必须修改静态工厂方法的业务逻辑,违反了开闭原则。

工厂方法模式的动机之一就是为了解决以上两个问题。

二、工厂方法模式概述

在简单工厂模式中只提供一个工厂类,这个工厂类处于实例化产品类的中心位置,他需要知道每个产品类的创建细节,并决定在何时实例化哪一个产品类。简单工厂最大的缺点就是每当有新的产品要加入系统的时候,就必须修改工厂类,在静态工厂方法中添加新产品的业务逻辑,这就违反了开闭原则。另外,简单工厂模式中,所有产品的创建都由同一个工厂类负责,工厂类的职责过于繁重,业务逻辑较为复杂,具体产品和工厂类之间的耦合度较高,严重影响了系统的扩展性和灵活性。工厂方法模式刚好就解决了这一点。

工厂方法模式,不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。定义如下:

工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Patter),又可称作虚拟构造器模式(Virtual Constructor Pattern)多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。

工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。结构图如下:从上图可以看出,工厂方法模式包含以下4个角色:

  1. IProduct(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。

  2. ConreteProduct(具体产品):它实现了抽象产品的接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应

  3. IFactory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口

  4. ConreteFactory(具体工厂):它是抽象工厂的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。

与简单工厂模式相比,工厂方法模式的主要区别就是引入了抽象工厂角色。**抽象工厂可以是接口、抽象类或具体类。**典型代码如下:

public interface IFactory{IProduct FactoryMethod();
}

抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责。客户端针对抽象工厂编程,可以在运行时再指定具体工厂类。不同的具体工厂类可以创建不同的具体产品。

public class ConcreteFactory : IFactory{public IProduct FactoryMethod(){return new ConcreteProduct}
}

具体工厂类除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工,比如:连接数据库、创建文件等。

三、完整的解决方案

开发人员决定是使用工厂方法模式来设计日志记录器,基本结构如图:ILogger 充当抽象产品,其子类 FileLogger 、DatabaseLogger 是具体产品。ILoggerFactory 充当抽象工厂,FileLoggerFactory、DatabaseLoggerFactory 充当具体工厂。完整代码如下:

    /// <summary>/// 日志记录器接口:抽象产品/// </summary>public interface ILogger{void WriteLog();}/// <summary>/// 文件日志记录器:具体产品/// </summary>public class FileLogger : ILogger{public void WriteLog(){Console.WriteLine("文件日志记录!");}}/// <summary>/// 数据库日志记录器:具体产品/// </summary>public class DatabaseLogger : ILogger{public void WriteLog(){Console.WriteLine("数据库日志记录!");}}/// <summary>/// 日志记录器工厂接口:抽象工厂/// </summary>public interface ILoggerFactory{ILogger CreateLogger();}/// <summary>/// 文件日志记录器工厂类:具体工厂/// </summary>public class FileLoggerFactory : ILoggerFactory{public ILogger CreateLogger(){//创建文件日志记录器对象var logger = new FileLogger();//创建文件,省略代码return logger;}}/// <summary>/// 数据库日志记录器工厂类:具体工厂/// </summary>public class DatabaseLoggerFactory : ILoggerFactory{public ILogger CreateLogger(){//连接数据库,代码省略//创建数据库日志记录器对象var logger = new DatabaseLogger();//初始化数据库日志记录器,代码省略return logger;}}

客户端测试代码:

    class Program{static void Main(string[] args){var factory = new FileLoggerFactory();//可引入配置文件实现var logger = factory.CreateLogger();logger.WriteLog();}}

输出:

四、反射与配置文件

目前来说代码还存在一些问题,就是如果客户端要更换具体的日志记录器,就需要修改客户端的具体日志记录器工厂类的创建,这一点上来说违背了开闭原则。

为了让系统具有更好的灵活性和可扩展性,开发人员决定对日志记录器客户端代码进行重构,希望最终可以达到在不修改客户端任何代码的情况下更换或增加新的日志记录方式。

在客户端代码中将不再使用 new 关键字来创建工厂对象,而是将具体工厂类的类名存储在配置文件中(比如XML中),通过读取配置文件获取工厂类的类名字符串,然后再借助 .NET 的反射机制,根据类名字符串生成对象。

1.反射

有这么一句话 反射反射,程序员的快乐 ,由此可见反射在开发中有着举足轻重的地位,在很多框架中都可以看到它的身影。

反射的定义:

反射是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的创建和实例类型的判断等。

2.实现

创建配置文件 App.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><!--value为具体工厂类的完全限定名(命名空间+类名)--><add key="LoggerFactory" value="LXP.DesignPattern.FactoryMethod.v2.FileLoggerFactory"/></appSettings>
</configuration>

创建一个配置文件的帮助类 AppConfigHelper 代码如下:

    /// <summary>/// 配置文件帮助类/// </summary>public class AppConfigHelper{/// <summary>/// 获取具体日志工厂方法/// </summary>/// <returns></returns>public static object GetLoggerFactory(){try{var loggerFactoryName = ConfigurationManager.AppSettings["LoggerFactory"];var type = Type.GetType(loggerFactoryName);return type == null ? null : Activator.CreateInstance(type);}catch (Exception ex){Console.WriteLine(ex.Message);}return null;}}

客户端测试代码:

    class Program{static void Main(string[] args){//var factory = new FileLoggerFactory();//可引入配置文件实现var factory = (ILoggerFactory) AppConfigHelper.GetLoggerFactory();var logger = factory.CreateLogger();logger.WriteLog();}}

后期如果要新增一个日志记录器,就创建一个具体的日志记录器(需要实现ILogger接口),然后新增一个具体的日志记录器的工厂类,然后将该工厂类的完全限定名(命名空间+类名)替换配置文件中原有工厂类类名字符串即可。原有类库代码和客户端无需做任何修改,完全符合开闭原则。

3.补充

.NET 中反射有多种方法:1.如果要反射一个 DLL 中的类,并且程序并没有引用该 DLL(即对该程序来说,这个DLL中的类是一个未知的类型),可通过以下方法:

Assembly assembly = Assembly.LoadFile("程序集路径,不能是相对路径"); // 加载程序集(EXE 或 DLL) 
object obj = assembly.CreateInstance("类的完全限定名(即包括命名空间)"); // 创建类的实例

2.如果要反射当前项目中的类

//方法1
Assembly assembly = Assembly.GetExecutingAssembly(); // 获取当前程序集 
object obj = assembly.CreateInstance("类的完全限定名(命名空间 + 类名)"); // 创建类的实例,返回为 object 类型,需要强制类型转换//方法2
Type type = Type.GetType("类的完全限定名(命名空间 + 类名)"); 
object obj = type.Assembly.CreateInstance(type); //方法3
Type type = Type.GetType("类的完全限定名(命名空间 + 类名)"); 
object obj = Activator.CreateInstance(type); 

详情的使用这里就不展开了,大家可以自行搜索。

五、工厂方法的隐藏

有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法。此时,在工厂类中将直接调用产品类的业务方法,客户端无需调用工厂方法创建具体的产品,直接通过工厂即可使用所创建的对象中的业务方法。

这时,需要需要将原来的工厂接口改为抽象工厂类,在抽象类中添加一个方法,在该方法中创建了具体的产品,并调用产品的业务方法。具体代码如下:

    /// <summary>/// 将工厂接口改为抽象类/// </summary>public abstract class LoggerFactory{/// <summary>/// 在工厂类中直接调用日志记录器类的业务方法 WriteLog()/// </summary>public void WriteLog(){var logger = this.CreateLogger();logger.WriteLog();}public abstract ILogger CreateLogger();}

具体的工厂类需要将实现 ILoggerFactory 修改为继承抽象类 LoggerFactory 。客户端代码修改:

    class Program{static void Main(string[] args){var factory = (ILoggerFactory) AppConfigHelper.GetLoggerFactory();factory.WriteLog();//直接使用工厂对象来调用产品对象的业务方法}}

六、工厂方法模式总结

既继承了简单工厂模式的优点,又弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一。

1.主要优点

  1. 工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节。用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。

  2. 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其它的具体工厂和具体产品,只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变的非常好,完全符合开闭原则。

2.主要缺点

  1. 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中的类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。

  2. 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。

3.适用场景

  1. 客户端不知道其所需要的对象的类。在工厂方法模式中,客户端不需要知道具体的产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件中或数据库中。

  2. 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏替换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

示例代码:

https://github.com/crazyliuxp/DesignPattern.Simples.CSharp

参考资料:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304404.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php restful规范,RESTFul API规范 详细指南

RESTFul规范RESTFul是一种HTTP API接口规范&#xff0c;只要满足的RESTFul规范&#xff0c;即可称为RESTFul API。既然是接口&#xff0c;我们先来了解一下&#xff0c;他和传统的API接口有何不同吧。本文以尽量简单明了的文字来介绍、描述&#xff0c;只讲核心内容&#xff0c…

求对一组数据进行排名的算法

为什么80%的码农都做不了架构师&#xff1f;>>> 我现在有一组数据&#xff0c;比如&#xff1a;25&#xff0c;19&#xff0c;29&#xff0c;3 怎么用java获得这组数据的排名&#xff0c;获得排名的结果应该是3&#xff0c;2&#xff0c;4&#xff0c;1 如果有相等…

移动端h5唤起键盘_移动端H5界面打开后,如何自动调用软键盘

test(){// let aa this.$refs.input1.blur();// this.$nextTick((x)>{ //正确写法// // this.$refs.inputs.focus();// // console.log(x)// // this.$refs.inp[0].$refs.input.focus();// },3000)// $api.dom(input).focus();// this.$refs.Inp.focus();// aa.focus();// …

一份数学小白也能读懂的「马尔可夫链蒙特卡洛方法」入门指南

在众多经典的贝叶斯方法中&#xff0c;马尔可夫链蒙特卡洛&#xff08;MCMC&#xff09;由于包含大量数学知识&#xff0c;且计算量很大&#xff0c;而显得格外特别。本文反其道而行之&#xff0c;试图通过通俗易懂且不包含数学语言的方法&#xff0c;帮助读者对 MCMC 有一个直…

使用 xunit 编写测试代码

使用 xunit 编写测试代码Introxunit 是 .NET 里使用非常广泛的一个测试框架&#xff0c;有很多测试项目都是在使用 xunit 作为测试框架&#xff0c;不仅仅有很多开源项目在使用&#xff0c;很多微软的项目也在使用 xunit 来作为测试框架。Get Started在 xunit 中不需要标记测试…

eclipse如何写python_(怎么用eclipse写python)python eclipse 使用教程

用eclipse写了python之后怎么回去写java?eclipse只是一个工具&#xff0c;开发java和python都是插件吧&#xff0c;有个Open Perspective可以选择你的开发视角怎么用eclipse写python方法/步骤如果不会Eclipse中配置搭建Python开发环建第一个File->New->Other弹出Selecta…

DBDesigner 4 与 MySql 5 不能连接主要是驱动的原因

DBDesigner 4 与 MySql 5 不能连接主要是驱动的原因&#xff0c;到 http://crlab.com/dbx/download.html 下载最新的驱动并安装&#xff0c;在安装路径中找到dbexpmda.dll这个文件拷贝到DBDesigner的安装路径中。 然后下载一个最新版本的libMYSQL.dll&#xff0c;也拷贝到DBDes…

白话AI:看懂深度学习真的那么难吗?初中数学,就用10分钟

如果在这个人工智能的时代&#xff0c;作为一个有理想抱负的程序员&#xff0c;或者学生、爱好者&#xff0c;不懂深度学习这个超热的话题&#xff0c;似乎已经跟时代脱节了。但是&#xff0c;深度学习对数学的要求&#xff0c;包括微积分、线性代数和概率论与数理统计等&#…

mysql特性举例_MySQL事务的四大特性和隔离级别

1、事务的四大特性(ACID)#### 1.1、原子性(Atomicity)原子性是指事务包含的一系列操作要么全部成功&#xff0c;要么全部回滚&#xff0c;不存在部分成功或者部分回滚&#xff0c;是一个不可分割的操作整体。1.2、一致性(Consistency)一致性是可以理解为事务对数据完整性约束的…

IdentityServer4密码模式

Oatuth2协议的密码模式介绍用户会将用户名&#xff0c;密码给予客户端&#xff0c;但是客户端不保存此信息&#xff0c;客户端带着用户的密码请求认证服务器&#xff0c;认证服务器密码验证通过后后将token返回给客户端。 这里借用下阮一峰老师画的图&#xff08;博客地址》htt…

IKVM 编程武林之.NET派的北冥神功

为什么80%的码农都做不了架构师&#xff1f;>>> 在编程武林中&#xff0c;Java派成立较久底子雄厚&#xff0c;虽然掌门人Sun已经老态龙钟&#xff0c;镇山之技的Java语言已经被后进的新秀.NET派的C#压得喘不过气来&#xff0c;甚至有时候Sun老大还得跑到.NET派潜伏…

php 自定义菜单 openid,微信公众平台开发(99) 自定义菜单获取OpenID

关键字 微信公众平台 自定义菜单 OpenID作者&#xff1a;方倍工作室原文&#xff1a;http://www.cnblogs.com/txw1958/p/weixin-menu-get-openid.html在这篇微信公众平台开发教程中&#xff0c;我们将介绍如何在自定义菜单中获得用户的OpenID。本篇开发教程的实质是微信自定义菜…

mysql优化的重要参数 key_buffer_size table_cache

MySQL服务器端的参数有很多&#xff0c;但是对于大多数初学者来说&#xff0c;众多的参数往往使得我们不知所措&#xff0c;但是哪些参数是需要我们调整的&#xff0c;哪些对服务器的性能影响最大呢&#xff1f;对于使用Myisam存储引擎来说&#xff0c;主要有key_buffer_size和…

上海大华条码称代码_上海大华计价电子秤 使用软件 TM-H 大华条码称设置15KG

电子秤上海&#xff0c;电子秤价格&#xff0c;电子秤上海牌子好 电子秤有哪些电子秤那个牌子好&#xff0c;产品质量好大华电子秤软件下载&#xff0c;大华电子秤设置&#xff0c;大华电子秤软件郑重声明&#xff1a;本店所售大华条码秤&#xff0c;属于上海大华的正品&#x…

代码传奇 | 明明可以靠颜值 却用代码把人类送上了月球的女人——Margaret Hamilton

据说「软件工程师」这个名词就是她发明的玛格丽特站在阿波罗计算机指导手册 (AGC) 的源代码程序列表旁边&#xff0c;这些材料摞起来比她的人还要高。图片来源&#xff1a;Margaret Hamilton缔造传奇的人似乎有个共性&#xff1a;本来没想干一票大的&#xff0c;甚至她的打算都…

如何在 ASP.Net Core 中使用 NCache

虽然 ASP.Net Core 中缺少 Cache 对象&#xff0c;但它引入了三种不同的cache方式。内存缓存分布式缓存Response缓存Alachisoft 公司提供了一个开源项目 NCache&#xff0c;它是一个高性能的&#xff0c;分布式的&#xff0c;可扩展的缓存框架&#xff0c;NCache不仅比 Redis 快…

oracle 动态游标行数,oracle动态游标的简单实现方法

下面就是例子程序--明细表打印予处理 通用报表&#xff1a;procedure mx_print_common(pd_id in mx_pd_syn.pd_id%type,p_pd_mxb_id IN mx_pd_mxb_syn.p_mxb_id%type,p_dept_no IN sc_mxk.dept_code%type,p1 sc_bz_syn.bz_code%type,p2 sc_cjjc_syn.cjjc_code%type,p3 sc_mxk.…

每扇区2048字节的U盘乱码的数据恢复

每扇区2048字节的U盘乱码的数据恢复一个U盘&#xff0c;FAT32分区&#xff0c;显示的是乱码&#xff0c;远程看对方的U盘参数&#xff0c;发现一个比较怪的现象&#xff1a;每扇区字节数是2048字节&#xff08;U盘量产时可能是以光盘形式形成的&#xff09;&#xff0c;对方传的…

为什么有些大公司的效率弱爆了?

阅读本文大概需要5分钟。上周写了篇文章&#xff1a;为什么有些大公司的技术弱爆了&#xff1f;不少朋友读完后表示有同感&#xff0c;还有一些朋友在读者群探讨大公司效率问题。有几个朋友谈到自己的公司效率低下&#xff0c;做一件事需要层层审批&#xff0c;并且遇到各种阻力…

2018年最有前景的十大行业

我们想和大家分享的是围绕十个行业、数十个细分领域&#xff0c;在2018年发展趋势展望&#xff1a;01 消费新零售——平台级近半年&#xff0c;新零售已被多次提及。但在年终盘点我们再次提到这个“热词”&#xff0c;是因为该领域未来还将出现超级平台级的公司。新零售未来更多…