第15章 就不能不换DB吗?----抽象工厂模式
15.1就不能不换DB吗?
“这么晚才回来,都11点了。”大鸟看着刚推门而入的小菜问道。
“我了个去~没办法呀,工作忙。”小菜叹气说道。
“怎么会这么忙。加班有点过头了呀。”
“都是换数据库惹的祸叹。”
“怎么了?”
“我本来写好了一个项目,是给一家企业做的电子商务网站,是用SQLServer作为数据库的,应该说上线后除了开始有些小问题,基本都还可以。而后,公司接到另外一家公司类似需求的项目,但这家公司想省钱,租用了一个空间,只能用Access,不能用SQL Server,于是就要求我今天改造原来那个项目的代码。”
“哈哈,你的麻烦来了。”
“是呀,那是相当的麻烦。但开始我觉得很简单呀,因为地球人都知道,SQL Server和Access在ADO.NET上的使用是不同的,在SQL Server上用的是System.Data.SqlClient命名空间下的SqlConnectian、SqlCommand、SqlParameter、 SqlDataReader、SqlDataAdapter,而Access则要用System.Data.OleDb命名空间下的相应对象,我以为只要做一个全体替换就可以了,哪知道,替换后,错误百出。”
“那是一定的,两者有不少不同的地方。你都找到了些什么问题?”
“实在是多呀。在插入数据时Access必须要insert into而SQL Server可以不用into的,SQL Server中的GeDate()在Access中没有,需要改成Now(),SQL Server中有字符串函数Substring,而Access中根本不能用,我找了很久才知道,可以用Mid,这好像是VB中的函数口”
“小菜还真犯了不少错呀,insert into这是标准语法,你干吗不加into,这是自找的麻烦。”
“这些问题也就罢了,最气人的是程序的登录代码,老是报错,我怎么也找不到出了什么问题,搞了几个小时口最后才知道,原来Access对一些关键字,例如password是不能作为数据库的字段的,如果密码的字段名是password,SQL Server中什么问题都没有,运行正常,在Access中就是报错,而且报得让人莫名其妙。”
“‘关键字’应该要用‘[’和‘]’包起来,不然当然是容易出错的。”
“就这样,今天加班到这时候才回来。”
“以后你还有的是班要加了。”
“为什么?”
“只要网站要维护,比如修改或增加一些功能,你就得改两个项目吧,至少在数据库中做改动。相应的程序代码都要改,甚至和数据库不相干的代码也要改,你既然有两个不同的版本,两倍的工作量也是必然的。”
“是呀,如果哪一天要用Oracle数据库,估计我要改动的地方更多了。”
“那是当然,Oracle的SQL语法与SQL Server的差别更大。你的改动将是空前的。”
“大鸟只会夸张,哪有这么严重,大不了再加两天班就什么都搞定了。”
“哼”,大鸟笑着摇了摇头,很不屑一顾,“菜鸟程序员碰到问颐,只会用时间来摆平,所以即使整天加班,老板也不想给菜鸟加工资,原因就在于此。”
“你什么意思嘛!”小菜气道,“我是菜鸟我怕谁。”接着又拉了拉大鸟,“那你说怎么搞定才是好呢?”
“知道求我啦,”大鸟端起架子,“教你可以,这一周的碗你洗。”
“行,”小菜很爽快地答应道,“在家洗碗也比加班熬夜强。”
15.2最基本的数据访问程序
“那你先写一段你原来的数据访问的做法给我看看。”
“那就用增加用户和得到用户为例吧。”
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
}
//SqlServerUser类,用于操作User表,假设只有新增用户和得到用户的方法,其余方法以及具体SQL语句省略。
public class SqlServerUser
{
public void insert(User user)
{
System.out.println("在SQL Server中给User表增加一条记录");
}
public User getUser(int id)
{
System.out.println("在SQL Server中根据ID得到User表一条记录");
return null;
}
}
//客户端代码
public class Main
{
public static void main(String[] args)
{
User user = new User();
SqlServerUser su = new SqlServerUser();
su.insert(user);
su.getUser(1);
}
}
“我最开始就是这样写的,非常简单。”
“这里之所以不能换数据库,原因就在于SqlServerUser su = new SqlServerUser()使得su这个对象被框死在了SQL Server上了。如果这里是灵活的,专业点的说法,是多态的,那么在执行su.insert(user)和su.getUser(1)时就不用考虑是在用SQL Server还是在用Access。”
“你的意思我明白,你是希望我用工厂方法模式来封装new SqlServerUser()所造成的变化?”
“小菜到了半夜,还是很清醒嘛,8错8错。工厂方法模式是定义一个用户创建对象的接口,让子类决定实例化哪个类,试试看吧。”
“中!”
15.3用了工厂方法模式的数据访问程序
小菜很快给出了工厂方法实现的代码。
代码结构图
User getUser(int id);
}
//SqlServerUser类,用于访问SQL Server的User
public class SqlServerUser implements IUser
{
public void insert(User user)
{
System.out.println("在SQL Server中给User表增加一条记录");
}
public User getUser(int id)
{
System.out.println("在SQL Server中根据ID得到User表一条记录");
return null;
}
}
//AccessUser类,用于访问Access的User
public class AccessUser implements IUser
{
public void insert(User user)
{
System.out.println("在Access中给User表增加一条记录");
}
public User getUser(int id)
{
System.out.println("在Access中根据ID得到User表一条记录");
return null;
}
}
//IFactory接口,定义一个创建访问User表对象的抽象的工厂接口
public interface IFactory
{
IUser createUser();
}
//SqlServerFactory类,实现IFactory接口,实例化SqlServerUser
public class SqlServerFactory implements IFactory
{
public IUser createUser()
{
return new SqlServerUser();
}
}
//AccessFactory类,实现IFactory接口,实例化AccessUser
public class AccessFactory implements IFactory
{
public IUser createUser()
{
return new AccessUser();
}
}
//客户端代码
public class Main
{
public static void main(String[] args)
{
User user = new User();
IFactory factory = new SqlServerFactory();
IUser iu = factory.createUser();
iu.insert(user);
iu.getUser(1);
}
}
“大鸟,来看看这样写成不?”
“非常好。现在如果要换数据库,只需要把new SqlServerFactory()改成new AccessFactory(),此时由于多态的关系,使得声明IUser接口的对象iu事先根本不知道是在访问哪个数据库,却可以在运行时很好地完成工作,这就是所谓的业务逻辑与数据访问的解耦。”
“但是,大鸟,这样写,代码里还是有指明new SqlServerFactory()啊,我要改的地方,依然很多。”
“这个先不急,待会再说,问题还没有完全解决,你的数据库里面不可能只有一个User表吧,很可能有其他表,比如增加部门表(Department表),此时如何办呢?”
“啊,我觉得那要增加好多的类了,我来试试看。”
“多写些类有什么关系,只要能增加灵活性,以后就不用加班了。小菜好好加油。”
15.4用了抽象工厂模式的数据访问程序
小菜再次修改代码,拉回了关于部门表的处理。
代码结构图
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
}
//IDepartment接口,用于客户端访问,解除与具体数据库访问的耦合
public interface IDepartment
{
void insert(Department department);
Department getDepartment(int id);
}
//SqlServerDepartment类,用于访问SQL Server的Department
public class SqlServerDepartment implements IDepartment
{
public void insert(Department department)
{
System.out.println("在SQL Server中给Deaprtment表增加一条记录");
}
public Department getDepartment(int id)
{
System.out.println("在SQL Server中根据ID得到Deaprtment表一条记录");
return null;
}
}
//AccessDepartment类,用于访问Access的Department
public class AccessDepartment implements IDepartment
{
public void insert(Department department)
{
System.out.println("在Access中给Deaprtment表增加一条记录");
}
public Department getDepartment(int id)
{
System.out.println("在Access中根据ID得到Deaprtment表一条记录");
return null;
}
}
//IFactory接口,定义一个创建访问User表对象的抽象工厂接口
public interface IFactory
{
IUser createUser();
IDepartment createDepartment();
}
//SqlServerFactory类,实现IFactory接口,实例化SqlServerUser和SqlServerDepartment
public class SqlServerFactory implements IFactory
{
public IUser createUser()
{
return new SqlServerUser();
}
public IDepartment createDepartment()
{
return new SqlServerDepartment();
}
}
//AccessFactory类,实现IFactory接口,实例化AccessUser和AccessDepartment
public class AccessFactory implements IFactory
{
public IUser createUser()
{
return new AccessUser();
}
public IDepartment createDepartment()
{
return new AccessDepartment();
}
}
//客户端代码
public class Main
{
public static void main(String[] args)
{
User user = new User();
Department department = new Department();
// IFactory factory = new SqlServerFactory();
IFactory factory = new AccessFactory();
IUser iu = factory.createUser();
iu.insert(user);
iu.getUser(1);
IDepartment id = factory.createDepartment();
id.insert(department);
id.getDepartment(1);
}
}
结果显示:
在Access中给User表增加一条记录
在Access中根据ID得到User表一条记录
在Access中给Deaprtment表增加一条记录
在Access中根据ID得到Deaprtment表一条记录
“大鸟,这样就可以了,只需要改IFactory factory = new AccessFactory()为IFactory factory = new SqlServerFactory(),就可以实现了数据库访问的切换了。”
“很好嘛,实际上,在不知不觉间,你已经通过需求的不断演化,重构出了一个非常重要的设计模式。”
“这不就是刚才的工厂方法模式吗?”
“只有一个User类和User操作类的时候,是只需要工厂方法模式的,但现在显然你的数据库中有很多的表,而SQL Server与Access又是两大不同的分类,所以解决这种涉及到多个产品系列的问题,有一个专门的工厂模式叫抽象工厂模式。”
15.5抽象工厂模式
抽象工厂模式(Abstract Factory),提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂模式(Abstract Factory)结构图
“AbstractProductA和AbstractProductB是两个抽象产品,之所以为抽象,是因为它们都有可能有两种不同的实现,就刚才的例子来说就是User和Department,而ProductA1、ProductA2和ProductB1、ProductB2就是对两个抽象产品的具体分类的实现,比如ProductA1可以理解为是SqlServerUser,而ProductB1是AccessUser。”
“这么说,IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法。而ConcreteFactory1和ConcreteFacotry2就是具体的工厂了。就像SqlServerFactory和AccessFactory一样。”
“理解的非常正确。通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应该使用不同的具体工厂。”
15.6抽象工厂模式的优点和缺点
“这样做有虾米好处?”
“最大好处在于易于交换产品系列,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。我们的设计不能去防止需求的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需要更改具体的工厂就可以做到了。第二大好处在于,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户端代码中。事实上,你刚才写的例子,客户端所认识的只有IUser和IDepartment,至于它是用SQL Server来实现还是用Access来实现就不知道了。”
“啊,我感觉这个模式把开放-封闭原则,依赖倒转原则发挥到极致了。”
“木啦木啦,木那么夸张的说,应该说就是这些设计原则的良好运用。抽象工厂模式也有缺点。你想的出来吗?”
“想不出来,我感觉它已经很好用了,哪有什么缺点?”
“是个模式就会有缺点的,都有不适用的时候,要辩证地看待问题啊。抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果你的需求来自增加功能,比如我们现在要增加项目表Project,你要改动哪些地方?”
“啊,那就要至少增加三个类,Iproject、SqlServerProject、AccessProject,还需要更改IFactory、SqlServerFactory和AccessFactory才可以完全实现。啊,要改三个类,这太糟糕了的说。”
“是啊,这是非常糟糕的说。”
“还有啊,就是刚才问你的,我的客户端程序类显然不会只有一个啊,有很多地方都在使用IUser或IDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactory factory = new SqlServerFactory(),如果我有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new AccessFactory()这样的代码才行啊?这不能解决我要更改数据库访问时,改动一处就完全更改的要求啊!”
“改就改啊,公司花那么多钱养你干嘛啊?不就是要你努力地工作吗?100个改动,不算难的,加个班,什么都搞定了。”
“球球蛋,你讲过,编程是门艺术,这样大批量地改动,显然是非常丑陋地做法。我需要地是一个非常优雅地解决方案,我来想想办法改进一下这个抽象工厂模式。”
“好,小伙子,有立场,有想法,不向丑陋地代码低头,那就等你的好消息啦。”
15.7用简单工厂来改进抽象工厂
十分钟后,小菜给出了一个改进方案。去除IFactory、SqlServerFactory和AccessFactory三个工厂类,取而代之的是一个DataAccess类,用一个简单工厂模式来实现。
代码结构图
public static IUser createUser()
{
IUser result = null;
if ("Sqlserver".equals(db))
{
result = new SqlServerUser();
}
else if ("Access".equals(db))
{
result = new AccessUser();
}
return result;
}