当前位置: 首页 > 编程日记 > 正文

技术图文:02 创建型设计模式(上)

创建型设计模式(上)

知识结构:

图1 知识结构


简单工厂模式

Sunny 软件公司欲基于 C# 语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,如:

  • 柱状图(histogram
  • 饼状图(pie
  • 折线图(line

Sunny 软件公司开发人员提出了一个初始设计方案,将所有图表的实现代码封装在一个Char类中,其框架代码如下所示:

public class Chart
{private string _type;private double[][] _data;public Chart(double[][] data, string type){_type = type.ToLower();_data = data;if (_type == "histogram"){//初始化柱状图}else if (_type == "pie"){//初始化饼状图}else if (_type=="line"){//初始化折线图}}public void Display(){if (_type == "histogram"){//显示柱状图}else if (_type == "pie"){//显示饼状图}else if (_type == "line"){//显示折线图}}
}

客户端代码通过调用Chart类的构造函数来创建图表对象,根据参数type的不同可以得到不同类型的图表,然后再调用Display()方法来显示相应的图表。

不难看出,Chart类是一个“巨大的”类,在该类的设计中存在如下几个问题:

  • Chart类中包含很多if…else…代码块,整个类的代码冗长,代码越长,阅读难度、维护难度和测试难度也越大。而且大量条件语句的存在还将影响系统的性能,程序在执行过程中需要做大量的条件判断。
  • Chart类的职责过重,将各种图表对象的初始化代码和显示代码集中在一个类中实现,违反了“单一职责原则”。
  • 当需要增加新类型的图表时,必须修改Chart类的源码,违反了“开闭原则”。

为了将Chart类的职责分离,Sunny 软件公司开发人员决定对图表库进行重构,重构后的结构如下图所示:

图2 图表库结构图

完整代码如下:

public interface IChart
{void Display();
}public class HistogramChart : IChart
{public void Display(){Console.WriteLine("显示柱状图");}public HistogramChart(){Console.WriteLine("创建柱状图");}
}public class PieChart : IChart
{public void Display(){Console.WriteLine("显示饼状图");}public PieChart(){Console.WriteLine("创建饼状图");}
}public class LineChart : IChart
{public void Display(){Console.WriteLine("显示折线图");}public LineChart(){Console.WriteLine("创建折线图");}
}public class ChartFactory
{public static IChart CreateChart(string type){IChart chart = null;switch (type.ToLower()){case "histogram":chart = new HistogramChart();break;case "pie":chart = new PieChart();break;case "line":chart = new LineChart();break;}return chart;}
}

客户端代码:

class Program
{static void Main(string[] args){IChart chart = ChartFactory.CreateChart("histogram");chart.Display();}
}

图3 输出结果

简单工厂模式(Simple Factory Pattern)
定义一个工厂类,它可以根据参数的不同返回不同类型的实例,被创建的实例通常都具有共同的父类。

由于在 简单工厂模式 中用于创建实例的方法是静态(static)方法,因此 简单工厂模式 又称为 静态工厂方法(Static Factory Method)模式。

简单工厂模式 提供了专门的工厂类来创建对象,将对象的创建和对象的使用分开,其要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建的细节。其结构图如下所示:

图4 简单工厂模式结构图

在使用 简单工厂模式 时,首先需要对产品类进行重构,不能设计一个包罗万象的产品类,而需要根据实际情况设计一个产品等级结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类实现。

典型的抽象产品类代码如下所示:

public abstract class Product
{public void MethodSame(){//所有产品类的公共业务方法}//声明抽象业务方法public abstract void MethodDiff();
}

在具体产品类中实现抽象产品类中声明的抽象方法,不同的具体产品类可以提供不同的实现,典型的具体产品类代码如下所示:

public class ConcreteProductA : Product
{public override void MethodDiff(){//实现业务方法}
}public class ConcreteProductB : Product
{public override void MethodDiff(){//实现业务方法}
}

简单工厂模式 的核心是工厂类,在没有工厂类之前,客户端一般会使用new 关键字来直接创建产品对象,而在引入工厂类之后,客户端可以通过工厂类中的静态工厂方法,根据传入的不同参数创建不同的产品对象。典型的工厂类代码如下所示:

public class Factory
{public static Product CreateProduct(string arg){Product product = null;switch (arg){case "A":product = new ConcreteProductA();break;case "B":product = new ConcreteProductB();break;}return product;}
}

在客户端代码中,我们通过调用工厂类的工厂方法即可得到产品对象,典型代码如下所示:

public class Client
{public static void Main(){//通过工厂类创建产品对象Product product = Factory.CreateProduct("A");product.MethodSame();product.MethodDiff();}
}

简单工厂模式简化

有时候,为了简化 简单工厂模式,我们可以将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中。客户端可以通过产品父类的静态工厂方法,根据参数的不同创建不同类型的产品子类对象。如下图所示:

图5 简化的简单工厂模式


工厂方法模式

Sunny 软件公司欲开发一个系统运行日志记录器Logger),该记录器可以通过多种途径保证系统能够记录日志,如:

  • 通过文件记录
  • 通过数据库记录

用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny 公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。

如何封装记录器的初始化过程并且保证多种记录器切换的灵活性是 Sunny 公司开发人员面临的一个难题。

Sunny 公司开发人员最初使用 简单工厂模式 对日志记录器进行了设计,初始结构如下图所示:

图6 基于简单工厂模式设计的日志记录器结构图

工厂类 LoggerFactory代码片段如下所示:

public class LoggerFactory
{public static ILogger CreateLogger(string arg){if (arg.ToUpper() == "DB"){//连接数据库,代码省略ILogger logger = new DatabaseLogger();//初始化数据库日志记录器,代码省略return logger;}if (arg.ToUpper() == "FILE"){ILogger logger = new FileLogger();//初始化文件日志记录器,代码省略return logger;}return null;}
}

通过使用 简单工厂模式,我们将日志记录对象的创建和使用分离,客户端只需要使用由工厂类创建的日志记录器对象即可,无须关心对象的创建过程,但是我们发现,虽然简单工厂模式实现了对象的创建和使用分离,但是仍然存在如下两个问题

  • 工厂类过于庞大,包含了大量的if语句代码,导致维护和测试难度增大,违反了“单一职责原则”。
  • 系统扩展不灵活,如增加新类型的日志记录器,必须修改静态工厂方法的业务逻辑,违反了“开闭原则”。

如何解决这两个问题?

于是产生了一种 简单工厂模式 的改进方案 – 工厂方法模式

Sunny 公司开发人员决定改进 简单工厂模式 来设计日志记录器,解决方案的基本结构如下图所示:

图7 日志记录器结构图

产品等级结构的代码如下:

public interface ILogger
{void WriteLog();
}public class DatabaseLogger : ILogger
{public void WriteLog(){Console.WriteLine("数据库日志记录。");}
}public class FileLogger : ILogger
{public void WriteLog(){Console.WriteLine("文件日志记录");}
}

工厂等级结构的代码如下:

public interface ILoggerFactory
{ILogger CreateLogger();
}public class FileLoggerFactory : ILoggerFactory
{public ILogger CreateLogger(){//创建文件日志记录器对象ILogger logger = new FileLogger();//初始化文件日志记录器,代码省略return logger;}
}public class DatabaseLoggerFactory : ILoggerFactory
{public ILogger CreateLogger(){//连接数据库,代码省略//创建数据库日志记录器对象ILogger logger = new DatabaseLogger();//初始化数据库日志记录器,代码省略return logger;}
}

客户端代码如下:

class Program
{static void Main(string[] args){ILoggerFactory factory = new FileLoggerFactory();ILogger logger = factory.CreateLogger();logger.WriteLog();}
}

为了让系统具有更好的灵活性和扩展性,Sunny 公司开发人员决定对日志记录器客户端代码进行重构,使得在不修改任何客户端代码的基础上可以实现更换或增加新的日志记录方式。

Sunny 公司开发人员创建了 App.config 文件用于存储日志记录器工厂类的类名。

<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="FactoryName" value="SunnyFactoryMethod.FileLoggerFactory"/></appSettings>
</configuration>

客户端不是通过 new 来创建工厂对象,而是通过读取配置文件获取该类名字符串,再使用 C# 反射机制,根据类名字符串来生成对象。

using System.Reflection;
using System.Configuration;class Program
{static void Main(string[] args){string className = ConfigurationManager.AppSettings["FactoryName"];Assembly assembly = Assembly.Load("SunnyFactoryMethod");ILoggerFactory factory = assembly.CreateInstance(className) as ILoggerFactory;if (factory != null){ILogger logger = factory.CreateLogger();logger.WriteLog();}}
}

引入配置文件 App.config 后,如果需要增加新的日志记录方式,只需要执行如下几个步骤:

  • Step1:新的日志记录器需要实现ILogger接口。
  • Step2:对应增加一个新的具体日志记录器工厂,需要实现ILoggerFactory接口,在CreateLogger方法中配置好初始化参数和环境变量,返回具体日志记录器对象。
  • Step3:修改配置文件 App.config,将新增的具体日志记录器的工厂类的类名字符串替换原有工厂类类名字符串。
  • Step4:编译新增的具体日志记录器类和具体日志记录器工厂类,运行客户端即可使用新的日志记录方式,而原有代码无须修改,符合“开闭原则”。

通过上述重构,可以使系统更加灵活,由于很多设计模式都关注系统的扩展性和灵活性,因此都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。

工厂方法模式(Factory Method Pattern)
定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式 让一个类的实例化延迟到其子类。

工厂方法模式 又简称为 工厂模式(Factory Pattern),也可称为 虚拟构造器模式(Virtual Constructor Pattern)或 多态工厂模式(Polymorphic Factory Pattern)。

工厂方法模式 中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,提供一个与产品等级结构对应的工厂等级结构

工厂方法模式 结构如下图所示:

图8 工厂方法模式结构图

工厂方法模式 结构图中包含如下几个角色:

  • Product:抽象产品类,是 工厂方法模式 所创建对象的基类。
  • ConcreteProduct:具体的产品类,继承Product抽象类。某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
  • Factory:抽象工厂类,声明了工厂方法(FactoryMethod),用于返回一个Product类型的对象。抽象工厂类是 工厂方法模式 的核心,所有创建对象的工厂类都必须继承它。
  • ConcreteFactroy:复写工厂方法以返回一个ConcreteProduct实例。

实现代码如下:

public abstract class Factory
{public abstract Product FactoryMethod();
}public class ConcreteFactory : Factory
{public override Product FactoryMethod(){return new ConcreteProduct();}
}

在实际使用时,具体工厂类在实现工厂方法时除了创建具体产品对象之外,还可以负责产品对象的初始化以及一些资源和环境配置工作,例如连接数据库、创建文件等。

在客户端代码中,只需要关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端代码如下:

class Program
{static void Main(string[] args){//可通过配置文件实现Factory creator = new ConcreteFactory();Product product = creator.FactoryMethod();}
}

扩展工厂方法模式

Sunny 公司开发人员通过进一步分析,发现可以通过多种方式来初始化日志记录器,例如:

  • 可以为各种日志记录器提供默认实现;
  • 还可以为数据库日志记录器提供数据库连接字符串,为文件日志记录器提供文件路径;
  • 也可以将参数封装在一个object类型的对象中,通过object对象将配置参数传入工厂类。

此时,可以提供一组重载的工厂方法,以不同的方式对产品对象进行创建。当然,对于同一个具体工厂而言,无论使用那个工厂方法,创建的产品类型均要相同。如下图所示:

图9 重载的工厂方法结构图


抽象工厂模式

Sunny 软件公司欲开发一套界面皮肤库,可以对 C# 桌面软件进行界面美化。用户在使用时可以通过菜单来选择皮肤,不同的皮肤提供视觉效果不同的按钮、文本框、组合框等界面元素,其结构如下图所示:

图10 界面皮肤库结构示意图

该皮肤库需要具备良好的灵活性和可扩展性,用户可以自由选择不同的皮肤,开发人员可在不修改既有代码的基础上增加新的皮肤。

Sunny 软件公司的开发人员针对上述要求,决定使用 工厂方法模式 进行系统的设计,初始结构如下图所示:



图11 基于工厂方法模式的界面皮肤库初始结构图

在图11中,提供了大量工厂来创建具体的界面组件,可以通过配置文件更换具体界面组件从而改变界面风格。但是,此设计方案存在如下问题:

  • 当需要增加新的皮肤时,虽然不要修改现有代码,但是需要增加大量类,针对每一个新增加具体组件都需要增加一个具体工厂,类的个数成对增加,这无疑会导致系统越来越庞大,增加系统的维护成本和运行开销。
  • 由于同一种风格的具体界面组件通常要一起显示,因此需要为每个组件都选择一个具体工厂,用户在使用时必须逐个进行设置,如果某个具体工厂选择失误会导致界面显示混乱。

改进后的设计如下图所示:

图12 界面皮肤库结构图

产品等级结构:

public interface IButton
{void Display();
}public class SpringButton:IButton
{public void Display(){Console.WriteLine("显示浅绿色按钮。");}
}public class SummerButton : IButton
{public void Display(){Console.WriteLine("显示浅蓝色按钮。");}
}public interface ITextField
{void Display();
}public class SpringTextField : ITextField
{public void Display(){Console.WriteLine("显示绿色边框文本框。");}
}public class SummerTextField : ITextField
{public void Display(){Console.WriteLine("显示蓝色边框文本框。");}
}public interface IComboBox
{void Display();
}public class SpringComboBox : IComboBox
{public void Display(){Console.WriteLine("显示绿色边框组合框。");}
}public class SummerComboBox : IComboBox
{public void Display(){Console.WriteLine("显示蓝色边框组合框。");}
}

工厂等级结构:

public interface ISkinFactory
{IButton CreateButton();ITextField CreateTextField();IComboBox CreateComboBox();
}public class SpringSkinFactory : ISkinFactory
{public IButton CreateButton(){return new SpringButton();}public ITextField CreateTextField(){return new SpringTextField();}public IComboBox CreateComboBox(){return new SpringComboBox();}
}public class SummerSkinFactory : ISkinFactory
{public IButton CreateButton(){return new SummerButton();}public ITextField CreateTextField(){return new SummerTextField();}public IComboBox CreateComboBox(){return new SummerComboBox();}
}

配置文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="FactoryName" value="SunnyAbstractFactory.SpringSkinFactory"/></appSettings>
</configuration>

客户端代码如下:

using System.Reflection;
using System.Configuration;
class Program
{static void Main(string[] args){string factory = ConfigurationManager.AppSettings["FactoryName"];ISkinFactory skinFactory = Assembly.Load("SunnyAbstractFactory").CreateInstance(factory) as ISkinFactory;if (skinFactory != null){IButton button = skinFactory.CreateButton();ITextField textField = skinFactory.CreateTextField();IComboBox comboBox = skinFactory.CreateComboBox();button.Display();textField.Display();comboBox.Display();}}
}

图13 程序输出结果

如果需要更换皮肤,只需修改配置文件即可,在实际环境中,我们可以提供可视化界面,例如菜单或者窗口来修改配置文件,用户无须直接修改配置文件。

如果需要增加皮肤,只需要增加一族新的具体组件并对应提供一个新的具体工厂,修改配置文件即可使用新的皮肤,原有代码无须修改,符合“开闭原则”。

工厂方法模式 中具体工厂负责生产具体产品,每一个具体工厂对应一种具体产品。但是有时候我们希望一个工厂可以提供多个产品对象,而不是单一的产品对象,如一个电器工厂,它可以生产电视机、电冰箱等多种电器,而不是只生产某一种电器。

为了更好地理解 抽象工厂模式,我们先引入两个概念:

  • 产品等级结构产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构。
  • 产品族指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱。海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成一个产品族。

产品等级结构与产品族如下图所示:

图14 产品族和产品等级结构示意图

图14中一共有五个 产品族,分别属于三个不同的 产品等级结构。如果使用 工厂方法模式,需要提供15个具体工厂,而使用 抽象工厂模式 只需要提供5个具体工厂。

抽象工厂模式工厂方法模式 最大的区别在于:

  • 工厂方法模式:针对的是一个产品等级结构;
  • 抽象工厂模式:针对的是一个产品族,每一个具体工厂可以生产属于一个产品族的所有产品。

抽象工厂(Abstract Factory)
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

抽象工厂模式 中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族,结构如下图所示:

图15 抽象工厂模式结构图

抽象工厂可以是接口,也可以是抽象类,典型代码如下:

public interface IFactory
{IProductA CreateProductA();IProductB CreateProductB();
}

具体工厂实现了抽象工厂。典型代码如下:

public class ConcreteFacory1 : IFactory
{public IProductA CreateProductA(){return new ConcreteProductA1();}public IProductB CreateProductB(){return new ConcreteProductB1();}
}

工厂方法模式 一样,抽象工厂模式 也可为每一种产品提供一组重载的工厂方法,以不同的方式对产品对象进行创建。

扩展案例 I

在如今的 Web 应用程序中 HTML5 正在成为趋势,但是仍然有很多不支持 HTML5 的浏览器。我们需要开发一个能够让用户在浏览器中进行绘图的通用画布,如:

  • 绘制直线;
  • 矩形;

程序可以通过判断浏览器是否支持 HTML5,来采用相应的绘图方式。

解决方案如下:

图16 通用画布结构图

产品等级结构代码:

public interface IBaseLine
{void SetWidth(int width);void DrawLine(int x1, int y1, int x2, int y2);
}public class GraphicalLine : IBaseLine
{public void SetWidth(int width){Console.WriteLine("set Graphical line width.");}public void DrawLine(int x1, int y1, int x2, int y2){Console.WriteLine("draw Graphical line.");}
}public class Html5Line : IBaseLine
{public void SetWidth(int width){Console.WriteLine("set html5 line width.");}public void DrawLine(int x1, int y1, int x2, int y2){Console.WriteLine("draw html5 line.");}
}public interface IBaseRectangle
{void SetLineWidth(int width);void DrawRect(int x1, int y1, int x2, int y2);
}public class GraphicalRectangle : IBaseRectangle
{public void SetLineWidth(int width){Console.WriteLine("set Graphical rect");}public void DrawRect(int x1, int y1, int x2, int y2){Console.WriteLine("draw Graphical rect");}
}public class Html5Rectangle : IBaseRectangle
{public void SetLineWidth(int width){Console.WriteLine("set html5 rect");}public void DrawRect(int x1, int y1, int x2, int y2){Console.WriteLine("draw html5 rect");}
}

工厂等级结构代码:

public interface ICanvasFactory
{IBaseRectangle CreateRectangle();IBaseLine CreateLine();
}public class GraphicalCanvas : ICanvasFactory
{public IBaseRectangle CreateRectangle(){return new GraphicalRectangle();}public IBaseLine CreateLine(){return new GraphicalLine();}
}public class Html5Canvas : ICanvasFactory
{public IBaseRectangle CreateRectangle(){return new Html5Rectangle();}public IBaseLine CreateLine(){return new Html5Line();}
}

客户端代码如下:

class Program
{private static bool isHtml5 = false;private static void Draw(ICanvasFactory canvas){IBaseLine line = canvas.CreateLine();IBaseRectangle rect = canvas.CreateRectangle();line.SetWidth(1);line.DrawLine(1, 1, 2, 2);rect.SetLineWidth(1);rect.DrawRect(1, 1, 2, 2 );}static void Main(string[] args){if (isHtml5 == false)Draw(new GraphicalCanvas());elseDraw(new Html5Canvas());}
}

扩展案例 II

大鸟看着刚推门而入的小菜问道:“怎么这么晚才回来?”

小菜:“嗨,没办法呀。我本来写好了一个项目,是给一家企业做的电子商务网站,是用 SQL Server 作为数据库的,应该说上线后除了开始有些小问题,基本都还可以。而后,接到另外一家公司类似需求的项目,但这家公司想省钱,租用了一个空间,只能用 Access,不能用 SQL Server,于是要求我改造原来那个项目的代码。”

大鸟:“你的麻烦来了。”

小菜:“是呀,那是相当的麻烦。但开始我觉得很简单呀,因为 SQL ServerAccessADO. NET 上的使用是不同的:

  • SQL Server上用的是System.Data.SqlClient命名空间下的SqlConnectionSqlCommandSqlParameterSqlDataReaderSqlDataAdapter
  • Access 则要用System.Data.OleDb命名空间下的相应对象;

我以为只要做一个全体替换就可以了,可替换后,错误百出。”

大鸟:“那是一定的,两者有不少不同的地方。你都找到了些什么问题?”

小菜:“实在是多呀。

  • 在插入数据时 Access 必须要 insert intoSQL Server 可以不用into的;
  • SQL Server中的GetDate()Access 中没有,需要改成Now()
  • SQL Server 中有字符串函数Substring,而 Access 中根本不能用,我找了很久才知道,可以用Mid,这好像是 VB 中的函数。”

大鸟:“小菜还真犯了不少错呀,insert into这是标准语法,你干吗不加into,这是自找的麻烦。”

小菜:“这些问题也就罢了,最气人的是程序的登陆代码,老是报错,我怎么也找不到出了什么问题,搞了几个小时。最后才知道,原来 Access 对一些关键字,例如 password 是不能作为数据库的字段的,如果字段名是 passwordSQL Server 中什么问题都没有,运行正常,在 Access 中就是报错,而且报得让人莫名其妙。”

大鸟:“‘关键字’应该要用‘[’ 和 ‘]’ 包起来,不然是容易出错的。”

小菜:“就这样,今天加班到这时候才回来。”

大鸟:“以后你还有的是班要加了。”

小菜:“为什么?”

大鸟:“只要网站要维护,比如修改或增加一些功能,你就得改两个项目吧,至少在数据库中做改动,相应的程序代码都要改,甚至和数据库不相干的代码也要改,你既然有两个不同的版本,两倍的工作量也是必然的。”

小菜:“是呀,如果哪一天要用 Oracle 数据库,估计我要改动的地方更多了。”

大鸟:“那是当然,OracleSQL 语法与 SQL Server 的差别更大。你的改动将是空前的。”

小菜:“大鸟只会夸张,哪有这么严重,大不了再加两天班就什么都搞定了。”

大鸟:“哼”,大鸟笑着摇摇头,很不屑一顾,“菜鸟程序员碰到问题,只会用时间来摆平,所以即使整天加班,老板也不想给菜鸟加工资,原因就在于此。”

小菜:“你什么意思嘛!那你说怎么搞定才是好呢?”小菜气道。

大鸟:“先写一段你原来数据访问的做法吧,以操作User表和Department表为例。”

  • User类和Department类,假设只有IdName两个字段,其余的省略。
  • SqlServerUser类:用于操作User表,假设只有“新增用户”和“得到用户”方法,其余方法以及具体的 SQL 语句省略。
  • SqlServerDepartment类:用于操作Department表,假设只有“新增部门”和“得到部门”方法,其余方法以及具体的 SQL 语句省略。

图17 最初数据库访问的结构

客户端代码如下:

class Program
{static void Main(string[] args){User user = new User{Id = 1,Name = "Patrick"};Department department = new Department{Id = 1,Name = "NCEPU"};SqlServerUser suser = new SqlServerUser();SqlServerDepartment sdepartment = new SqlServerDepartment();suser.Insert(user);suser.GetUser(1);sdepartment.Insert(department);sdepartment.GetDepartment(1);}
}

“这里之所以不能换数据库,原因就在于

SqlServerUser suser = new SqlServerUser();
SqlServerDepartment sdepartment = new SqlServerDepartment ();

使得susersdepartment对象被框死在 SQL Server 上了。如果这里是灵活的,专业的说法,是多态的,那么在执行InsertGet方法时就不用考虑是用 SQL Server 还是用 Access 了。”

由于这里不满足 依赖倒转原则,使得数据库更换后需要修改大量的代码。这样也违背了 开闭原则

图18 利用抽象工厂访问数据库的结构

利用 抽象工厂模式 来访问数据库时,若客户端程序很多地方都在使用IUserIDepartment,而这样的设计,其实在每一个类的开始都需要声明:

IFactory factory = new SqlServerFactory();

如果我有100个调用数据库访问的类,就要更改100次

IFactory factory = new AccessFactory();

这样的代码才行。这不能解决更改数据库访问时,改动一处就完全更改的要求!

编程是艺术,这样大批量的改动,显然是非常丑陋的做法。一定有更好的办法。”

利用命名规则+反射原理,重新设计的方案如下:

图19 访问数据库的改进

配置文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="DB" value="SqlServer"/></appSettings>
</configuration>

程序代码如下:

namespace AbstractFactory
{using System.Reflection;using System.Configuration;public class DataAccess{private const string AssemblyName = "AbstractFactory ";private static readonly string Db = ConfigurationManager.AppSettings["DB"];public static IUser CreateUser(){string className = AssemblyName + "." + Db + "User";return Assembly.Load(AssemblyName).CreateInstance(className) as IUser;}public static IDepartment CreateDepartment(){string className = AssemblyName + "." + Db + "Department";return Assembly.Load(AssemblyName).CreateInstance(className) as IDepartment;}}
}

“开闭原则”的倾斜性

Sunny 公司使用 抽象工厂模式 设计了界面皮肤库,该皮肤库可以较为方便地增加新的皮肤,但是现在遇到一个非常严重的问题:

  • 由于设计时考虑不全面,忘记为单选按钮(RadioButton)提供不同皮肤的风格化显示,导致无论选择哪种皮肤,单选按钮都显得那么“格格不入”。
  • Sunny 公司的设计人员决定向系统中增加单选按钮,但是发现原有系统居然不能够在符合“开闭原则”的前提下增加新的组件,原因是抽象工厂ISkinFactory中根本没有提供创建单选按钮的方法,如果需要增加单选按钮,首先需要在ISkinFactory中新增创建单选按钮方法的声明,然后逐个修改具体工厂类,增加相应方法以实现在不同的皮肤中创建单选按钮,此外还需要修改客户端,否则单选按钮无法应用于现有系统。

这是 抽象工厂模式 最大的缺点。

抽象工厂模式 中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式 的这种性质称为开闭原则”的倾斜性

开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族多个产品等级结构的系统,其功能增强包括两个方面:

  • 增加产品族:对于增加新的产品族,抽象工厂模式 很好地支持了“开闭原则”。
  • 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。

正因为 抽象工厂模式 存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品等级结构提供这样的方便。

因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。


后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码:



相关文章:

转:初探 jQuery 的 Sizzle 选择器

这是一篇关于介绍jQuery Sizzle选择器的文章&#xff0c;由我和obility共同完成。在文中&#xff0c;我们试图用自己的语言配以适量的代码向读者展现出Sizzle在处理选择符时的流程原理&#xff0c;以及末了以少许文字给你展示出如何借用Sizzle之手实现自定义选择器&#xff08;…

安装hadoop图文

1.下载hadoop-2.5.1,存放根目录 2.通过tar -zxvf 包名 来进行解压 3.通过mv命令将解压后的hadoop包移动到/home下 4.修改hadoop-en.sh配置文件,添加jdk的安装目录,操作如下图所示 5.修改core-site.xml配置文件,添加namenode的配置信息 6.修改hdfs-site.xml配置文件,添加seconda…

Java中父类方法重写有哪些需要注意的?

在继承关系中&#xff0c;子类会自动继承父类中公共的方法&#xff0c;但有时在子类中需要对继承的方法进行一些修改&#xff0c;即对父类的方法进行重写。需要注意的是&#xff0c;子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型。 在上一节…

技术图文:02 创建型设计模式(下)

创建型设计模式&#xff08;下&#xff09; 知识结构&#xff1a; 图1 知识结构 单例模式 – 确保对象的唯一性 Sunny 软件公司承接了一个服务器负载均衡软件的开发工作&#xff0c;该软件运行在一台负载均衡服务器上&#xff0c;可以将并发访问和数据流量分发到服务器集群中…

[转载]C# 二进制与十进制,十进制与十六进制相互转换

原文地址&#xff1a;C# 二进制与十进制,十进制与十六进制相互转换作者&#xff1a;tonytonglx十进制转二进制&#xff1a;用2辗转相除至结果为1 将余数和最后的1从下向上倒序写就是结果例如302302/2 151 余0151/2 75 余175/2 37 余137/2 18 余118/2 9 余09/2 4 余14/2 …

感知哈希算法——找出相似的图片

参考Neal Krawetz博士的这篇文章, 实现这种功能的关键技术叫做"感知哈希算法"(Perceptual Hash Algorithm), 意思是为图片生成一个指纹(字符串格式), 两张图片的指纹越相似, 说明两张图片就越相似. 但关键是如何根据图片计算出"指纹"呢? 下面用最简单的步…

学web前端需要了解哪些常识

想要学好web前端技术&#xff0c;那么一定要掌握足够的知识&#xff0c;web前端技术包含很多方面的知识&#xff0c;具体学web前端需要了解哪些常识?来看看下面的详细介绍。 学web前端需要了解哪些常识? html css javascript。 要学的内容实在很多&#xff0c;如果没有其他编…

linux下后台执行shell脚本

一句话 nohup sh startup_Server.sh & 转载于:https://www.cnblogs.com/phpcode/archive/2012/04/24/2522761.html

线性代数:第一章 线性方程组

本讲义是自己上课所用幻灯片&#xff0c;里面没有详细的推导过程&#xff08;笔者板书推导&#xff09;只以大纲的方式来展示课上的内容&#xff0c;以方便大家下来复习。 从本章开始&#xff0c;我们一起来学习线性代数的有关知识&#xff0c;线性代数的应用之一就是求解复杂…

菜鸟也来学习ORACLE(1)_linux下安装oracle 11g

加入 oracle Club 之前&#xff0c;学长给我们开了个小会 说是看看我们加入的意愿&#xff0c;哎哎 其实直无聊&#xff0c;但是大体比较重视linux 服务器的搭建 以及在linux 下安装oracle 搭建一个oracle 环境吧、我就想这东西能有多难&#xff0c;于是回来就搭建起了&#x…

CSS浮动元素特点有什么

什么是浮动? 元素的浮动是指设置了浮动属性(flot)的元素。 CSS浮动有什么作用? 1.让多个盒子水平排列成一行&#xff0c;浮动成为布局的重要手段; 2.可以实现盒子的左右对齐等等; 3.浮动最早是用来控制图片&#xff0c;实现文字环绕图片的效果。 CSS浮动的语法&#xff1a; 选…

数据结构与算法:11 Leetcode同步练习(四)

目录 题目01&#xff1a;最小栈题目02&#xff1a;有效的括号题目03&#xff1a;用队列实现栈题目04&#xff1a;整数反转题目05&#xff1a;逆波兰表达式求值题目06&#xff1a;全排列题目07&#xff1a;字符串转换整数 (atoi)题目08&#xff1a;设计循环双端队列题目09&…

trie树 详解

前几天学习了并查集和trie树&#xff0c;这里总结一下trie。 本文讨论一棵最简单的trie树&#xff0c;基于英文26个字母组成的字符串&#xff0c;讨论插入字符串、判断前缀是否存在、查找字符串等基本操作&#xff1b;至于trie树的删除单个节点实在是少见&#xff0c;故在此…

启动hadoop的节点

1.启动hadoop的节点 start-dfs.sh 本文转自 素颜猪 51CTO博客&#xff0c;原文链接:http://blog.51cto.com/suyanzhu/1959242

什么是Python线程?Python线程如何创建?

相信正在学习Python技术或者对Python语言有一定了解的人对于Python线程应该都不陌生&#xff0c;但是也有刚接触Python的小伙伴对于Python线程并不了解&#xff0c;今天小编就跟大家聊聊什么是Python线程&#xff0c;又该如何创建Python线程! 什么是Python线程?Python线程如何…

ItemAdding实现数据验证--中文字段,properties.AfterProperties值为null的问题

最近写事件接收器&#xff0c;发现中文字段如果直接用properties.AfterProperties[“申请人"]这样获取的值为null&#xff0c;无法得到值。后拉忽然发现用英文字段可以得到值。难道中文字段需要编码&#xff1f;经过测试果真如此。 代码部分如下&#xff1a;public overri…

jstl c:choose、c:when和c:otherwise标签

在用spring mvc中&#xff0c;页面前端老用jstl&#xff0c;记录一下。 <c:choose>、<c:when>和<c:otherwise>在一起连用&#xff0c;可以实现Java语言中的if-else语句的功能。例如以下代码根据username请求参数的值来打印不同的结果&#xff1a; <c:choo…

怎样设计出优秀的测试用例?看看下面就知道了

想要成为一名合格的软件测试工程师&#xff0c;一份合格软件测试报告是非常重要的&#xff0c;软件测试的核心也就是测试的用例了&#xff0c;我们通过用例可以看出怎么设计出来可以发现问题&#xff0c;可以有效的覆盖需求的&#xff0c;没有冗余的用例是每个测试工程师必须跨…

数据结构与算法:12 数组与稀疏矩阵

12 数组与稀疏矩阵 知识结构&#xff1a; 1. 数组 1.1 数组的定义 数组是具有一定顺序关系的若干对象组成的集合&#xff0c;组成数组的对象称为数组元素。 例如&#xff1a; 向量对应一维数组 A(a0,a1,⋯,an−1)A(a_0,a_1,\cdots,a_{n-1}) A(a0​,a1​,⋯,an−1​) 矩阵…

管理索引表:深入研究B树索引--重建,合并,删除(理论篇3)

重建索引  如果表中记录频繁地被删除或插入&#xff0c;尽管表中的记录总量保持不变&#xff0c;索引空间的使用量会不断增加。虽然记录从索引中被删除&#xff0c;但是该记录索引项的使用空间不能被重新使用。因此&#xff0c;如果表变化不定&#xff0c;索引空间量会不断增…

模块架构不是软件成功的“决定因素”

【本文是09年的一篇旧文&#xff0c;出于某些原因&#xff0c;对原文内容有删减&#xff0c;在这里整理后重新发表】 前言感谢XXX对我们技术&#xff0c;对我们公司产品提出这些意见&#xff0c;我们公司卖的是软件产品&#xff0c;开发软件是一件技术活&#xff0c;说实话&…

JavaScript面向对象修改标签页详解

双击标签页组件中的li小标签或者section 中的文本&#xff0c;可以对文本进行编辑。为了实现这个功能&#xff0c;需要先给li和section元素绑定双击事件&#xff0c;当双击文本后&#xff0c;将文本改成一个文本框&#xff0c;用来输入新的内容&#xff0c;在文本框中显示原来的…

数据结构与算法:13 字符串与整数集合

13 字符串与整数集合 知识点&#xff1a; 1. 字符串 我们古人没有电影电视&#xff0c;没有游戏网络&#xff0c;所以文人们就会想出一些文字游戏来娱乐。比如宋代的李禺写了这样一首诗&#xff1a;“枯眼望遥山隔水&#xff0c;往来曾见几心知&#xff1f;壶空怕酌一杯酒&am…

是时候开始使用JavaScript严格模式了怎样启用javascri

E是时候开始使用JavaScript严格模式了怎样启用javascriCMAScript5将严格模式(strictmode)引入了Javascript中&#xff0c;目的是允许开发人员能够选择“更好”的Javascript版本&#xff0c;这个版本能用不同的方式处理那些普遍而又臭名昭著的错误。一开始的时候&#xff0c;我对…

Linux服务器日志备份到本地

1、确定线上服务器的日志文件名称和路径 2、一台本地服务器能连接公网&#xff0c;创建一个日志账户&#xff0c;设置密码 3、线上服务器要求&#xff1a; a、确定是否已安装sshpass包 [rootiZwz9ghdadtaey1msor7gnZ sh]# rpm -qa|grep sshpass sshpass-1.06-1.el7.x86_64 如不…

学习UI设计能做什么

UI设计这个岗位对于目前的很多企业来说是供不应求的&#xff0c;很多刚培训完UI设计的小伙伴&#xff0c;都不知道该如何定位自己的职能岗&#xff0c;那么学习UI设计能做什么呢?来看看下面小编的详细介绍就知道了。 学习UI设计能做什么? 1、图形设计/界面设计 软件产品的产品…

数据结构与算法:14 Leetcode同步练习(五)

Leetcode同步练习&#xff08;五&#xff09; 目录 题目01&#xff1a;用栈实现队列题目02&#xff1a;托普利茨矩阵题目03&#xff1a;罗马数字转整数题目04&#xff1a;最长公共前缀题目05&#xff1a;反转字符串题目06&#xff1a;无重复字符的最长子串题目07&#xff1a;…

Oracle Spatial构建自定义投影坐标系

之前项目换过服务器&#xff0c;移植数据库时候并没有正确完整的移植自定义的投影坐标系&#xff0c;结果就报出莫名其妙的一些错误&#xff0c;比如unable to transform rectangle due to: ORA-13199: SRID does not exist。 因为在移植坐标系的时候仅仅只是将MDSYS.SDO_CRS_C…

php.ini 中开启短标签

控制参数&#xff1a; short_open_tag On如果设置为Off&#xff0c;则不能正常解析类似于这样形式的php文件&#xff1a;<?phpinfo()?>而只能解析<?phpphpinfo()?>这样形式的php文件所以要想php支持短标签&#xff0c;需要我们把short_open_tag 设置为On. 本…