在讲FACTORY METHOD之前,我们先来看个故事(注:本故事纯属虚构)。
小王是某大学学生,刚刚毕业,找了一家知名企业,一路过关斩将,面试也很成功,HR表示很满意,在面试结束后,HR给了小王这样一道题目让他1天后发给他,并说第二天通知他结果。
“请用C++、Java、C#或VB.NET任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。”
小王看到题目有点意外,他心里想着这个简单呀(相信很多人都是这样觉得,因为题目本来就够简单 0.0),回家后,他花了2分钟就写好了这个程序,就给HR发过去了,然后就和同学出去。
然后呢,第二天,小王就收到了HR的信,他没被录用。
为什么没被录用呢,原因出在哪呢?面试hr都已经表示很满意了呀,小王很是不解。
大家肯定也很纳闷,到底是怎么回事呢?那么首先让我们来看看小王写的这个程序的吧。
class Program
{
static void Main(string[] args)
{
Console.Write("请输入数字A:");
string A = Console.ReadLine();
Console.Write("请选择运算符号(+、-、*、/):");
string B = Console.ReadLine();
Console.Write("请输入数字B:");
string C = Console.ReadLine();
string D = "";
if (B == "+")
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
if (B == "-")
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
if (B == "*")
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
if (B == "/")
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
Console.WriteLine("结果是:" + D);
}
}
“且先不说出题人的意思,单就现在的代码,就有很多不足的地方需要改进。比如
变量命名,你的命名就是ABCD,变量不带有任何具体含义,这是非常不规范的;
判断分支,你这样的写法,意味着每个条件都要做判断,等于计算机做了三次无用功;
数据输入有效性判断等,如果用户输入的是字符符号而不是数字怎么办?
如果除数时,客户输入了0怎么办?
这些都是可以改进的地方。”
就这样的代码给你,如果你是HR,你愿意录用小王吗?
小王自己也觉得问题出在这里了,就回信请HR再给他次机会,HR给他回信说,好吧,再给你一次机会,你把之前的程序重新做一下吧。
然而,在第三天收到HR的回信,依然没被录用,这次他是怎么写的呢,怎么还会没被录用呢,让我们里看看吧。
class Program
{
static void Main(string[] args)
{
try
{
Console.Write("请输入数字A:");
string strNumberA = Console.ReadLine();
Console.Write("请选择运算符号(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.Write("请输入数字B:");
string strNumberB = Console.ReadLine();
string strResult = "";
switch (strOperate)
{
case "+":
strResult = Convert.ToString(Convert.ToDouble(strNumberA) + Convert.ToDouble(strNumberB));
break;
case "-":
strResult = Convert.ToString(Convert.ToDouble(strNumberA) - Convert.ToDouble(strNumberB));
break;
case "*":
strResult = Convert.ToString(Convert.ToDouble(strNumberA) * Convert.ToDouble(strNumberB));
break;
case "/":
if (strNumberB != "0")
strResult = Convert.ToString(Convert.ToDouble(strNumberA) / Convert.ToDouble(strNumberB));
else
strResult = "除数不能为0";
break;
}
Console.WriteLine("结果是:" + strResult);
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("您的输入有错:" + ex.Message);
}
}
}
小王这次的程序相比较之前的程序已经有很大的进步了,然而他却有犯了初学者经常犯的问题。
所有编程初学者都会有这样的问题,就是碰到问题就直觉的用计算机能够理解的逻辑来描述和表达待解决的问题及具体的求解过程。这其实是用计算机的方式去思考,比如计算器这个程序,先要求输入两个数和运算符号,然后根据运算符号判断选择如何运算,得到结果,这本身没有错,但这样的思维却使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。
如果现在我们要求再写一个windows的计算器,现在的代码能不能复用呢?
答案是否定的。
这样的问题是我们经常要遇到的,当然只要我们让业务逻辑与界面逻辑分开,让它们之间的耦合度下降。只有分离开,才容易达到容易维护或扩展。
class Program
{
static void Main(string[] args)
{
try
{
Console.Write("请输入数字A:");
string strNumberA = Console.ReadLine();
Console.Write("请选择运算符号(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.Write("请输入数字B:");
string strNumberB = Console.ReadLine();
string strResult = "";
strResult = Convert.ToString(
Operation.GetResult(Convert.ToDouble(strNumberA),
Convert.ToDouble(strNumberB), strOperate));
Console.WriteLine("结果是:" + strResult);
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("您的输入有错:" + ex.Message);
}
}
}
public class Operation
{
public static double GetResult(double numberA, double numberB, string operate)
{
double result = 0d;
switch (operate)
{
case "+":
result = numberA + numberB;
break;
case "-":
result = numberA - numberB;
break;
case "*":
result = numberA * numberB;
break;
case "/":
result = numberA / numberB;
break;
}
return result;
}
}
上面的代码将耦合度降低了,比上次的好了不少,但是如果现在我们希望增加一个开根(sqrt)运算呢。
你可能会说,那只需要改Operation类就行了,在switch中加一个分支就可以了。
改Operation类,在switch中加一个分支就行吗?
问题是要加一个平方根运算,却需要把加减乘除的运算都得来参与编译,如果你一不小心,把加法运算改成了减法,这不是大大的糟糕。
打个比方,如果现在公司要求XX为公司的薪资管理系统做维护,原来只有技术人员(月薪),市场销售人员(底薪+提成),经理(年薪+股份)三种运算算法,现在要增加兼职工作人员的(时薪)算法,但按照上面程序写法,公司就必须要把包含有的原三种算法的运算类进行修改,如果心中小算盘一打,于是XX除了增加了兼职算法以外,在技术人员(月薪)算法中写了一句 :
if (员工是“某某”)
{
salary = salary * 1.1;
}
那后果会怎么样了。本来你加一个功能结果是原有的运行良好的代码发生了变化,这个风险太大了。
有人立马就想到了,那我将加减乘除等运算分离,修改其中一个不影响另外几个,增加运算也不影响其他代码,就可以了呀。
那就让我们来做一下吧。
/// 运算类
class Operation
{
private double _numberA = 0;
private double _numberB = 0;
/// 数字A
public double NumberA
{
get { return _numberA; }
set { _numberA = value; }
}
/// 数字B
public double NumberB
{
get { return _numberB; }
set { _numberB = value; }
}
/// 得到运算结果
public virtual double GetResult()
{
double result = 0;
return result;
}
}
/// 加法类
class OperationAdd : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA + NumberB;
return result;
}
}
/// 减法类
class OperationSub : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA - NumberB;
return result;
}
}
/// 乘法类
class OperationMul : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA * NumberB;
return result;
}
}
/// 除法类
class OperationDiv : Operation
{
public override double GetResult()
{
double result = 0;
if (NumberB == 0)
throw new Exception("除数不能为0。");
result = NumberA / NumberB;
return result;
}
}
首先有一个运算类,它有两个Number属性,主要用于计算的前后数,然后有一个虚函数GetResult() 用于得到结果,然后把加减乘除写成了运算类的子类,继承他后重写GetResult()方法,这样如果要修改任何一个算法,就不需要提供其他算法的代码了。但是呢,我们应该如何让计算器知道我是希望用那一个算法呢?
现实中的工厂给我们启示,工厂一般有多个部门,每个部门都有相应的职责,就像我们的加减乘除子类,而在一个工厂内,肯定有一个独立部门是进行组装的,他需要其他各个部门生产的零件。当他们需要某个零件的时候,向上级申请,然后上级再去调配。
我们的程序完全可以按照这种思想来做。
/// 运算类工厂
class OperationFactory
{
public static Operation createOperate(string operate)
{
Operation oper = null;
switch (operate)
{
case "+": { oper = new OperationAdd(); break; }
case "-": { oper = new OperationSub(); break; }
case "*": { oper = new OperationMul(); break; }
case "/": { oper = new OperationDiv(); break; }
}
return oper;
}
}
//界面类调用代码
Operation oper;
oper = OperationFactory.createOperate("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
运算类工厂就相当于工厂内的上级,当主程序发出“+”指令后,就去调用加法子类,进行运算。
需要增加各种复杂运算,比如平方根,立方根,自然对数,正弦余弦等,如何做?
只要增加相应的运算子类,还需要去修改运算类工厂,在switch中增加分支。
如果要修改界面呢?
改界面,不影响运算
对于前辈们有没有遇到这样的类似的问题呢,下面让给我们来看看第一种设计模式FACTORY METHOD:
FACTORY METHOD(工厂方法)
1. 意图
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
2. 别名
虚构造器( Virtual Constructor)
3. 动机
框架使用抽象类定义和维护对象之间的关系。这些对象的创建通常也由框架负责。
考虑这样一个应用框架,它可以向用户显示多个文档。在这个框架中,两个主要的抽象是类Application和Document。这两个类都是抽象的,客户必须通过它们的子类来做与具体应用相关的实现。因为被实例化的特定Document子类是与特定应用相关的,所以Application类不可能预测到哪个Document子类将被实例化—Application类仅知道一个新的文档何时应被创建,而不知道哪一种Document将被创建。
这就产生了一个尴尬的局面:框架必须实例化类,但是它只知道不能被实例化的抽象类。
Factory Method模式提供了一个解决办案。它封装了哪一个Document子类将被创建的信息并将这些信息从该框架中分离出来。
Application的子类重定义Application的抽象操作Create Document以返回适当的Document子类对象。一旦一个Application子类实例化以后,它就可以实例化与应用相关的文档,而无需知道这些文档的类。我们称CreateDocument是一个工厂方法( factory method),因为它负责“生产”一个对象。
4. 适用性
当一个类不知道它所必须创建的对象的类的时候。
当一个类希望由它的子类来指定它所创建的对象的时候。
当类将创建对象的职责委托给多个帮助子类中的某一个,并且希望将哪一个帮助子类是代理者这一信息局部化的时候。
5. 结构
6. 参与者
Product ( Document )
— 定义工厂方法所创建的对象的接口。
Concrete Product(My Document)
— 实现Product接口。
Creator(Application)
— 声明工厂方法,该方法返回一个Product类型的对象。Creator也可以定义一个工厂方法的缺省实现,它返回一个缺省的Concrete Product对象。
— 可以调用工厂方法以创建一个Product对象。
Concrete Creator(My Application)
— 重定义工厂方法以返回一个Concrete Product实例。
7. 协作
Creator依赖于它的子类来定义工厂方法,所以它返回一个适当的ConcreteProduct实例。
8.效果
工厂方法不再将与特定应用有关的类绑定到你的代码中。代码仅处理Product接口;因此它可以与用户定义的任何ConcreteProduct类一起使用。
工厂方法的一个潜在缺点在于客户可能仅仅为了创建一个特定的ConcreteProduct对象,就不得不创建Creator的子类。当Creator子类不必需时,客户现在必然要处理类演化的其他方面;但是当客户无论如何必须创建Creator的子类时,创建子类也是可行的。
9. 实现
当应用Factory Method模式时要考虑下面一些问题:
1 ) 主要有两种不同的情况Factory Method模式主要有两种不同的情况:
第一种情况是,Creator类是一个抽象类并且不提供它所声明的工厂方法的实现。
第二种情况是,Creator是一个具体的类而且为工厂方法提供一个缺省的实现。也有可能有一个定义了缺省实现的抽象类,但这不太常见。
2 ) 参数化工厂方法该模式的另一种情况使得工厂方法可以创建多种产品。工厂方法采用一个标识要被创建的对象种类的参数。工厂方法创建的所有对象将共享Product接口
优点:
工厂方法模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过它,外界可以从直接创建具体产品对象的尴尬局面中摆脱出来。
外界与具体类隔离开来,偶合性低。
明确区分了各自的职责和权力,有利于整个软件体系结构的优化。
缺点:
工厂类集中了所有实例的创建逻辑,容易违反GRASPR的高内聚的责任分配原则。
虽然工厂方法模式能够适应一定的变化,但是它所能解决的问题是远远有限的。
它所能创建的类只能是事先教考虑到的,如果需要添加新的类,则就需要改变工厂类了
对于上面的代码,我们根据工厂方法再进行下修改:
/// 运算类
class Operation
{
private double _numberA = 0;
private double _numberB = 0;
public double NumberA
{
get { return _numberA; }
set { _numberA = value; }
}
public double NumberB
{
get { return _numberB; }
set { _numberB = value; }
}
/// 得到运算结果
public virtual double GetResult()
{
double result = 0;
return result;
}
}
// 加法类
class OperationAdd : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA + NumberB;
return result;
}
}
// 减法类
class OperationSub : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA - NumberB;
return result;
}
}
/// 乘法类
class OperationMul : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA * NumberB;
return result;
}
}
/// 除法类
class OperationDiv : Operation
{
public override double GetResult()
{
double result = 0;
if (NumberB == 0)
throw new Exception("除数不能为0。");
result = NumberA / NumberB;
return result;
}
}
/// 工厂方法
interface IFactory
{
Operation CreateOperation();
}
/// 专门负责生产“+”的工厂
class AddFactory : IFactory
{
public Operation CreateOperation()
{
return new OperationAdd();
}
}
/// 专门负责生产“-”的工厂
class SubFactory : IFactory
{
public Operation CreateOperation()
{
return new OperationSub();
}
}
/// 专门负责生产“*”的工厂
class MulFactory : IFactory
{
public Operation CreateOperation()
{
return new OperationMul();
}
}
/// 专门负责生产“/”的工厂
class DivFactory : IFactory
{
public Operation CreateOperation()
{
return new OperationDiv();
}
}
//调用方式
//工厂方法
IFactory operFactory=new AddFactory();
Operation oper=operFactory.createOperate();
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
//简单工厂
Operation oper;
oper = OperationFactory.createOperate("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
推家大家去看看《大话设计模式》这本书,很有意思,写的很好