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

发布于:2024-04-09 ⋅ 阅读:(92) ⋅ 点赞:(0)

1.1 就不能不换DB吗?

        都是换数据库惹的祸。

        "我们团队前段时间用.net的C#来开发好一个项目,是给一家企业做的电子商务网站,是用SQL Server作为数据库的,应该说上线后除了开始有些小问题,基本都还可以。而后,公司接到另外一家公司类似需求的项目,但这家公司想省钱,租用了一个空间,只能用Access,不能用SQL Server,于是就要求我今天改造原来那个项目的代码。"
        "C#与Java差不多,这不是重点,但换数据库远远没有我想得那么简单。"
        "哈哈,你的麻烦来了。"
        "是呀,那是相当的麻烦。但开始我觉得很简单呀,因为SQL Server和Access在ADO.NET上的使用是不同的,在SQL Server上用的是System.Data.SqlClient命名空间下的SqlConnection、SqlCommand、SqlParameter、SqlDataReader、SqlDataAdapter,而Access则要用System.Data.OleDb命名空间下的相应对象,我以为只要做一个全体替换就可以了,哪知道,替换后,错误百出。"
注:以上为.net框架上的术语,不了解并不影响阅读,只要知道数据库之间调用代码相差很大即可
        "那是一定的,两者有不少不同的地方。你都找到了些什么问题?"
        "实在是多呀。在插入数据时Access必须要insert into而SQL Server可以不用into的;SQL Server中的GetDate()在Access中没有,需要改成Now();SQL Server中有字符串函数Substring,而Access中根本不能用,我找了很久才知道,可以用Mid,这好像是VB中的函数。"
        "insert into这是标准语法,你干吗不加into,这是自找的麻烦。"
        "这些问题也就罢了,最气人的是程序的登录代码,老是报错,我怎么也找不到出了什么问题,搞了几个小时。最后才知道,原来Access对一些关键字,例如password是不能作为数据库的字段的,如果密码的字段名是password,SQL Server中什么问题都没有,运行正常,在Access中就是报错,而且报得让人莫名其妙。"
        "'关键字'应该要用'['和']'包起来,不然当然是容易出错的。"
        "就这样,今天加班到这时候才回来。"
        "以后你还有的是班要加了。"
        "为什么?"
        "只要网站要维护,比如修改或增加一些功能,你就得改两个项目吧,至少在数据库中做改动,相应的程序代码都要改,甚至和数据库不相干的代码也要改,你既然有两个不同的版本,两倍的工作量也是必然的。"
        "是呀,如果哪一天要用MySQL或者Oracle数据库,估计我要改动的地方更多了。"
        "那是当然,MySQL、Oracle的SQL语法与SQL Server的差别更大。你的改动将是空前的。"
        "哪有这么严重,大不了再加两天班就什么都搞定了。"
        "菜鸟程序员碰到问题,只会用时间来摆平,所以即使整天加班,老板也不想给菜鸟加工资,原因就在于此。"

1.2 最基本的数据访问程序

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

        用户类,假设只有ID和Name两个字段,其余省略。

package code.chapter15.abstractfactory1;

//用户类
public class User {

    //用户ID
    private int _id;
    public int getId(){
        return this._id;
    }
    public void setId(int value){
        this._id=value;
    }

    //用户姓名
    private String _name;
    public String getName(){
        return this._name;
    }
    public void setName(String value){
        this._name=value;
    }
    
}

        SqlserverUser类——用于操作User表,假设只有"新增用户"和"得到用户"方法,其余方法以及具体的SQL语句省略。

package code.chapter15.abstractfactory1;

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

package code.chapter15.abstractfactory1;

public class Test {

