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

15 就不能不换DB吗?----抽象工厂模式

15.1就不能不换DB吗?

“这么晚才回来,都11点了。”大鸟看着刚推门而入的小菜问道。

“我了个去~没办法呀,工作忙。”小菜叹气说道。

“怎么会这么忙。加班有点过头了呀。”

“都是换数据库惹的祸叹。”

“怎么了?

“我本来写好了一个项目,是给一家企业做的电子商务网站,是用SQLServer作为数据库的,应该说上线后除了开始有些小问题,基本都还可以。而后,公司接到另外一家公司类似需求的项目,但这家公司想省钱,租用了一个空间,只能用Access,不能用SQL Server,于是就要求我今天改造原来那个项目的代码。”

“哈哈,你的麻烦来了。”

“是呀,那是相当的麻烦。但开始我觉得很简单呀,因为地球人都知道,SQL ServerAccessADO.NET上的使用是不同的,在SQL Server上用的是System.Data.SqlClient命名空间下的SqlConnectianSqlCommandSqlParameter SqlDataReaderSqlDataAdapter,而Access则要用System.Data.OleDb命名空间下的相应对象,我以为只要做一个全体替换就可以了,哪知道,替换后,错误百出。”

“那是一定的,两者有不少不同的地方。你都找到了些什么问题?

“实在是多呀。在插入数据时Access必须要insert intoSQL Server可以不用into的,SQL Server中的GeDate()Access中没有,需要改成Now()SQL Server中有字符串函数Substring,而Access中根本不能用,我找了很久才知道,可以用Mid,这好像是VB中的函数口”

“小菜还真犯了不少错呀,insert into这是标准语法,你干吗不加into,这是自找的麻烦。”

“这些问题也就罢了,最气人的是程序的登录代码,老是报错,我怎么也找不到出了什么问题,搞了几个小时口最后才知道,原来Access对一些关键字,例如password是不能作为数据库的字段的,如果密码的字段名是passwordSQL Server中什么问题都没有,运行正常,在Access中就是报错,而且报得让人莫名其妙。”

“‘关键字’应该要用‘[’和‘]’包起来,不然当然是容易出错的。”

“就这样,今天加班到这时候才回来。”

“以后你还有的是班要加了。”

“为什么?

“只要网站要维护,比如修改或增加一些功能,你就得改两个项目吧,至少在数据库中做改动。相应的程序代码都要改,甚至和数据库不相干的代码也要改,你既然有两个不同的版本,两倍的工作量也是必然的。”

“是呀,如果哪一天要用Oracle数据库,估计我要改动的地方更多了。”

“那是当然,OracleSQL语法与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()所造成的变化?”

“小菜到了半夜,还是很清醒嘛,88错。工厂方法模式是定义一个用户创建对象的接口,让子类决定实例化哪个类,试试看吧。”

“中!”

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

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

代码结构图

 

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

//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 ServerAccess又是两大不同的分类,所以解决这种涉及到多个产品系列的问题,有一个专门的工厂模式叫抽象工厂模式。”

15.5抽象工厂模式

抽象工厂模式(Abstract Factory),提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

抽象工厂模式(Abstract Factory)结构图

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

AbstractProductAAbstractProductB是两个抽象产品,之所以为抽象,是因为它们都有可能有两种不同的实现,就刚才的例子来说就是UserDepartment,而ProductA1ProductA2ProductB1ProductB2就是对两个抽象产品的具体分类的实现,比如ProductA1可以理解为是SqlServerUser,而ProductB1AccessUser。”

“这么说,IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法。而ConcreteFactory1ConcreteFacotry2就是具体的工厂了。就像SqlServerFactoryAccessFactory一样。”

“理解的非常正确。通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应该使用不同的具体工厂。”

15.6抽象工厂模式的优点和缺点

“这样做有虾米好处?”

“最大好处在于易于交换产品系列,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。我们的设计不能去防止需求的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需要更改具体的工厂就可以做到了。第二大好处在于,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户端代码中。事实上,你刚才写的例子,客户端所认识的只有IUserIDepartment,至于它是用SQL Server来实现还是用Access来实现就不知道了。”

“啊,我感觉这个模式把开放-封闭原则,依赖倒转原则发挥到极致了。”

“木啦木啦,木那么夸张的说,应该说就是这些设计原则的良好运用。抽象工厂模式也有缺点。你想的出来吗?”

“想不出来,我感觉它已经很好用了,哪有什么缺点?”

“是个模式就会有缺点的,都有不适用的时候,要辩证地看待问题啊。抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果你的需求来自增加功能,比如我们现在要增加项目表Project,你要改动哪些地方?”

“啊,那就要至少增加三个类,IprojectSqlServerProjectAccessProject,还需要更改IFactorySqlServerFactoryAccessFactory才可以完全实现。啊,要改三个类,这太糟糕了的说。”

“是啊,这是非常糟糕的说。”

“还有啊,就是刚才问你的,我的客户端程序类显然不会只有一个啊,有很多地方都在使用IUserIDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactory factory = new SqlServerFactory(),如果我有100个调用数据库访问的类,是不是就要更改100IFactory factory = new AccessFactory()这样的代码才行啊?这不能解决我要更改数据库访问时,改动一处就完全更改的要求啊!”

“改就改啊,公司花那么多钱养你干嘛啊?不就是要你努力地工作吗?100个改动,不算难的,加个班,什么都搞定了。”

“球球蛋,你讲过,编程是门艺术,这样大批量地改动,显然是非常丑陋地做法。我需要地是一个非常优雅地解决方案,我来想想办法改进一下这个抽象工厂模式。”

“好,小伙子,有立场,有想法,不向丑陋地代码低头,那就等你的好消息啦。”

15.7用简单工厂来改进抽象工厂

十分钟后,小菜给出了一个改进方案。去除IFactorySqlServerFactoryAccessFactory三个工厂类,取而代之的是一个DataAccess类,用一个简单工厂模式来实现。

代码结构图

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

//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); } }

“大鸟,来看看我的设计,我觉得这里与其用那么多的工厂类,不如直接用一个简单工厂来实现,我抛弃了IFactorySqlServerFactoryAccessFactory三个工厂类,取而代之的是DataAccess类,由于事先设置了db的值(SqlserverAccess),所以简单工厂的方法都不需要输入参数,这样在客户端就只需要DataAccess.createUser()DataAccess.createDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL ServerAccess的字样,达到了解耦合的目的。”

“小菜,你确实很厉害啊,你的改进确实是比之前的代码要更进一步了,客户端已经不需要受改动数据库访问的影响了,可以打95分。为什么不能得满分,原因是如果我需要增加Oracle数据库的访问,本来抽象工厂只增加一个OracleFactory工厂类就可以了,现在就比较麻烦了。”

“是啊,但没木办法啊,这样就需要在DataAccess类中每个方法的if分支语句里面增加了。”

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

“我们要考虑的就是可不可以不在程序里写明‘如果是Sqlserver就去实例化SQL Server数据库相关的类,如果是Access就去实例化Access相关的类’这样的语句,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。这样,我们的if就可以对它说再见了。”

“听不懂啊,什么叫去哪个地方找应该要实例化的类是哪一个?”

“我要说的就是一种编程方式:依赖注入(Dependency Injection),从字面上不太好理解,我们也不去管它。关键在于如何去用这种方法来解决我们的if判断问题。本来依赖注入是需要专门的Ioc容器提供,比如Spring.NET,显然当前这个程序不需要这么麻烦,你只需要了解一个简单的‘反射’技术就可以了。”

“大鸟,你一下子说出又是依赖注入又是反射这些莫名其妙的概念,头晕。我就想知道,如何向if或者switchbyebye,至于那些什么概念我都不想了解。”

“心急讨不到好媳妇!急个毛啊?反射技术看起来很玄乎,其实实际用起来不算难。它的格式是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吗?”

“不可以,这都是事先编译好的代码。”

“那你看看,在反射中可以灵活更换SqlServerUserAccessUser吗?”

“还不是一样的,写死在代码里面,等等,啊,我明白了。happy啊,我终于看明白这个东东的意思了,因为是字符串处理,可以用变量来代替,就可以根据需要更换了。Happy~

“你丫才看到,太让我失望了,就像我对你讲四大发明之活字印刷一样的,你现在体会到面向对象带来的好处了吧。”

“嗯,我一下子知道这里面的差别所在了,主要在原来的实例化是写死在程序里面的,但现在用了反射,就可以利用字符串来实例化对象了,而且变量是可以更换的。”

“写死在程序里,太难听了,有点专业精神好不,这叫硬编码。准确地说,是将程序由编译时置为运行时。由于反射中的字符串是可以写成变量的,而变量的值到底是SQL Server,还是Access完全可以由事件的那个db变量来决定。所以就去除了switchif判断带来的麻烦。”

代码结构图,其中DataAccess类,用反射技术,取代IFactorySqlServerFactoryAccessFactory

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

“现在如果我们增加了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; } }

“哈哈,这下基本上可以得个满分木有啥问题了。现在我们应用了反射+抽象工厂模式解决了数据库访问时的可维护、可扩展问题。”

“从这个角度上说,所有的用简单工厂的地方,都可以考虑用反射技术消除ifswitch,解除分支判断带来的耦合。”

“说的好,switchif是程序里面的好东东,但在应对变化上,却显得老态龙钟。反射技术确实可以很好地解决它们难以应对的变化,难以维护和扩展的诟病。”

15.10无痴迷,不成功

“设计模式真的很神奇哦,如果早先是这样设计的话,我今天就用不着加班加点了。”

“好了,都快l点了,你还要不要睡觉呢?”

“啊,今天都加了一晚上的班,但学起设计模式来,我把时间都给忘记了,什么劳累都没了。”

“这就说明你是做程序员的料,一个程序员如果从来没有熬夜写程序的经历,不能算是一个好程序员,因为他没有痴迷过,所以他不会有大成就,”

“是的,无痴迷,不成功。我一定会成为优秀的程序员。我坚信。”小菜非常自信地说道。

你可能感兴趣的:(《大话设计模式》之--第15章 就不能不换DB吗?----抽象工厂模式)