设计模式实用案例之工厂模式
本着实用的原则,我们应用工厂模式的时候首先应抓住最常用的方法,掌握了最常用的方法,熟练应用后将自然的掌握更多的派生方法。
比如工厂模式有简单工厂(Simple Factory )模式、多态工厂模式(Polymorphic Factory)、抽象工厂模式(Abstract Factory Pattern)。但这其中最常用的就是简单工厂模式。
下面我们用一个实际案例来看看简单工厂模式。
我们需要实现一个能够灵活配置数据库访问的功能,现在我们有一些实时数据库(eDNA、openplant、PI等),每一个实时数据库有自己的API,但其行为都是相似的,例如:
1、取某个点(tag)的实时值(传入点名);
2、按照一定的时间间隔取某个点的区间值的List(传入点名、start_time、end_time、interval;取该点从start_time到end_time的所有值,间隔为interval);
3、更新某个点的值(传入点名、更新时间、更新值)。
好了,作为产品化的考虑,上层应用的功能是标准的,而上层应用依赖于数据库的就是上面几种行为方法。由于不同的项目客户所使用的数据库不同,所以我们经常需要切换数据库。我们的目标是,系统将所有数据库的API都实现一遍,然后我们希望在配置文件中配置具体使用哪个数据库,这样产品客户化时,配置一下数据库,上层应用功能就能灵活的调用相应数据库的API来实现与数据库的交互功能。
我们使用工厂模式来实现。首先看工厂类:
import com.cpeam.sis.service.RTDBService; import com.cpeam.sis.util.ConfigUtil; /** * 创建 RTDBService的工厂类 */ public class RTDBServiceFactory { /** * 构造方法 */ private RTDBServiceFactory() { } /** * 根据类名初始化RTDBServic对象 * @param classname RTDBServic类名称 * @return RTDBServic对象 * @throws ClassNotFoundException 没有找到类异常 * @throws InstantiationException 实例化异常 * @throws IllegalAccessException 非法访问异常 */ public static RTDBService createRTDBService(String classname) throws ClassNotFoundException, InstantiationException, IllegalAccessException { return (RTDBService) Class.forName(classname).newInstance(); } /** * 根据配置文件初始化RTDBServic对象 * 在classpath存在rtdb.properties文件, 则使用rtdb.properties文件中classname对应的类名来初始化RTDBServic对象, 否则初始化默认的CpeamDBService对象 * @return RTDBServic子对象 * @throws IOException IO异常 * @throws InstantiationException 实例化异常 * @throws IllegalAccessException 非法访问异常 * @throws ClassNotFoundException 没有找到类异常 */ public static RTDBService createRTDBService() { try { return (RTDBService) Class.forName(ConfigUtil.getRtdbClassname()).newInstance(); } catch (InstantiationException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } catch (IllegalAccessException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } return null; } }
如上面的代码,工厂提供了2个重载(overload)的创建数据库服务的方法,一个是传入数据库的名字,一个是没有参数、根据配置文件。具体使用了反射(Class.forName().newInstance()方法)来创建服务,这样比较灵活,当我们新增实现类的时候不需要重写工厂类。
在工厂生产的产品的结构中,需要有1个接口和N个实现。先看接口,接口规定了数据库服务的标准方法:
public interface RTDBService { /** * 根据测点名称获得当前的实时值 * @param pointName 测点名称 * @return 测点实时值 */ public abstract double getRealtimeValue(String pointName); /** * 根据测点名称获得当前的实时值 * @param pointName 测点名称 * @param accuracy 精度 * @param errorValue 当获取实时值发生错误时返回的值 * @return 测点实时值 */ public abstract double getRealtimeValue(String pointName, int accuracy, double errorValue); /** * 根据测点名称集合获得当前测点集合中每个测点的实时值集合 * @param pointNames 测点名称集合 * @param accuracy 精度 * @return 测当前测点集合中每个测点的实时值集合 */ public abstract List<PointValueInfo> getRealtimeValues(List<String> pointNames, int accuracy); /** * 根据测点名称获得一段时间内的测点值对象集合 * @param pointName 测点名称 * @param start 开始时间 * @param end 结束时间 * @param interval 间隔(s) * @param maxPointNumber 测点值对象集合的元素数量最大值 * @param historyType 历史类型 * @param accuracy 精度 * @param errorValue 当获取一段时间内的测点值对象集合出错时每个测点值对象的值 * @return 一段时间内的测点值对象集合 */ public List<PointValueInfo> getPointValueInfoHistory(String pointName, Date start, Date end, int interval, long maxPointNumber, String historyType, int accuracy, double errorValue); /** * 根据测点名称获得一段时间内的测点值对象集合 * @param pointNames 多个测点名称 * @param start 开始时间 * @param end 结束时间 * @param interval 间隔(s) * @param maxPointNumber 测点值对象集合的元素数量最大值 * @param historyType 历史类型 * @param accuracy 精度 * @param errorValue 当获取一段时间内的测点值对象集合出错时每个测点值对象的值 * @return 一段时间内的测点值对象集合 * @return */ public List<PointValueInfo> getPointValueInfoHistory(List<String> pointNames, Date start, Date end, int interval, long maxPointNumber, String historyType, int accuracy, double errorValue); /** * 更新测点值 * @param pointName 测点名称 * @param time 时间戳 * @param value 值 * @return */ public boolean updatePoint(String pointName, long time, double value); /** * 批量修改测点值 * eDNA:最大支持160个 * @param pointNames * @param time * @param values * @return */ public boolean updatePoints(String[] pointNames, long time, double[] values); /** * 批量修改测点值 * eDNA:最大支持160个 * @param pointName * @param times * @param values * @return */ public boolean updatePoints(String pointName, long[] times, double[] values); }
基于上面的接口,可以有多个实现,具体的实现需要根据不同的数据库产品提供的api来实现接口,下面看一个例子:
/** * eDNA实时数据库接口实现类 */ public class EDNADBServiceImpl implements RTDBService { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//时间格式化 @Override public double getRealtimeValue(String pointname) { EDNAClient client = new EDNAClient(); double value = 0; try{ value = client.getEDNAApi().getRealtimeTagValue(ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(pointname)).DValue; }catch(Exception e){ } return value; } @Override public double getRealtimeValue(String pointname, int accuracy, double errorValue) { //TODO double value = getRealtimeValue(pointname); return CommonUtil.accurateValue(value, accuracy); } @Override public List<PointValueInfo> getRealtimeValues(List<String> pointnames, int accuracy) { EDNAClient client = new EDNAClient(); List<PointValueInfo> list = new ArrayList<PointValueInfo>(); try{ String[] ps = new String[pointnames.size()]; for(int i = 0; i < ps.length; i++){ //ps[i] = ConfigUtil.getRtdbServerName()+ pointnames.get(i); //String tagName = ConfigUtil.getRtdbServerName()+pointnames.get(i); ps[i] = ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(pointnames.get(i)); //System.out.println("longid:"+client.getEDNAApi().longidFromShortid(ConfigUtil.getRtdbServerName()+"AI00006")); //System.out.println("shortid:"+client.getEDNAApi().shortidFromLongid(tagName)); //System.out.println(ps[i]); //System.out.println(pointnames.get(i)); /* double value = getRealtimeValue(pointnames.get(i), accuracy, 0); PointValueInfo pv = new PointValueInfo(); pv.setValue(value); pv.setTime(new Date()); list.add(pv); */ } Map<String, DNAVal> vals = client.getEDNAApi().getRealtimeTagValues(ps); //System.out.println("-----------"); //System.out.println(pointnames.size()); List<PointValueInfo> list_temp = new ArrayList<PointValueInfo>(); for(Map.Entry<String,DNAVal> setEntry : vals.entrySet()) { PointValueInfo pv = new PointValueInfo(); try{ //System.out.println("实时" + setEntry.getKey() + " : " + setEntry.getValue().DValue + " - " + setEntry.getValue().Time + " - " + setEntry.getValue().Status); //System.out.println(client.getEDNAApi().longidFromShortid(setEntry.getKey())); pv.setPointName(setEntry.getKey()); pv.setValue(CommonUtil.accurateValue(setEntry.getValue().DValue, accuracy)); pv.setTime(new Date(setEntry.getValue().Time*1000L));//这里需要将UTC时间转换成timestamp pv.setStatus(setEntry.getValue().Status); }catch(Exception e){ pv.setValue(-9999); pv.setTime(new Date()); } list_temp.add(pv); } for(String p:ps){ boolean isHas = false; for(PointValueInfo pv:list_temp){ if(p.equals(pv.getPointName())){ list.add(pv); isHas = true; } } if(!isHas){ PointValueInfo pv = new PointValueInfo(); list.add(pv); } } //System.out.println(list.size()); //System.out.println("-----------"); }catch(Exception e){ e.printStackTrace(); } return list; } @Override public List<PointValueInfo> getPointValueInfoHistory(String pointName, Date start, Date end, int interval, long maxPointNumber, String historyType, int accuracy, double errorValue) { EDNAClient client = new EDNAClient(); if(historyType == null || historyType.equals(""))historyType=ApiEntry.HIST_SNAP+""; Map<String, DNAVal> vals = client.getEDNAApi().getHistoryTagValues(ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(pointName), sdf.format(start), sdf.format(end), interval, Integer.valueOf(historyType)); //System.out.println(vals.entrySet().size()); List<PointValueInfo> list = new ArrayList<PointValueInfo>(); try{ //System.out.println("-----------"); /* for(Map.Entry<String,DNAVal> setEntry : vals.entrySet()) { PointValueInfo pv = new PointValueInfo(); try{ System.out.println("历史" + setEntry.getKey() + " : " + setEntry.getValue().DValue + " - " + setEntry.getValue().Time + " - " + setEntry.getValue().Status); pv.setPointName(setEntry.getKey()); pv.setValue(CommonUtil.accurateValue(setEntry.getValue().DValue, accuracy)); pv.setTime(sdf.parse(setEntry.getKey())); }catch(Exception e){ pv.setValue(0); } list.add(pv); } */ long[] keys = new long[vals.size()]; int i = 0; for(Map.Entry<String,DNAVal> setEntry : vals.entrySet()) { //System.out.println("历史" + setEntry.getKey() + " : " + setEntry.getValue().DValue + " - " + setEntry.getValue().Time + // " - " + setEntry.getValue().Status); keys[i++] = sdf.parse(setEntry.getKey()).getTime(); } QuickSort.quickSort(keys); for(long k:keys){ String dk = sdf.format(new Date(k));//这里需要将UTC时间转换成timestamp //System.out.println(dk); DNAVal dna = vals.get(dk); //System.out.println("历史" + dk + " : " + dna.DValue + " - " + dna.Time + // " - " + dna.Status); PointValueInfo pv = new PointValueInfo(); try{ //System.out.println("历史" + setEntry.getKey() + " : " + setEntry.getValue().DValue + " - " + setEntry.getValue().Time + // " - " + setEntry.getValue().Status); pv.setPointName(dk); pv.setValue(CommonUtil.accurateValue(dna.DValue, accuracy)); pv.setTime(sdf.parse(dk)); pv.setStatus(dna.Status); }catch(Exception e){ pv.setValue(0); } list.add(pv); } }catch(Exception e){ e.printStackTrace(); } //System.out.println("-----------"); return list; } @Override public List<PointValueInfo> getPointValueInfoHistory( List<String> pointnames, Date start, Date end, int interval, long maxPointNumber, String historyType, int accuracy, double errorValue) { EDNAClient client = new EDNAClient(); List<PointValueInfo> list = new ArrayList<PointValueInfo>(); String[] ps = new String[pointnames.size()]; for(int i = 0; i < ps.length; i++){ //ps[i] = ConfigUtil.getRtdbServerName() +pointnames.get(i); ps[i] = ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(pointnames.get(i)); } if(historyType == null || historyType.equals(""))historyType=ApiEntry.HIST_SNAP+""; Map<String, DNAVal> vals = client.getEDNAApi().getHistoryMatrixValues(sdf.format(start), sdf.format(end), Integer.valueOf(historyType), ps); //System.out.println("-----------"); //System.out.println(pointnames.size()); List<PointValueInfo> list_temp = new ArrayList<PointValueInfo>(); for(Map.Entry<String,DNAVal> setEntry : vals.entrySet()) { PointValueInfo pv = new PointValueInfo(); try{ //System.out.println("历史" +setEntry.getKey() + " : " + setEntry.getValue().DValue + " - " + setEntry.getValue().Time + " - " + setEntry.getValue().Status); pv.setPointName(setEntry.getKey()); pv.setValue(CommonUtil.accurateValue(setEntry.getValue().DValue, accuracy)); pv.setTime(new Date(setEntry.getValue().Time * 1000L));//这里需要将UTC时间转换成timestamp pv.setStatus(setEntry.getValue().Status); }catch(Exception e){ pv.setValue(0); pv.setTime(new Date()); e.printStackTrace(); } list_temp.add(pv); } for(String p:ps){ boolean isHas = false; for(PointValueInfo pv:list_temp){ if(p.equals(pv.getPointName())){ list.add(pv); isHas = true; } } if(!isHas){ PointValueInfo pv = new PointValueInfo(); pv.setValue(0); pv.setTime(new Date()); list.add(pv); } } //System.out.println(list.size()); //System.out.println("-----------"); return list; } @Override public boolean updatePoint(String pointName, long time, double value) { boolean flag = false; try { //pointName = ConfigUtil.getRtdbServerName()+pointName;//加上前缀 pointName = ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(pointName); String timeStr = new DateTime(time).toString("yyyy-MM-dd HH:mm:ss"); EDNAClient client = new EDNAClient(); //删除 //client.getEDNAApi().deletePointRec(pointName, timeStr, timeStr); //【注意】eDNA删除和插入操作之间要让它休息一会儿,不然会处理不过来导致不能成功 Thread.sleep(100); //插入测点数据 DNAVal[] vals = new DNAVal[1]; DNAVal val = new DNAVal(); val.DValue = value;//值 //val.Time = (int)(time/1000);//时间戳(UTC) val.Time = client.getEDNAApi().parseDateStringToUTC(timeStr);//时间字符串转UTC时间 val.Status = 3;//状态 vals[0] = val; client.getEDNAApi().updateVals(pointName, vals); flag = true; }catch(Exception e){ e.printStackTrace(); } return flag; } @Override public boolean updatePoints(String[] pointNames, long time, double[] values) { boolean flag = false; try { EDNAClient client = new EDNAClient(); String timeStr = new DateTime(time).toString("yyyy-MM-dd HH:mm:ss"); int utcTime = client.getEDNAApi().parseDateStringToUTC(timeStr);//时间字符串转UTC时间 //封装参数值 for(int i = 0; i < pointNames.length; i++){ String p = pointNames[i]; //pointNames[i] = ConfigUtil.getRtdbServerName()+p;//加上前缀 pointNames[i] = ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(p); //删除 //client.getEDNAApi().deletePointRec(pointNames[i], utcTime, utcTime); //【注意】eDNA和操作之间要让它休息一会儿,不然会处理不过来导致不能成功 //Thread.sleep(100); } //【注意】eDNA删除和插入操作之间要让它休息一会儿,不然会处理不过来导致不能成功 Thread.sleep(100); //插入测点数据 for(int i = 0; i < pointNames.length; i++){ String p = pointNames[i]; DNAVal val = new DNAVal(); val.DValue = values[i];//值 //val.Time = (int)(time/1000);//时间戳(UTC) val.Time = client.getEDNAApi().parseDateStringToUTC(timeStr);//时间字符串转UTC时间 val.Status = 3;//状态 client.getEDNAApi().updateVals(p, val); //【注意】eDNA和操作之间要让它休息一会儿,不然会处理不过来导致不能成功 Thread.sleep(100); } flag = true; }catch(Exception e){ e.printStackTrace(); } return flag; } @Override public boolean updatePoints(String pointName, long[] times, double[] values) { boolean flag = false; try { //pointName = ConfigUtil.getRtdbServerName()+pointName;//加上前缀 pointName = ConfigUtil.getRtdbServerName()+EDNAClient.yingshe.get(pointName); EDNAClient client = new EDNAClient(); DNAVal[] vals = new DNAVal[times.length]; for(int i = 0; i < times.length; i++){ long time = times[i]; String timeStr = new DateTime(time).toString("yyyy-MM-dd HH:mm:ss"); int utcTime = client.getEDNAApi().parseDateStringToUTC(timeStr);//时间字符串转UTC时间 DNAVal val = new DNAVal(); val.DValue = values[i];//值 //val.Time = (int)(time/1000);//时间戳(UTC) val.Time = client.getEDNAApi().parseDateStringToUTC(timeStr);//时间字符串转UTC时间 val.Status = 3;//状态 //删除 //client.getEDNAApi().deletePointRec(pointName, utcTime, utcTime); //【注意】eDNA和操作之间要让它休息一会儿,不然会处理不过来导致不能成功 //Thread.sleep(100); vals[i] = val; } //【注意】eDNA删除和插入操作之间要让它休息一会儿,不然会处理不过来导致不能成功 Thread.sleep(100); //插入测点数据 client.getEDNAApi().updateVals(pointName, vals); flag = true; }catch(Exception e){ e.printStackTrace(); } return flag; } }
这样工厂-数据库服务接口-数据库服务实现的结构就建好了,以后如果需要接入新的数据库,只需要编写新的实现类,然后在配置文件中配置,上层应用就可以使用这个数据库了,不需要更新上层的代码。
另外,补充一下工厂模式和适配器模式的区别,看上去工厂模式和适配器模式都能运用在一个接口、多个实现灵活调用的场景中。但是工厂模式是创建类的方法;适配器模式是重新封装类的方法,是基于原有系统中已有的类来做的,所以两者在应用场景上有很大的不同。

浙公网安备 33010602011771号