	public static void main(String[] args){

		System.out.println("**********************************************");		
		System.out.println("《大话设计模式》代码样例");
		System.out.println();		

        User user = new User();
        
        SqlserverUser su = new SqlserverUser();

        su.insert(user);    //新增一个用户
        su.getUser(1);      //得到用户ID为1的用户信息

		System.out.println();
		System.out.println("**********************************************");

	}
}

        "这里之所以不能换数据库,原因就在于SqlserverUser su = new SqlserverUser()使得su这个对象被框死在SQL Server上了。你可能会说,是因为取名叫SqlserverUser,但即使没有Sqlserver名称,它本质上也是在使用SQL Server的SQL语句代码,确实存在耦合。如果这里是灵活的,专业点的说法,是多态的,那么在执行'su.insert(user);'和'su.getUser(1);'时就不用考虑是在用SQL Server还是在用Access。"
        "我明白你的意思了,你是希望我用'工厂方法模式'来封装new SqlserverUser()所造成的变化?"

        工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。

1.3 用了工厂方法模式的数据访问程序

代码结构图

IUser接口:用于客户端访问,解除与具体数据库访问的耦合。

package code.chapter15.abstractfactory2;

//用户类接口
public interface IUser {

    public void insert(User user);

    public User getUser(int id);
}


SqlserverUser类:用于访问SQL Server的User。

package code.chapter15.abstractfactory2;

//用户类接口
public interface IUser {

    public void insert(User user);

    public User getUser(int id);
}


AccessUser类:用于访问Access的User。

package code.chapter15.abstractfactory2;

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表对象的抽象的工厂接口。

package code.chapter15.abstractfactory2;

//工厂接口
public interface IFactory {

    public IUser createUser();
    
}


SqlServerFactory类:实现IFactory接口,实例化SqlserverUser。

package code.chapter15.abstractfactory2;

//Sqlserver工厂
public class SqlserverFactory implements IFactory {

    public IUser createUser(){
        return new SqlserverUser();
    }
    
}


AccessFactory类:实现IFactory接口,实例化AccessUser。

package code.chapter15.abstractfactory2;

//Access工厂
public class AccessFactory implements IFactory {

    public IUser createUser(){
        return new AccessUser();
    }

}

package code.chapter15.abstractfactory2;

public class Test {

	public static void main(String[] args){

		System.out.println("**********************************************");		
		System.out.println("《大话设计模式》代码样例");
		System.out.println();		

        User user = new User();
        
        IFactory factory = new SqlserverFactory();

        IUser iu = factory.createUser();

        iu.insert(user);    //新增一个用户
        iu.getUser(1);      //得到用户ID为1的用户信息

        IFactory factory2 = new AccessFactory();
        IUser iu2 = factory2.createUser();

        iu2.insert(user);    //新增一个用户
        iu2.getUser(1);      //得到用户ID为1的用户信息

		System.out.println();
		System.out.println("**********************************************");

	}
}

        现在如果要换数据库,只需要把new SqlServerFactory()改成new AccessFactory(),此时由于多态的关系,使得声明IUser接口的对象iu事先根本不知道是在访问哪个数据库,却可以在运行时很好地完成工作,这就是所谓的业务逻辑与数据访问的解耦。

        这样写,代码里还是有指明'new SqlServerFactory()'呀,我要改的地方,依然很多。

        问题没有完全解决,你的数据库里不可能只有一个User表吧,很可能有其他表,比如增加部门表(Department表),此时如何办呢?

package code.chapter15.abstractfactory3;

//部门类
public class Department {

    //部门ID
    private int _id;
    public int getId(){
        return this._id;
    }
    public void setId(int value){
        this._id=value;
    }

    //部门名称
    private String _name;
    public String getName(){
        return this._name;
    }
    public void setName(String value){
        this._name=value;
    }
    
}


1.4 用了抽象工厂模式的数据访问程序

IDepartment接口:用于客户端访问,解除与具体数据库访问的耦合。

package code.chapter15.abstractfactory3;

//部门类接口
public interface IDepartment {

    public void insert(Department department);

    public Department getDepartment(int id);
}


SqlserverDepartment类:用于访问SQL Server的Department。

package code.chapter15.abstractfactory3;

public class SqlserverDepartment implements IDepartment {

    //新增一个部门
    public void insert(Department department){
        System.out.println("在SQL Server中给Department表增加一条记录");     
    }

    //获取一个部门信息
    public Department getDepartment(int id){
        System.out.println("在SQL Server中根据部门ID得到Department表一条记录");   
        return null;  
    }
}



AccessDepartment类:用于访问Access的Department。

package code.chapter15.abstractfactory3;

public class AccessDepartment implements IDepartment {

    //新增一个部门
    public void insert(Department department){
        System.out.println("在Access中给Department表增加一条记录");     
    }

    //获取一个部门信息
    public Department getDepartment(int id){
        System.out.println("在Access中根据部门ID得到Department表一条记录");   
        return null;  
    }

}


IFactory接口:定义一个创建访问Department表对象的抽象的工厂接口。

package code.chapter15.abstractfactory3;

//工厂接口
public interface IFactory {

    public IUser createUser();

    public IDepartment createDepartment();
    
}


SqlServerFactory类:实现IFactory接口,并实例化SqlserverUser和SqlserverDepartment。

package code.chapter15.abstractfactory3;

//Sqlserver工厂
public class SqlserverFactory implements IFactory {

    public IUser createUser(){
        return new SqlserverUser();
    }
    
    public IDepartment createDepartment(){
        return new SqlserverDepartment();
    }
    
}


AccessFactory类:实现IFactory接口,实例化AccessUser和AccessDepartment。

package code.chapter15.abstractfactory3;

//Access工厂
public class AccessFactory implements IFactory {

    public IUser createUser(){
        return new AccessUser();
    }

    public IDepartment createDepartment(){
        return new AccessDepartment();
    }

}


package code.chapter15.abstractfactory3;

public class Test {

	public static void main(String[] args){

		System.out.println("**********************************************");		
		System.out.println("《大话设计模式》代码样例");
		System.out.println();		

        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);      //得到用户ID为1的用户信息

        IDepartment idept = factory.createDepartment();
        idept.insert(department);    //新增一个部门
        idept.getDepartment(2);      //得到部门ID为2的用户信息

        

		System.out.println();
		System.out.println("**********************************************");

	}
}

        这样就可以做到,只需更改IFactory factory = new SqlServerFactory()为IFactory factory = new AccessFactory(),就实现了数据库访问的切换了。

        很好,实际上,在不知不觉间,你已经通过需求的不断演化,重构出了一个非常重要的设计模式。刚才不就是工厂方法模式吗?只有一个User类和User操作类的时候,是只需要工厂方法模式的,但现在显然你数据库中有很多的表,而SQL Server与Access又是两大不同的分类,所以解决这种涉及多个产品系列的问题,有一个专门的工厂模式叫抽象工厂模式。

1.5 抽象工厂模式

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

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

        "AbstractProductA和AbstractProductB是两个抽象产品,之所以为抽象,是因为它们都有可能有两种不同的实现,就刚才的例子来说就是User和Department,而ProductA1、ProductA2和ProductB1、ProductB2就是对两个抽象产品的具体分类的实现,比如ProductA1可以理解为是SqlserverUser,而ProductB1是SqlserverDepartment。"
        "这么说,IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。就像SqlserverFactory和AccessFactory一样。"
        "理解得非常正确。通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。"

1.6 抽象工厂模式的特点

        "这样做的好处是什么呢?"
        "最大的好处便是易于交换产品系列,由于具体工厂类,例如IFactory factory =new AccessFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。我们的设计不能去防止需求的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需要更改具体工厂就可以做到。第二大好处是,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。事实上,你刚才写的例子,客户端所认识的只有IUser和IDepartment,至于它是用SQL Server来实现还是Access来实现就不知道了。"
        "啊,我感觉这个模式把开放-封闭原则、依赖倒转原则发挥到极致了。
        "没这么夸张,应该说就是这些设计原则的良好运用。抽象工厂模式也有缺点。你想得出来吗?"
        "想不出来,我觉得它已经很好用了,哪有什么缺点?"
        "是个模式都是会有缺点的,都有不适用的时候,要辩证地看待问题哦。抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果你的需求来自增加功能,比如我们现在要增加项目表Project,你需要改动哪些地方?"
        "啊,那就至少要增加三个类,IProjectSqlserverProjectAccessProject,还需要更改IFactorySqlserverFactoryAccessFactory才可以完全实现。啊,要改三个类,这太糟糕了。"
        "是的,这非常糟糕。"
        "还有就是刚才问你的,我的客户端程序类显然不会是只有一个,有很多地方都在使用IUserIDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactory factory = new SqlserverFactory(),如果我有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new AccessFactory()这样的代码才行?这不能解决我要更改数据库访问时,改动一处就完全更改的要求呀!"
        "改就改啰,公司花这么多钱养你干吗?不就是要你努力工作吗。100个改动,不算难的,加个班,什么都搞定了。"
        "不可能,你讲过,编程是门艺术,这样大批量的改动,显然是非常丑陋的做法。一定有更好的办法。"我来想想办法改进一下这个抽象工厂。"
"好,小伙子,有立场,有想法,不向丑陋代码低头,那就等你的好消息。"

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

        去除IFactory、SqlserverFactory和AccessFactory三个工厂类,取而代之的是DataAccess类,用一个简单工厂模式来实现。

代码结构图

package code.chapter15.abstractfactory4;

public class DataAccess {

    private static String db = "Sqlserver";//数据库名称,可替换成Access
    //private static String db ="Access";

    //创建用户对象工厂
    public static IUser createUser(){
        IUser result = null;
        switch(db){
            case "Sqlserver":
                result = new SqlserverUser();
                break;
            case "Access":
                result = new AccessUser();
                break;
        }
        return result;
    }

    //创建部门对象工厂
    public static IDepartment createDepartment(){
        IDepartment result = null;
        switch(db){
            case "Sqlserver":
                result = new SqlserverDepartment();
                break;
            case "Access":
                result = new AccessDepartment();
                break;
        }
        return result;
    }
    
}
package code.chapter15.abstractfactory4;

public class Test {

	public static void main(String[] args){

		System.out.println("**********************************************");		
		System.out.println("《大话设计模式》代码样例");
		System.out.println();		

        User user = new User();
        Department department = new Department();
        
        //直接得到实际的数据库访问实例,而不存在任何依赖
        IUser iu = DataAccess.createUser();
        iu.insert(user);    //新增一个用户
        iu.getUser(1);      //得到用户ID为1的用户信息

        //直接得到实际的数据库访问实例,而不存在任何依赖
        IDepartment idept = DataAccess.createDepartment();
        idept.insert(department);    //新增一个部门
        idept.getDepartment(2);      //得到部门ID为2的用户信息

		System.out.println();
		System.out.println("**********************************************");

	}
}

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

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

        "我们要考虑的就是可不可以不在程序里写明'如果是Sqlserver就去实例化SQL Server数据库相关类,如果是Access就去实例化Access相关类'这样的语句,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。这样,我们的switch就可以对它说再见了。"
        "听不太懂哦,什么叫'去某个地方找应该要实例化的类是哪一个'?
        "我要说的就是一种编程方式:依赖注入(Dependency Injection),从字面上不太好理解,我们也不去管它。关键在于如何去用这种方法来解决我们的switch问题。本来依赖注入是需要专门的IoC容器提供,比如Spring,显然当前这个程序不需要这么麻烦,你只需要再了解一个简单的Java技术'反射'就可以了。"
        "你一下子说出又是'依赖注入'又是'反射'这些莫名其妙的名词,很晕。"我就想知道,如何向switch说bye-bye!至于那些什么概念我不想了解。"
        "心急讨不了好媳妇!你急什么?"反射技术看起来很玄乎,其实实际用起来不算难。它的格式是:

Object result = Class.forName(className).getDeclaredConstructor().newInstance();

        这样使用反射来帮我们克服抽象工厂模式的先天不足。"
        "具体怎么做呢?快说快说。
        "有了反射,我们获得实例可以用下面两种写法。"

//常规的写法
IUser result = new SqlserverUser();

//反射的写法
IUser result = (IUser)Class.forName("code.chapter15.abstractfactory5.SqlserverUser")
                           .getDeclaredConstructor().newInstance();

        "实例化的效果是一样的,但这两种方法的区别在哪里?"
        "常规方法是写明了要实例化SqlserverUser对象。反射的写法,其实也是指明了要实例化SqlserverUser对象呀。"
        "常规方法你可以灵活更换为AccessUser吗?"
        "不可以,这都是事先编译好的代码。"
        "那你看看,在反射中'Class.forName("code.chapter15.abstractfactory5.SqlserverUser").getDeclaredConstructor().newInstance();',可以灵活更换'SqlserverUser'为'AccessUser'吗?"
        "还不是一样,写死在代码……等等,哦!!!我明白了。""因为这里是字符串,可以用变量来处理,也就可以根据需要更换。哦,My God!太妙了!"
        "哈哈,我以前对你讲四大发明之活字印刷时,曾说过'体会到面向对象带来的好处,那种感觉应该就如同是一中国酒鬼第一次喝到了茅台,西洋酒鬼第一次喝到了XO一样,怎个爽字可形容呀',你有没有这种感觉了?"
        "嗯,我一下子知道这里的差别主要在原来的实例化是写死在程序里的,而现在用了反射就可以利用字符串来实例化对象,而变量是可以更换的。"
        "写死在程序里,太难听了。准确地说,是将程序由编译时转为运行时。由于'Class.forName("包名。类名").getDeclaredConstructor().newInstance();'中的字符串是可以写成变量的,而变量的值到底是Sqlserver,还是Access,完全可以由事先的那个db变量来决定。所以就去除了switch判断的麻烦。"
        DataAccess类,用反射技术,取代IFactory、SqlserverFactory和AccessFactory。

package code.chapter15.abstractfactory5;

import java.lang.reflect.InvocationTargetException;
public class DataAccess {
    private static String assemblyName = "code.chapter15.abstractfactory5.";
    private static String db ="Sqlserver";//数据库名称,可替换成Access

    //创建用户对象工厂
    public static IUser createUser() {
        return (IUser)getInstance(assemblyName + db + "User");
    }
    //创建部门对象工厂
    public static IDepartment createDepartment(){
        return (IDepartment)getInstance(assemblyName + db + "Department");
    }
    private static Object getInstance(String className){
        Object result = null;
        try{
            result = Class.forName(className).getDeclaredConstructor().newInstance();
        }
        catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch (InstantiationException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }
}

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

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

        "我们还可以利用配置文件来解决更改DataAccess的问题。"
        "哦,对的,对的,我可以读文件来给DB字符串赋值,在配置文件中写明是Sqlserver还是Access,这样就连DataAccess类也不用更改了。"
        添加一个db.properties文件,内容如下。

db=Sqlserver

        再更改DataAccess类,添加与读取文件内容相关的包。

package code.chapter15.abstractfactory6;

import java.lang.reflect.InvocationTargetException;  

//与读文件内容相关的包 
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;                                       

public class DataAccess {

    private static String assemblyName = "code.chapter15.abstractfactory6.";
    
    public static String getDb() {
        String result="";
        try{
            Properties properties = new Properties();
            //编译后,请将db.properties文件复制到要编译的class目录中,并确保下面path路径与
            //实际db.properties文件路径一致。否则会报No such file or directory错误
            String path=System.getProperty("user.dir")+"/code/chapter15/abstractfactory6/db.properties";
            System.out.println("path:"+path);            
            BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
            properties.load(bufferedReader);
            result = properties.getProperty("db");
        }
        catch(IOException e){
            e.printStackTrace();
        }
        return result;
    }

    //创建用户对象工厂
    public static IUser createUser() {
        String db=getDb();
        return (IUser)getInstance(assemblyName + db + "User");
    }

    //创建部门对象工厂
    public static IDepartment createDepartment(){
        String db=getDb();
        return (IDepartment)getInstance(assemblyName + db + "Department");
    }

    private static Object getInstance(String className){
        Object result = null;
        try{
            result = Class.forName(className).getDeclaredConstructor().newInstance();
        }
        catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch (InstantiationException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }
    
}

