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

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 无痴迷,不成功

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/521771.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

政安晨:【Keras机器学习实践要点】(十八)—— 利用视觉转换器进行图像分类

目录 简介 设置 准备数据 配置超参数 使用数据增强 实施多层感知器(MLP) 将创建修补程序作为一个层 实施补丁编码层 建立 ViT 模型 编译、培训和评估模式 政安晨的个人主页:政安晨 欢迎 👍点赞✍评论⭐收藏 收录专栏: T…

Android源码笔记-输入事件(二)

这一节主要了解输入事件的获取,InputReaderThread继承自C的Thread类,Thread类封装了pthread线程工具,提供了与Java层Thread类相似的API。C的Thread类提供了一个名为threadLoop()的纯虚函数,当线程开始运行后,将会在内建…

【Linux实践室】Linux高级用户管理实战指南:创建与删除用户组操作详解

🌈个人主页:聆风吟_ 🔥系列专栏:Linux实践室、网络奇遇记 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 🔔Linux创建用户组命令2.1.1 知识点讲解2.1.2…

Arduino的OTA在线升级

一、OTA 介绍 OTA是Over-the-Air的缩写,中文意思是空中下载技术。通过移动通信(GSM或CDMA)的空中接口对SIM卡数据及应用进行远程管理的技术。空中接口可以采用WAP、GPRS、CDMA1X及短消息技术。OTA技术的应用,使得移动通信不仅可以…

读所罗门的密码笔记12_群雄逐鹿(上)

1. 国际电信规则 1.1. 美国坚持互联网自由和极少的内容限制,这一立场肯定会遭到许多国家的反对 1.2. 除去两个各方针锋相对、无法妥协的议题,比如内容限制规定,实际上所有国家都已在打击垃圾邮件和常见网络安全威胁方…

Windows Server 2012 R2安装Web服务器IIS

文章目录 一、打开【服务器管理器】二、点击【添加角色和功能】三、点击【下一步】四、点击【下一步】五、点击【下一步】六、勾选【Web服务器(IIS)】→点击【添加功能】→点击【下一步】七、勾选【.NET Framework 3.5 功能】→点击【下一步】八、点击【下一步】九、点击【下一…

基于H2O AutoML与集成学习策略的房屋售价预测模型研究与实现

项目简述: 本项目采用H2O AutoML工具,针对加州房屋销售价格预测问题进行了深入研究与建模。项目以Kaggle提供的加州房屋 交易数据集为基础,通过数据清洗、特征工程、模型训练与评估等步骤,构建了一种基于集成学习策略的房价预测模…

LeetCode刷题记(二):31~60题

31. 下一个排列 整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。 例如,arr [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。 整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。…

如何确认RID池是否耗尽,以及手动增加RID池大小

确认RID池是否耗尽: 事件查看器: 在RID主控域控制器上打开事件查看器,导航至“Windows日志 > 应用程序和服务日志 > Microsoft > Windows > Directory Service > Operations”。搜索事件ID 16656和16657。事件ID 16656表明RID…

C++超高精度计时器

#include <chrono> #include <QDebug> #pragma execution_character_set("utf-8") /*1 秒&#xff08;s&#xff09; 1,000 毫秒&#xff08;ms&#xff09; 1,000,000 微秒&#xff08;μs&#xff09; 1,000,000,000 纳秒&#xff08;ns&#xff09…

Leetcode 第 389 场周赛题解

Leetcode 第 389 场周赛题解 Leetcode 第 389 场周赛题解题目1&#xff1a;3083. 字符串及其反转中是否存在同一子字符串思路代码复杂度分析 题目2&#xff1a;3084. 统计以给定字符开头和结尾的子字符串总数思路代码复杂度分析 题目3&#xff1a;3085. 成为 K 特殊字符串需要删…

Fire Smoke - Dynamic Nature

烟雾、火灾和爆炸预制件、着色器的集合。粒子支持HD、URP和标准渲染,自然制造风,因此它们对风速、方向和颤抖做出反应。 包装支持: Unity 2021.2及更高版本 Unity 2021.2 HD RP Unity 2021.2 URP Unity 2021.3及更高版本 Unity 2021.3 LTS HD RP Unity 2021.3 LTS URP Unity…

202112青少年软件编程(Scratch图形化)等级考试试卷(四级)

第1题&#xff1a;【 单选题】 小猫和小狗是非常好的朋友&#xff0c; 他们发明了一种加密方法&#xff1a; 用两位数字代表字母。比如 65 代表 A&#xff0c; 66 代表 B……&#xff0c; 75 代表 K&#xff0c; ……&#xff0c; 78 代表 N&#xff0c; 79 代表 O、 80 代表 …

zdpdjango_argonadmin使用Django开发一个美观的后台管理系统

初始代码 安装依赖 pip install -r requirements.txt生成管理员账户 迁移模型&#xff1a; python manage.py makemigrations python manage.py migrate创建超级用户&#xff1a; python manage.py createsuperuser启动服务 python manage.py runserver浏览器访问&#xf…

ChatGPT官宣新增Dynamic(动态)模式!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

Java中IO流详解

文章目录 字符流问题导入编码表**出现乱码的原因**ASCII表Unicode表汉字存储和展示过程解析问题导入解答 介绍分类字符输出流字符输入流字符缓冲流 特殊操作流转化流对象操作流打印流 工具包Commons-io介绍分类IOUtils类FileUtils类 字符流 问题导入 既然字节流能操作所有文件…

探索Linux的挂载操作

在Linux这个强大的操作系统中&#xff0c;挂载操作是一个基本而重要的概念。它涉及到文件系统、设备和数据访问&#xff0c;对于理解Linux的工作方式至关重要。那么&#xff0c;挂载操作究竟是什么&#xff0c;为什么我们需要它&#xff0c;如果没有它&#xff0c;我们将面临什…

递归学习第一个课

一、递归定义 基本定义 函数自己调用自己&#xff08;通俗第一印象&#xff09;大问题可以拆分小问题&#xff08;拆分&#xff0c;边界&#xff09;大问题与小问题的关系&#xff08;递归关系&#xff09; 为什么拆分小问题&#xff1f; 小问题更容易求解大问题与小问题内部…

基于Spark中随机森林模型的天气预测系统

基于Spark中随机森林模型的天气预测系统 在这篇文章中&#xff0c;我们将探讨如何使用Apache Spark和随机森林算法来构建一个天气预测系统。该系统将利用历史天气数据&#xff0c;通过机器学习模型预测未来的天气情况&#xff0c;特别是针对是否下雨的二元分类问题。 简介 Ap…

SpringBoot3整合RabbitMQ之四_发布订阅模型中的fanout模型

SpringBoot3整合RabbitMQ之四_发布订阅模型中的fanout模型 文章目录 SpringBoot3整合RabbitMQ之四_发布订阅模型中的fanout模型3. 发布/订阅模型之fanout模型1. 说明1. 消息发布者1. 创建工作队列的配置类2. 发布消费Controller 2. 消息消费者One3. 消息消费者Two4. 消息消费者…