您的位置:首页 > 其它

《大话设计模式》之--第15章 就不能不换DB吗?----抽象工厂模式

2010-07-01 15:07 381 查看

第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最基本的数据访问程序

“那你先写一段你原来的数据访问的做法给我看看。”
“那就用增加用户和得到用户为例吧。”

//用户类假设只有ID和Name两个字段,其余省略。
public class User
{
private int		id;
private String	name;

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用了工厂方法模式的数据访问程序

小菜很快给出了工厂方法实现的代码。
代码结构图



//IUser接口,用于客户端访问,解除与具体数据库访问的耦合
public interface IUser
{
void insert(User user);

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用了抽象工厂模式的数据访问程序

小菜再次修改代码,拉回了关于部门表的处理。
代码结构图



//Department类
public class Department
{
private int		id;
private String	name;

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类,用一个简单工厂模式来实现。
代码结构图



//DataAccess类
public class DataAccess
{
private static final String	db	= "Sqlserver";

public static IUser createUser()
{
IUser result = null;
if ("Sqlserver".equals(db))
{
result = new SqlServerUser();
}
else if ("Access".equals(db))
{
result = new AccessUser();
}

return result;
}

public static IDepartment createDepartment()
{
IDepartment result = null;
if ("Sqlserver".equals(db))
{
result = new SqlServerDepartment();
}
else if ("Access".equals(db))
{
result = new AccessDepartment();
}

return result;
}
}
//客户端代码
public class Main
{
public static void main(String[] args)
{
User user = new User();
Department department = new Department();

IUser iu = DataAccess.createUser();

iu.insert(user);
iu.getUser(1);

IDepartment id = DataAccess.createDepartment();

id.insert(department);
id.getDepartment(1);
}
}

“大鸟,来看看我的设计,我觉得这里与其用那么多的工厂类,不如直接用一个简单工厂来实现,我抛弃了IFactory、SqlServerFactory和AccessFactory三个工厂类,取而代之的是DataAccess类,由于事先设置了db的值(Sqlserver或Access),所以简单工厂的方法都不需要输入参数,这样在客户端就只需要DataAccess.createUser()和DataAccess.createDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL Server或Access的字样,达到了解耦合的目的。”
“小菜,你确实很厉害啊,你的改进确实是比之前的代码要更进一步了,客户端已经不需要受改动数据库访问的影响了,可以打95分。为什么不能得满分,原因是如果我需要增加Oracle数据库的访问,本来抽象工厂只增加一个OracleFactory工厂类就可以了,现在就比较麻烦了。”
“是啊,但没木办法啊,这样就需要在DataAccess类中每个方法的if分支语句里面增加了。”

15.8用反射+抽象工厂的数据访问程序

“我们要考虑的就是可不可以不在程序里写明‘如果是Sqlserver就去实例化SQL Server数据库相关的类,如果是Access就去实例化Access相关的类’这样的语句,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。这样,我们的if就可以对它说再见了。”
“听不懂啊,什么叫去哪个地方找应该要实例化的类是哪一个?”
“我要说的就是一种编程方式:依赖注入(Dependency Injection),从字面上不太好理解,我们也不去管它。关键在于如何去用这种方法来解决我们的if判断问题。本来依赖注入是需要专门的Ioc容器提供,比如Spring.NET,显然当前这个程序不需要这么麻烦,你只需要了解一个简单的‘反射’技术就可以了。”
“大鸟,你一下子说出又是依赖注入又是反射这些莫名其妙的概念,头晕。我就想知道,如何向if或者switch说byebye,至于那些什么概念我都不想了解。”
“心急讨不到好媳妇!急个毛啊?反射技术看起来很玄乎,其实实际用起来不算难。它的格式是Class.forName(className).newInstance();,只要在程序顶端写上import sun.reflect.Reflection;,就可以来引用Reflection,就可以使用反射来帮我们克服抽象工厂模式的先天不足了。”
“具体怎么做呢?”
“有了反射,我们获得实例可以用下面的两种方法。”
//常规的写法
Iuser result = new SqlServerUser();
//反射写法
public class DataAccess
{
private static final String	db			= "SqlServer";
private static String		className	= null;

public static IUser createUser()
{
className = db + "User";
try
{
return (IUser) Class.forName(className).newInstance();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
return null;
}

public static IDepartment createDepartment()
{
className = db + "Department";
try
{
return (IDepartment) Class.forName(className).newInstance();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
return null;
}
}

“实例化的效果是一样的,但这两种方法的区别在哪里?”
“常规方法是写明了要实例化SqlServerUser对象。反射的写法,其实也是指明了要实例化SqlServerUser对象。”
“常规方法你可以灵活更改为AccessUser吗?”
“不可以,这都是事先编译好的代码。”
“那你看看,在反射中可以灵活更换SqlServerUser为AccessUser吗?”
“还不是一样的,写死在代码里面,等等,啊,我明白了。happy啊,我终于看明白这个东东的意思了,因为是字符串处理,可以用变量来代替,就可以根据需要更换了。Happy~”
“你丫才看到,太让我失望了,就像我对你讲四大发明之活字印刷一样的,你现在体会到面向对象带来的好处了吧。”
“嗯,我一下子知道这里面的差别所在了,主要在原来的实例化是写死在程序里面的,但现在用了反射,就可以利用字符串来实例化对象了,而且变量是可以更换的。”
“写死在程序里,太难听了,有点专业精神好不,这叫硬编码。准确地说,是将程序由编译时置为运行时。由于反射中的字符串是可以写成变量的,而变量的值到底是SQL Server,还是Access完全可以由事件的那个db变量来决定。所以就去除了switch或if判断带来的麻烦。”
代码结构图,其中DataAccess类,用反射技术,取代IFactory、SqlServerFactory和AccessFactory。



“现在如果我们增加了Oracle数据访问,相关的类的增加是不可避免的,这点无论我们用任何办法都解决不了,不过这叫扩展,开放-封闭原则告诉我们,对于扩展,我们开放。但对于修改,我们应该要尽量关闭,就目前而言,我们只需要更改private static final String db = "SqlServer";为private static final String db = "Oracle";也就意味着(IUser) Class.forName(className).newInstance();这一句话发生了变化。”
“这样的结果就是DataAccess.createUser()本来得到的是SqlServerUser的实例,而现在变成了OracleUser的实例了。”
“那么如果我们需要增加Project产品时,如何做呢?”
“只需要增加三个与Project相关的类,再修改DataAccess,在其中增加一个public static IProject createProject()方法就可以了。”
“怎样,编程的艺术感体现出现木?”
“哈,比以前,代码漂亮多了。但总体感觉还是有缺憾,因为在更改数据库访问时,还是需要去改程序啊,改db这个字符串的值重编译,如果可以不改程序,那才是真正地符合开放-封闭原则。而且createUser()和createDepartment()的内部实现代码几乎是完全一致的嘛。”

15.9用反射+配置文件实现数据访问程序

“小菜很追求完美嘛!我们可以复用配置文件来解决更改DataAccess的问题。”
“对啊,我可以读取文件来给DB字符串赋值,在配置文件中写明是SqlServer还是Access,这样就连DataAccess类也不用更改了。”
添加一个app.properties文件。内容如下:
DB = SqlServer
//再解析app.properties来获取DB字段值。
public class DataAccess
{
private static String		DB			= null;
private static String		className	= null;
private static Properties	properties	= new Properties();

static
{
try
{
properties.load(DataAccess.class.getClassLoader()
.getResourceAsStream("config/app.properties"));
}
catch (IOException e)
{
e.printStackTrace();
}

DB = properties.getProperty("DB");
}

public static IUser createUser()
{
return (IUser) create("User");
}

public static IDepartment createDepartment()
{
return (IDepartment) create("Department");
}

public static Object create(String name)
{
className = DB + name;

try
{
return Class.forName(className).newInstance();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}

return null;
}
}

“哈哈,这下基本上可以得个满分木有啥问题了。现在我们应用了反射+抽象工厂模式解决了数据库访问时的可维护、可扩展问题。”
“从这个角度上说,所有的用简单工厂的地方,都可以考虑用反射技术消除if或switch,解除分支判断带来的耦合。”
“说的好,switch或if是程序里面的好东东,但在应对变化上,却显得老态龙钟。反射技术确实可以很好地解决它们难以应对的变化,难以维护和扩展的诟病。”

15.10无痴迷,不成功

“设计模式真的很神奇哦,如果早先是这样设计的话,我今天就用不着加班加点了。”
“好了,都快l点了,你还要不要睡觉呢?”
“啊,今天都加了一晚上的班,但学起设计模式来,我把时间都给忘记了,什么劳累都没了。”
“这就说明你是做程序员的料,一个程序员如果从来没有熬夜写程序的经历,不能算是一个好程序员,因为他没有痴迷过,所以他不会有大成就,”
“是的,无痴迷,不成功。我一定会成为优秀的程序员。我坚信。”小菜非常自信地说道。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: