简单工厂模式
简单工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在简单工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
介绍
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
主要解决:主要解决接口选择的问题。
何时使用:我们明确地计划不同条件下创建不同实例时。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
关键代码:创建过程在其子类执行。
应用实例: 1、您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。 2、Hibernate 换数据库只需换方言和驱动就可以。
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
使用场景: 1、日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 2、数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 3、设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。
注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
角色
Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product
Product(抽象产品角色):它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
ConcreteProduct(具体产品角色):它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法
在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须直接使用new关键字来创建对象,它是工厂模式家族中最简单的一员
简单工厂模式示例
以水果为例:
public interface Fruit { void grow();// 生长 void harveset(); // 收货 void plant();// 种植 }
有两个子类苹果和葡萄:
public class Apple implements Fruit { public void grow() { System.out.println("Apple.grow()"); } public void harveset() { System.out.println("Apple.harveset()"); } public void plant() { System.out.println("Apple.plant()"); } }
public class Grape implements Fruit { public void grow() { System.out.println("Grape.grow()"); } public void harveset() { System.out.println("Grape.harveset()"); } public void plant() { System.out.println("Grape.plant()"); } }
有一个园丁,专门负责生产出各种水果:
public class Gardener { public static Fruit getFruit(String fruit) { if ("apple".equalsIgnoreCase(fruit)) { return new Apple(); } else if ("grape".equalsIgnoreCase(fruit)) { return new Grape(); } else { return null; } } }
想要什么水果就问园丁拿就好了:
public static void main(String[] args) { Fruit fruit0 = Gardener.getFruit("APPLE"); fruit0.grow(); Fruit fruit1 = Gardener.getFruit("GRAPE"); fruit1.harveset(); }
程序这么写优点就出来了:
1、用户不自己去生产产品,只需要负责去拿自己需要的东西就好了,这样用户-->产品之间的耦合度就降低了
2、代码模块职责更明确了,有专门消费的模块、有专门生产的模块
改进
上面的代码虽然实现了用户-->产品之间的分离,但还是有一个问题,工厂并不知道有多少种产品,所以每一次新增产品的时候,都需要新增else if分支,这样是不是不便呢?所以我们又想了一个办法,就是反射,园丁可以这么修改:
public class Gardener { public static Fruit getFruit(String fruitPath) throws Exception { Class<?> c = Class.forName(fruitPath); return (Fruit)c.newInstance(); } }
调用的地方可以写成:
public static void main(String[] args) throws Exception { Fruit fruit0 = Gardener.getFruit("com.xrq.simplefactory.Apple"); fruit0.grow(); Fruit fruit1 = Gardener.getFruit("com.xrq.simplefactory.Grape"); fruit1.harveset(); }
当然,这么写其实也有一点点问题,假如有一天我的项目想进行一个重构,重整类路径,包路径,比方说生产Apple的地方有100处,岂不是要修改100处?当然不用,有以下三种方法推荐:
1、写一个接口FruitPath,里面定义常量:
public interface FruitPath { public final static String apple = "com.xrq.simplefactory.Apple"; public final static String grape = "com.xrq.simplefactory.Grape"; }
2、写一个Fruit.properties文件,里面定义水果和类路径的对应关系:
Apple=com.xrq.simplefactory.Apple Grape=com.xrq.simplefactory.Grape
3、写一个Fruit.xml文件,里面定义水果和类路径的对应关系:
<apple>com.xrq.simplefactory.Apple</apple> <grape>com.xrq.simplefactory.Grape</grape>
第一种方式不说了,第二种方式.properties可以用Java自带的Properties类来解析,第三种方式.xml可以用DOM4J来解析。这样, 假设我以后要修改水果的路径,修改一个文件就可以了。
从设计模式的角度讲,这么修改也有很大的优点。现在不管我新增还是删除水果,园丁(类工厂)都不用变了,只需要告诉工厂我需要哪种水果就够了,工厂自然会给调用者返回。这种写法,也是Spring的基础。
最后说一点,希望大家明白,简单工厂模式或者说工厂模式的关注点并不在于在工厂中是如何生产出来需要的类的,而在于将创建产品与消费产品分离。前面使用过if...else if...else、反射,除了这些方法,还可以有别的方法可以创建产品,比如传入一个具体产品的标识,根据这个标识去数据库里面查询。
工厂模式的优缺点
优点:
1、简单优化了软件体系结构,明确了各自功能模块的职责和权利
2、通过工厂类,外界不需要直接创建具体产品对象,只需要负责消费,不需要关心内部如何创建对象
缺点:
1、改进前的简单工厂模式全部创建逻辑都集中在一个工厂类中,能创建的类只能是考虑到的,如果需要添加新的类,就必须改变工厂类了
2、改进前的简单工厂模式随着具体产品的不断增多,可能会出现共产类根据不同条件创建不同实例的需求,这种对条件的判断和对具体产品类型的判断交错在一起,很难避免功能模块的蔓延,对系统的维护和扩展不利
3、改进后的简单工厂模式主要是使用反射效率会低一些
简单工厂者模式在Java中的应用及解读
Calendar 类获取日历类对象
Calendar
抽象类,该类的子类有 BuddhistCalendar
、JapaneseImperialCalendar
、GregorianCalendar
、RollingCalendar
等
getInstance
方法,根据参数获取一个Calendar
子类对象,该方法实际将参数传给 createCalendar
方法,createCalendar
在根据参数通过 provider
或 switch
或者 if-else
创建相应的子类对象
以下为 Java8 中的 Calendar
类代码,Java7 中的实现为 if-else
方式
1 public static Calendar getInstance(TimeZone zone, Locale aLocale) { 2 return createCalendar(zone, aLocale); 3 } 4 5 private static Calendar createCalendar(TimeZone zone, Locale aLocale) { 6 CalendarProvider provider = LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale).getCalendarProvider(); 7 if (provider != null) { 8 try { 9 return provider.getInstance(zone, aLocale); 10 } catch (IllegalArgumentException iae) { 11 } 12 } 13 14 Calendar cal = null; 15 16 if (aLocale.hasExtensions()) { 17 String caltype = aLocale.getUnicodeLocaleType("ca"); 18 if (caltype != null) { 19 switch (caltype) { 20 case "buddhist": 21 cal = new BuddhistCalendar(zone, aLocale); break; 22 case "japanese": 23 cal = new JapaneseImperialCalendar(zone, aLocale); break; 24 case "gregory": 25 cal = new GregorianCalendar(zone, aLocale); break; 26 } 27 } 28 } 29 if (cal == null) { 30 if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") { 31 cal = new BuddhistCalendar(zone, aLocale); 32 } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja" && aLocale.getCountry() == "JP") { 33 cal = new JapaneseImperialCalendar(zone, aLocale); 34 } else { 35 cal = new GregorianCalendar(zone, aLocale); 36 } 37 } 38 return cal; 39 }
可以看到抽象产品角色
和工厂角色
都由 Calendar
担任,具体产品角色
由 Calendar
的子类担任
JDBC 获取数据库连接
一般JDBC获取MySQL连接的写法如下:
1 //加载MySql驱动 2 Class.forName("com.mysql.jdbc.Driver"); 3 DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
首先通过反射加载驱动类 com.mysql.jdbc.Driver
类,然后再通过 DriverManager
获取连接
看看 com.mysql.jdbc.Driver
的代码,该类主要的内容是静态代码块,其会随着类的加载一块执行
1 public class Driver extends NonRegisteringDriver implements java.sql.Driver { 2 public Driver() throws SQLException { 3 } 4 static { 5 try { 6 DriverManager.registerDriver(new Driver()); 7 } catch (SQLException var1) { 8 throw new RuntimeException("Can't register driver!"); 9 } 10 } 11 }
静态代码块:new 一个 Driver
类并注册到 DriverManager
驱动管理类中
1 public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException { 2 /* Register the driver if it has not already been added to our list */ 3 if(driver != null) { 4 registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); 5 } else { 6 throw new NullPointerException(); 7 } 8 println("registerDriver: " + driver); 9 }
其中的 registeredDrivers
是一个 CopyOnWriteArrayList
对象
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写
时复制器",Java并发包中类似的容器还有CopyOnWriteSet。
再通过 DriverManager.getConnection
获取连接对象的主要代码如下:通过for循环从已注册的驱动中(registeredDrivers)获取驱动,尝试连接,成功则返回连接
1 private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { 2 // ...省略... 3 println("DriverManager.getConnection("" + url + "")"); 4 for(DriverInfo aDriver : registeredDrivers) { 5 // If the caller does not have permission to load the driver then skip it. 6 if(isDriverAllowed(aDriver.driver, callerCL)) { 7 try { 8 println(" trying " + aDriver.driver.getClass().getName()); 9 Connection con = aDriver.driver.connect(url, info); 10 if (con != null) { 11 // Success! 12 println("getConnection returning " + aDriver.driver.getClass().getName()); 13 return (con); 14 } 15 } catch (SQLException ex) { 16 if (reason == null) { 17 reason = ex; 18 } 19 } 20 } else { 21 println(" skipping: " + aDriver.getClass().getName()); 22 } 23 } 24 // ...省略... 25 }
工厂角色为 DriverManager
类,抽象产品角色为 Connection
,具体产品角色则很多
spring中的BeanFactory
Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
<bean id="TeacherSubject" class="com.observer.sprintevent.TeacherSubject" > <constructor-arg name="name" value=""></constructor-arg> </bean>
1 ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); 2 3 TeacherSubject teacher = (TeacherSubject) context.getBean("TeacherSubject");