        "将来要更换数据库,根本无须重新编译任何代码,只需要更改配置文件就好了。这下基本可以算是满分了,现在我们应用了反射+抽象工厂模式解决了数据库访问时的可维护、可扩展的问题。"


        "从这个角度上说,所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。"
        "说得没错,switch或者if是程序里的好东西,但在应对变化上,却显得老态龙钟。反射技术的确可以很好地解决它们难以应对变化,难以维护和扩展的诟病。"

1.10 商场收银程序再再升级

        "还记得我们在策略模式、装饰模式、工厂方法模式都学习过的商场收银程序吗?"
        "我记得,当时做了很多次的重构升级,感觉代码的可维护、可扩展能力都提高很多很多。"
        "今天我们学习了反射,你想想看,那个代码,还有重构的可能性吗?"
        "呃!我想想看。原来的CashContext是有一个长长的switch,这是可以用反射来解决的。"

        经过一定时间的思考,对代码改进如下:
        首先要制作一个可以很容易修改的文本配置文件data.properties,将它放在编译的.class同一目录下。


strategy1=CashRebateReturnFactory,1d,0d,0d
strategy2=CashRebateReturnFactory,0.8d,0d,0d
strategy3=CashRebateReturnFactory,0.7d,0d,0d
strategy4=CashRebateReturnFactory,1d,300d,100d
strategy5=CashRebateReturnFactory,0.8d,300d,100d
strategy6=CashReturnRebateFactory,0.7d,200d,50d



        修改CashContext类。
        先修改构造方法,此时已经没有了长长的switch,直接读文件配置即可。
        增加两个函数,一个用来读配置文件,一个通过反射生成实例。 

package code.chapter15.abstractfactory7;

import java.lang.reflect.InvocationTargetException; 
//与读文件内容相关的包 
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;   

public class CashContext {

    private static String assemblyName = "code.chapter15.abstractfactory7.";
    
    private ISale cs;   //声明一个ISale接口对象
    
    //通过构造方法,传入具体的收费策略
    public CashContext(int cashType){
        
        String[] config = getConfig(cashType).split(",");

        IFactory fs=getInstance(config[0],
                                Double.parseDouble(config[1]),
                                Double.parseDouble(config[2]),
                                Double.parseDouble(config[3]));

        this.cs = fs.createSalesModel();
    }

    //通过文件得到销售策略的配置文件
    private String getConfig(int number) {
        String result="";
        try{
            Properties properties = new Properties();
            String path=System.getProperty("user.dir")+"/code/chapter15/abstractfactory7/data.properties";
            System.out.println("path:"+path);            
            BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
            properties.load(bufferedReader);
            result = properties.getProperty("strategy"+number);
        }
        catch(IOException e){
            e.printStackTrace();
        }
        return result;
    }

    //根据配置文件获得相关的对象实例
    private IFactory getInstance(String className,double a,double b,double c){
        IFactory result = null;
        try{
            result = (IFactory)Class.forName(assemblyName+className)
                                    .getDeclaredConstructor(new Class[]{double.class,double.class,double.class})
                                    .newInstance(new Object[]{a,b,c});  
        }
        catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch (InstantiationException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }


    public double getResult(double price,int num){
        //根据收费策略的不同,获得计算结果
        return this.cs.acceptCash(price,num);
    }    
}


         "此时,我们如果需要更改销售策略,不再需要去修改代码了,只需要去改data.properties文件即可。我们的每个代码都尽量做到了'向修改关闭,向扩展开放'。"

1.11 无痴迷,不成功

        "设计模式真的很神奇哦,如果早先这样设计,我今天就用不着加班加点了。""这就说明你是做程序员的料,一个程序员如果从来没有熬夜写程序的经历,不能算是一个好程序员,因为他没有痴迷过,所以他不会有大成就。"
"是的,无痴迷,不成功。我一定会成为优秀的程序员。我坚信.


网站公告

今日签到

点亮在社区的每一天
去签到