在平时编程中,构建对象最常用的方式是 new 一个对象。乍一看这种做法没什么不好,而实际上这也属于一种硬编码。每 new 一个对象,相当于调用者多知道了一个类,增加了类与类之间的联系,不利于程序的松耦合。其实构建过程可以被封装起来,工厂模式便是用于封装对象的设计模式。
1. 简单工厂模式
问题来了,我是调用者,我需要水果,new一个苹果,梨子,香蕉………我知道很多种水果名字,我就能new出这些吗?答案不一定,因为每个水果怎么创建出来的不一样。我要new出不同的水果,我不光要知道水果名字,我还要知道这个水果的创建方式(构造函数)。
这时候有个水果工厂,我只需要跟工厂说我要什么水果,给他一个水果名字,它就给我返回一个相应的水果。我不用知道这个水果是怎么创建的,工厂帮我做。
水果类
class Fruit//水果父类
{
}
class Apple: Fruit//苹果
{
}
class Pear: Fruit //梨子
{
}
class Banana: Fruit//香蕉
{
}
class Strawberry: Fruit //草莓
{
}
简单的水果工厂
class FruitsFactory//水果工厂
{
//给一个水果名,返回一个水果
public Fruit Create(string fruitname)
{
switch (fruitname)
{
case "苹果":return new Apple();
case "梨":return new Pear();
case "香蕉":return new Banana();
case "草莓":return new Strawberry();
default:return null;
}
}
}
调用工厂
static void Main(string[] args)
{
FruitsFactory factory = new FruitsFactory();
//有了工厂,现在我要一个苹果
Fruit apple = factory.Create("苹果");
//我要草莓
Fruit strawberry = factory.Create("草莓");
}
事实上,将构建过程封装的好处不仅可以降低耦合,如果某个产品构造方法相当复杂,使用工厂模式可以大大减少代码重复。比如,如果生产一个苹果需要苹果种子、阳光、水分,将工厂修改如下:
class FruitsFactory//水果工厂
{
//给一个水果名,返回一个水果
public Fruit Create(string fruitname)
{
switch (fruitname)
{
case "苹果":
AppleSeed appleSeed = new AppleSeed();
Sunlight sunlight = new Sunlight();
Water water = new Water();
return new Apple(appleSeed, sunlight, water);
case "梨":return new Pear();
case "香蕉":return new Banana();
case "草莓":return new Strawberry();
default:return null;
}
}
}
调用者的代码则完全不需要变化,而且调用者不需要在每次需要苹果时,自己去构建苹果种子、阳光、水分以获得苹果。苹果的生产过程再复杂,也只是工厂的事。这就是封装的好处,假如某天科学家发明了让苹果更香甜的肥料,要加入苹果的生产过程中的话,也只需要在工厂中修改,调用者完全不用关心。
总结
总而言之,简单工厂模式就是让一个工厂类承担构建所有对象的职责。调用者需要什么产品,让工厂生产出来即可。
它的弊端也显而易见:
一是如果需要生产的产品过多,此模式会导致工厂类过于庞大,承担过多的职责,变成超级类。当苹果生产过程需要修改时,要来修改此工厂。梨子生产过程需要修改时,也要来修改此工厂。也就是说这个类不止一个引起修改的原因。违背了单一职责原则。
二是当要生产新的产品时,必须在工厂类中添加新的分支。而开闭原则告诉我们:类应该对修改封闭。我们希望在添加新功能时,只需增加新的类,而不是修改既有的类,所以这就违背了开闭原则。
2. 工厂方法模式
为了解决简单工厂模式的这两个弊端,工厂方法模式应运而生,它规定每个产品都有一个专属工厂。比如苹果有专属的苹果工厂,梨子有专属的梨子工厂
不同的水果工厂 ```csharp class AppleFactory//苹果工厂 { public Fruit Create() {return new Apple();
} }
class PearFactory//梨工厂 { public Fruit Create() { return new Pear(); } }
class BananaFactory//香蕉工厂 { public Fruit Create() { return new Banana(); } }
调用工厂
```csharp
static void Main(string[] args)
{
//创建一个苹果工厂
AppleFactory factory = new AppleFactory();
//通过苹果工厂可以获得苹果
Fruit apple = factory.Create();
//需要梨子的时候,就创建梨子工厂
PearFactory pearFactory = new PearFactory();
Fruit pear = pearFactory.Create();
}
这样和直接 new 出苹果和梨子有什么区别?上文说工厂是为了减少类与类之间的耦合,让调用者尽可能少的和其他类打交道。
用简单工厂模式,我们只需要知道 FruitFactory,无需知道 Apple 、Pear 类,很容易看出耦合度降低了。
但用工厂方法模式,调用者虽然不需要和 Apple 、Pear 类打交道了,但却需要和 AppleFactory、PearFactory 类打交道。有几种水果就需要知道几个工厂类,耦合度完全没有下降啊,甚至还增加了代码量!
工厂模式的第二个优点在工厂方法模式中还是存在的。当构建过程相当复杂时,工厂将构建过程封装起来,调用者可以很方便的直接使用,同样以苹果生产为例:
class AppleFactory
{
public Fruit Create()
{
AppleSeed appleSeed = new AppleSeed();
Sunlight sunlight = new Sunlight();
Water water = new Water();
return new Apple(appleSeed, sunlight, water);
}
}
总结
调用者无需知道苹果的生产细节,当生产过程需要修改时也无需更改调用端。同时,工厂方法模式解决了简单工厂模式的两个弊端。
当生产的产品种类越来越多时,工厂类不会变成超级类。工厂类会越来越多,保持灵活。不会越来越大、变得臃肿。如果苹果的生产过程需要修改时,只需修改苹果工厂。梨子的生产过程需要修改时,只需修改梨子工厂。符合单一职责原则。
当需要生产新的产品时,无需更改已有的工厂,只需要添加新的工厂即可。保持了面向对象的可扩展性,符合开闭原则。
3. 抽象工厂模式
工厂方法模式可以进一步优化,提取出工厂接口。然后不同的水果工厂都实现这个接口
interface IFactory//工厂接口
{
Fruit Create();
}
class AppleFactory: IFactory
{
public Fruit Create()
{
return new Apple();
}
}
class PearFactory: IFactory
{
public Fruit Create()
{
return new Pear();
}
}
class BananaFactory: IFactory
{
public Fruit Create()
{
return new Banana();
}
}
可以看到,我们在创建时指定了具体的工厂类后,在使用时就无需再关心是哪个工厂类,只需要将此工厂当作抽象的 IFactory 接口使用即可。这种经过抽象的工厂方法模式被称作抽象工厂模式
static void Main(string[] args)
{
//需要苹果时,就创建苹果工厂
IFactory factory = new AppleFactory();
//通过苹果工厂可以获得苹果
Fruit fruit = factory.Create();
}
如果要替换成梨子,只需要修改一行代码
static void Main(string[] args)
{
IFactory factory = new PearFactory();
Fruit fruit = factory.Create();
}
总结
抽象工厂模式很好的发挥了开闭原则、依赖倒置原则,但缺点是抽象工厂模式太重了,如果 IFactory 接口需要新增功能,则会影响到所有的具体工厂类。使用抽象工厂模式,替换具体工厂时只需更改一行代码,但要新增抽象方法则需要修改所有的具体工厂类。所以抽象工厂模式适用于增加同类工厂这样的横向扩展需求,不适合新增功能这样的纵向扩展。