前言:工作中,经常会有朋友抱怨说,最烦的就是翻来覆去的修改功能,尤其是客户天马行空的功能需求,用再多的设计模式也避免不了改改改。。。没错,功能的变更不可避免的会导致代码的修改,万能的模式是不存在的。但是,在同样的功能修改的前提下,我们能加以控制的,就是多改与少改。好的设计可以使我们在修改功能需求时变得更加高效。
——————————————————————————————————————————————————————————————————————
有这样的一个场景,相信许多的朋友也遇到过,就是我们的某些已经比较成熟的项目,突然要重新包装一下,换个名字,换个数据库,当成一个新的项目提供给客户。也相信许多朋友在这个“重新包装”的过程中,吃尽了苦头。有些朋友吃的还不只一次,这就是没能很好地运用设计模式,导致程序出现变化时,不能做到灵活的扩展,从而徒增了大量的工作量。下面我们就用这个场景进行举例,来说说我们今天要讲的一个设计模式。(哈哈,这个例子也是被大家举烂了的)
首先,描述一下场景。我们现在需要重新修改一个程序,使之变成一个新的产品提供给客户,这个程序涉及到两个数据库表User(用户)和Department(部门),我们成熟的项目用的是SQL Server,但是新的产品需要用到Access。对于这样的一个程序,我们应该如何设计。
一、对于经验比较少的人,或对面向对象编程与设计模式理解不是很深刻的人,他可能会这样编写代码:
1、首先创建User与Department的实体类(我们仅用id与name两个属性)
public class Department {
public String id;
public String dName;
}
public class Department {
public String id;
public String dName;
}
2、创建用于操作数据库增删改查功能的工具类:
/**
* 操作User的工具类
*/
public class SqlServerUser {
public void insert(User user) {
// ......
System.out.println("在SqlServer中插入User成功");
}
public User getUser(String id) {
User u = null;
// ......
System.out.println("在SqlServer中查询到了User");
return u;
}
}
/**
* 操作Department的工具类
*/
public class SqlServerDepartment {
public void insert(Department department) {
// ......
System.out.println("在SqlServer中插入Department成功");
}
public Department getDepartment(String id) {
Department d = null;
// ......
System.out.println("在SqlServer中查询到了Department");
return d;
}
}
3、之后,在客户端调用
SqlServerUser su = new SqlServerUser();
su.insert(new User()); // 插入User
User u = su.getUser("1"); // 查询User
SqlServerDepartment sd = new SqlServerDepartment();
sd.insert(new Department()); // 插入Department
Department d = sd.getDepartment("1"); // 查询Department
好,从功能上看,这段代码是实现了操作数据库的目的。但是,这样写没有体现出面向对象语言的特性。SqlServerUser su = new SqlServerUser()与SqlServerDepartment du = new SqlServerDepartment()的写法完全的将其框死在了Sql Server上,这种写法专业点说,不是多态的,违反了里氏代换原则。其次就是,我们将实例化过程放到了客户端,使其必须考虑所用的具体数据库的种类,以及它的组成过程,这也违背了依赖倒转原则,不是面向接口编程的实现。当发生前面描述的变化时,直接导致的后果是,我们需要增加AccessUser和AccessDepartment,并且要修改多处使用了SqlServerUser su = new SqlServerUser()与SqlServerDepartment du = new SqlServerDepartment()的地方。如此看来,这种设计是不是很糟糕。
有些朋友会说,我哪知道以后会有哪些变化,毕竟功能需求是千变万化的。没错,但是我们也要尽可能全面的想到以后会有的变化,提前做好准备。或者在出现变化苗头的时候,及时重构我们的代码,应变将来发生的变化。
首先,我们程序开发的时候,我们应该知道,数据库操作,就是增删改查的操作,所以不管是什么表,用的是什么类型的数据库,都会有insert(插入)、get(查询获取)、updata(更新)和delete(删除)操作,唯一的区别就是我们操作的对象可能是User或者Deparment。按照依赖倒转原则,我们可以将这部分抽象成一个个的接口,然后针对接口进行程序的编写,这样就会提高我们程序的灵活性:
将对User对象的操作抽象成接口IUser
public abstract class IUser {
public abstract void insert(User user);
public abstract User getUser(String id);
}
将对Department对象的操作抽象成接口IDepartment
public abstract class IDepartment {
public abstract void insert(Department deprecated);
public abstract Department getDepartment(String id);
}
然后我们分别去实现这几个接口,因为我们自己用到的是SQL Server,我们就用它来实现,作为接口的子类
实现IUser
public class SqlServerUser extends IUser {
@Override
public void insert(User user) {
// ......
System.out.println("在SqlServer中插入User成功");
}
@Override
public User getUser(String id) {
User u = null;
// ......
System.out.println("在SqlServer中查询到了User");
return u;
}
}
实现IDepartment
public class SqlServerDepartment extends IDepartment {
@Override
public void insert(Department department) {
// ......
System.out.println("在SqlServer中插入Department成功");
}
@Override
public Department getDepartment(String id) {
Department d = null;
// ......
System.out.println("在SqlServer中查询到了Department");
return d;
}
}
接下来,我们要做的就是在客户端中调用他们,进行数据库的操作,这里我们要考虑的就是实例化SqlServerUser和SqlServerDepartment对象的过程了。有朋友会在客户端这样写:IUser u = new SqlServerUser(),还记得刚才我们分析的,让客户端自己考虑使用哪个数据库,考虑产品对象的组合过程,是不合理的。打个比方来说,我去快餐店吃薯条,我只需要知道,我要大份还是小份的就好,其他的我不关心。将数据库实例化放到客户端,就相当于,我去吃薯条,我要知道薯条怎么做成的,还要知道大份有多少,小份有多少,怎么包装的等等,这对我来说显然是不合理的。所以说,就我们程序而言,对SqlServerUser的实例化放到客户端也是不合理的。那我们应该怎么做呢?
我们一直在讲的就是创建型模式,所谓创建型模式就是将产品的创建过程抽象了出来,使客户不需要知道它是如何创建的,只知道我需要时,有途径可以创建它。我们前面已经学了两个创建型设计模式:简单工厂,工厂方法。这里要提的一句是:通常来说,设计应该从工厂方法开始,当设计者发现需要更大的灵活性时,设计便会向其他的创建型模式演变。
好,现在我们来设计接下来的代码,首先,抽象出创建所需对象的接口IFactory:
public abstract class IFactory {
public abstract IUser creatUser();
public abstract IDepartment creatDepartment();
}
因为有两个表,他们各自操作的对象不同,因此,User与Department属于不同的等级结构,所以我们要分别对他们的对象进行创建
然后,实现他们的创建过程:
public class SqlServerFactory extends IFactory{
@Override
public IUser creatUser() {
return new SqlServerUser();
}
@Override
public IDepartment creatDepartment() {
return new SqlServerDepartment();
}
}
最后,在客户端进行调用操作:
IFactory factory = new SqlServerFactory();
IUser user = factory.creatUser();
user.insert(new User()); // 插入User
User u = user.getUser("1"); // 查询User
IDepartment department = factory.creatDepartment();
department.insert(new Department()); // 插入Department
Department d = department.getDepartment("1"); // 查询Department
这样再看,是不是就比较舒服了。首先对于客户端,我无需知道数据库操作类的对象是如何创建的,我只通过create接口就获得了我所需要的对象,他的组成过程被封装到了SqlServerFactory中。当需要变更数据库为Access时,我只需要:
1、增加AccessUser和AccessDepartment操作类:
public class AccessUser extends IUser {
@Override
public void insert(User user) {
// ......
System.out.println("在Access中插入User成功");
}
@Override
public User getUser(String id) {
User u = null;
// ......
System.out.println("在Access中查询到了User");
return u;
}
}
public class AccessDepartment extends IDepartment {
@Override
public void insert(Department deprecated) {
// ......
System.out.println("在Access中插入Department成功");
}
@Override
public Department getDepartment(String id) {
Department d = null;
// ......
System.out.println("在Access中查询到了Department");
return d;
}
}
2、增加其实例化的工厂子类:
public class AccessFactory extends IFactory{
@Override
public IUser creatUser() {
return new AccessUser();
}
@Override
public IDepartment creatDepartment() {
return new AccessDepartment();
}
}
3、在客户端中,只需要将工厂替换为AccessFactory就可以完成数据库的替换:
IFactory factory = new AccessFactory();
IUser user = factory.creatUser();
user.insert(new User()); // 插入User
User u = user.getUser("1"); // 查询User
IDepartment department = factory.creatDepartment();
department.insert(new Department()); // 插入Department
Department d = department.getDepartment("1"); // 查询Department
是不是很方便?有人又会说,你还是在客户端实例化了,只不过这次实例化的是工厂AccessFactory。我举个例子,还是你去吃薯条,你最起码也要知道你去吃肯德基,还是吃麦当劳吧,不可能你虎躯一震,大吼一声:“老子要吃薯条!”天上就会掉下薯条吧,总需要有人去制作它们。而肯德基和麦当劳就充当了制造薯条的工厂,而他们的薯条就是产品对象,只不过属于不同的牌子。就像SqlServerUser(肯德基牌薯条)和AccessUser(麦当劳牌薯条)都是IUser(薯条),只不过SqlServerUser(肯德基牌薯条)是SqlServerFactory(肯德基)生产的,AccessUser(麦当劳牌薯条)是AccessFactory(麦当劳)生产的。
说了这么多,有朋友会问,不是说要讲抽象工厂模式么,说了那么多,抽象工厂到底怎么写?哈哈,其实前面的这些就是抽象工厂模式了,怎么样,没想到吧。
抽象工厂就是提供一个创建一系列相关对象的接口,而无需指定它们具体的类。简单来说就是,抽象工厂是伴随着产品族的概念而生的,它提供了创建这些产品的接口,而不必指出具体怎么创建某个确定的产品。
这么说,估计有人还是不理解,我再举例子解释下。
1、不管是肯德基还是麦当劳,它们不仅仅只生产薯条一种产品,也会生产汉堡
2、它们生产的产品,都属于各自的品牌。如麦当劳薯条,肯德基汉堡
3、它们都属于快餐店,或者说快餐工厂,都提供了create薯条() 和 create汉堡() 的接口
4、它们都有自己的名字,创建出属于自己品牌的制作流水线
这种一个工厂提供创建多种产品,一个产品可以有多重品牌的模式,就是抽象工厂了。
在拿我们的程序说明:
1、首先IUser和IDepartment都是产品父类。如薯条与汉堡
2、SqlServerUser、AccessUser / SqlServerDepartment、AccessDepartment就是用来生产不同品牌产品的流水线对象。如生产肯德基薯条、麦当劳薯条/ 肯德基汉堡、麦当劳汉堡
3、IFactory就是工厂父类,定义了createUser()和CreateDepartment()的接口,以便统一规范生产流水线的过程。
4、SqlServerFactory和AccessFactory就是各自的工厂子类,负责实例化出生产各自产品的流水线对象。如肯德基快餐店,麦当劳快餐店,负责生产各自的产品
优点:
1、抽象工厂很好地应对了产品族的场景,它最大的好处就是便于在产品系列当中进行切换,由于具体工厂类,如IFactory factory = new AccessFactory(),只在一个程序初始化的时候出现一次即可,这就使得改变一个应用的具体工厂变得非常容易,只需改变这个工厂,就可以使用不同的产品配置
2、它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操作实例,产品的具体类名也被具体的工厂实现分离,不会出现在客户的代码中。
例如:又有新的快餐店——德克士,也要生产薯条,汉堡。只需实现薯条,汉堡接口,生产自己品牌的产品,然后实现工厂接口,生成德克士快餐店
缺点:
如果我的更改不是来自于品牌(流水线)的增加,而是来自于产品的研发。例如,我要增加Project表,我不仅要增加IProject和SqlServerProject、AccessProjet,还要修改已有的IFactory、SqlServerFactory和AccessFactory,添加createProject新接口与实现。
好了,抽象工厂就先说到这里,在下一篇中,我们再说如何在抽象工厂的基础上进行优化,以便更好地提升程序的扩展与维护性。