聊聊工厂模式相关的设计
《设计模式》《代码大全》都要提及抽象工厂模式、工厂方法模式,都需要创建型模式,属于常见的 23 中设计模式中,工作中设计到工厂相关的模式还有另外两种,简单工厂,静态工厂方法,他们之间的区别是什么呢?
工厂方法模式
维基百科的描述:“ 定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。”
通俗点:定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。小米电视工厂,TCL电视工厂。强调的 产品等级
,小米电视、TCL电视,都属于不同的电视平台,属于同一产品等级
使用场景
- 创建对象需要大量重复的代码
- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
- 一个类通过其子类来指定创建那个对象
优点
- 用户只需要关心所需产品对应的工厂,无须关心创建细节
- 加入新产品符合开闭原则,提高可扩展性
缺点
- 类的个数容易过多,增加复杂度
- 增加了系统的抽象性和理解难度
Java
1 | public static void main(String[] args) { |
UML
抽象工厂模式
维基百科的描述:“ 提供接口,创建一系列相关或独立的对象,而不指定这些对象的具体类。”
通俗点:为一个产品族提供了统一的创建接口。当需要这个产品族的某一系列的时候,可以从抽象工厂中选出相应的系列创建一个具体的工厂类。强调的是创建产品族
、一些列相关或独立的对象
,小米电视、小米电饭煲、小米手表等相关的小米智能家居产品都应当属于一个产品族。
使用场景
- 一个系统要独立于它的产品的创建、组合和表示时。
- 一个系统要由多个产品系列中的一个来配置时。
- 需要强调一系列相关的产品对象的设计以便进行联合使用时。
- 提供一个产品类库,而只想显示它们的接口而不是实现时。
优点
具体产品从客户代码中被分离出来
容易改变产品的系列
将一个系列的产品族统一到一起创建
缺点
- 在产品族中扩展新的产品是很困难的,它需要修改抽象工厂的接口
Java
1 | public static void main(String[] args) { |
UML
简单工厂
维基百科的描述:“ 普通的工厂方法模式通常伴随着对象的具体类型与工厂具体类型的一一对应,客户端代码根据需要选择合适的具体类型工厂使用。然而,这种选择可能包含复杂的逻辑。这时,可以创建一个单一的工厂类,用以包含这种选择逻辑,根据参数的不同选择实现不同的具体对象。这个工厂类不需要由每个具体产品实现一个自己的具体的工厂类,所以可以将工厂方法设置为静态方法。 ”
通俗点:不需要通过具体的工厂类型来创建对应的对象,而是通过静态方法、参数的形式来选择性的创建指定对象,在一些场景下,方便理解,更高效,简单工厂模式由于使用了静态工厂方法,也可以代替构造器来创建对象。
使用场景
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
优点
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
缺点
- 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
Java
1 | public static void main(String[] args) throws InstantiationException, IllegalAccessException { |
UML
静态工厂方法
很多人分不清楚静态工厂方法和简单工厂的区别,简单工厂一种模式,简单工厂模式使用了静态工厂方法,静态工厂方法更小的概念,一般用来创建和销毁对象,静态工厂用的比较多的场景是取代构造器的方法来构建对象。
使用场景
对于类而言,为了让客户端获取它自身的一个实例,最传统的方法就是提供一个公有的构造器。还有一种方法,也应该在每个程序员的工具箱中占有一席之地。类可以提供一个公有的静态工厂方法( static factory method ),它只是一个返回类的实例的静态方法。下面是一个来自Bolean (基本类型boolean 的装箱类)的简单示例。这个方法将boolean 基本类型值转换成了一个Boolean 对象引用:
优势
静态工厂方法与构造器不同的第一大优势在于,它们有名称。
如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读。例如,构造器BigInteger (int , int, Random )返回的 Biginteger 可能为素数,如果用名为BigInteger.probablePrime 的静态工厂方法来表示,显然更为清楚。(Java 4 版本中增加了这个方法。)一个类只能有一个带有指定签名的构造器。编程人员通常知道如何避开这一限制: 通过提供两个构造器,它们的参数列表只在参数类型的顺序上有所不同。实际上这并不是个好主意。面对这样的AP I , 用户永远也记不住该用哪个构造器, 结果常常会调用错误的构造器。并且在读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云。
由于静态工厂方法有名称,所以它们不受上述限制。当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且仔细地选择名称以便突出静态工厂方法之间的区别。
静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。
这使得不可变类(详见第17 条)可以使用预先构建好的实例,或者将构建好的实例缓存起来, 进行重复利用,从而避免创建不必要的重复对象。Boolean.valueOf (b oolean )方法说明了这项技术: 它从来不创建对象。这种方法类似于享元(Flyweight )模式[ Gamma95 ] 。如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能。静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称作实例受控的类( instance-controlled ) 。编写实例受控的类有几个原因。实例受控使得类可以确保它是一个Singleton (详见第3 条)或者是不可实例化的(详见第4 条) 。它还使得不可变的值类(详见第17 条)可以确保不会存在两个相等的实例, 即当且仅当a==b 时, a . equals(b )才为true 。这是享元模式[ Gamma95 ] 的基础。枚举(巳num )类型(详见第34 条)保证了这一点。
静态工厂方法与构造器不同的第三大优势在子,它们可以返回原返回类型的任何子类型的对象。
这样我们在选择返回对象的类时就有了更大的灵活性。这种灵活性的一种应用是, API 可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API 变得非常简洁。这项技术适用于基于接口的框架( interface based framework ) (详见第20 条),因为在这种框架中,接口为静态工厂方法提供了自然返回类型。在Java 8 之前,接口不能有静态方法,因此按照惯例,接口Type 的静态工厂方法被放在一个名为Types 的不可实例化的伴生类(详见第4 条)中。例如Java Collections Framework的集合接口有45 个工具实现,分别提供了不可修改的集合、同步集合,等等。几乎所有这些实现都通过静态工厂方法在-个不可实例化的类( java.util. Collections ) 中导出。所有返回对象的类都是非公有的。
现在的Collections Framework API 比导出45 个独立公有类的那种实现方式要小得多,每种便利实现都对应一个类。这不仅仅是指API 数量上的减少,也是概念意义上的减少:为了使用这个API,用户必须掌握的概念在数量和难度上都减少了。程序员知道,被返回的对象是由相关的接口精确指定的,所以他们不需要阅读有关的类文档。此外,使用这种静态工厂方法时,甚至要求客户端通过接口来引用被返回的对象, 而不是通过它的实现类来引用被返回的对象,这是一种良好的习惯(详见第64 条) 。
从Java 8 版本开始,接口中不能包含静态方法的这一限制成为历史,因此一般没有任何理由给接口提供一个不可实例化的伴生类。已经被放在这种类中的许多公有的静态成员,应该被放到接口中去。但是要注意,仍然有必要将这些静态方法背后的大部分实现代码,单独放进一个包级私有的类中。这是因为在Java 8 中仍要求接口的所有静态成员都必须是公有的。在Java 9 中允许接口有私有的静态方法,但是静态域和静态成员类仍然需要是公有的。
静态工厂的第四大优势在于,所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发行版本的不同而不同。EnumSet (详见第36 条)没有公有的构造器,只有静态工厂方法。在OpenJDK 实现中,它们返回两种子类之一的一个实例,具体则取决于底层枚举类型的大小:如果它的元素有64个或者更少,就像大多数枚举类型一样,静态工厂方法就会返回一个RegalarEumSet 实例,用单个long 进行支持;如果枚举类型有65 个或者更多元素,工厂就返回JumboEnumSet实例,用一个long 数组进行支持。
这两个实现类的存在对于客户端来说是不可见的。如果RegularEnumSet 不能再给小的枚举类型提供性能优势,就可能从未来的发行版本中将它删除,不会造成任何负面的影H向。同样地,如果事实证明对性能有好处,也可能在未来的发行版本中添加第三甚至第四个
EnumSet 实现。客户端永远不知道也不关心它们从工厂方法中得到的对象的类,它们只关心它是EnumSet 的某个子类。静态工厂的第五大优势在于,方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
这种灵活的静态工厂方法构成了服务提供者框架( Service ProviderFramework)的基础,例如JDBC(Java 数据库连接)API 。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。服务提供者框架中有三个重要的组件:服务接口( Service Interface ),这是提供者实现的;提供者注册API ( Provider Registration API ),这是提供者用来注册实现的;服务访问API (Service Access API) ,这是客户端用来获取服务的实例。服务访问API 是客户端用来指定某种选择实现的条件。如果没有这样的规定, API 就会返回默认实现的一个实例,或者允许客户端遍历所有可用的实现。服务访问API 是“灵活的静态工厂”,它构成了服务提供者框架的基础。
服务提供者框架的第四个组件服务提供者接口( Service Provider Interface )是可选的,它表示产生服务接口之实例的工厂对象。如果没有服务提供者接口,实现就通 过反射方式进行实例化(详见第65 条) 。对于JDBC 来说, Connectio 口就是其服务接口的一部分DriverManager.registerDriver 是提供者注册API,DriverManager.getConnection是服务访问API, Driver 是服务提供者接口。
服务提供者框架模式有着无数种变体。例如,服务访问API 可以返回比提供者需要的更丰富的服务接口。这就是桥接( Bri dge )模式[ Gamma95 ] 。依赖、注入框架(详见第5 条)可以被看作是一个强大的服务提供者。从Java 6 版本开始, Java 平台就提供了一个通用的服务提供者框架j ava . util.ServiceLoader ,因此你不需要(一般来说也不应该)再自己编写了(详见第59 条) 。JDBC 不用S er v 工ceLoader ,因为前者出现得比后者早。
劣势
静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化。
例如,要想将Collections Framework 中的任何便利的实现类子类化, 这是不可能的。但是这样也许会因祸得福,因为它鼓励程序员使用复合(composition ),而不是继承(详见第四条),这正是不可变类型所需要的(详见第17 条) 。静态工厂方法的第二个缺点在于,程序员很难发现它们。
在API 文档中,它们没有像构造器那样在API 文档中明确标识出来, 因此对于提供了静态工厂方法而不是构造器的类来说,要想查明如何实例化一个类是非常困难的。Javadoc 工具总有一天会注意到静态工厂方法。同时,通过在类或者接口注释中关注静态工厂, 并遵守标准的命名习惯,也可以弥补这一劣势。下面是静态工厂方法的一些惯用名称。这里只列出了其中的一小部分:from一一类型转换方法,它只有单个参数,返回该类型的一个相对应的实例,例如:
Date d = Date . from(instant) ;
of一一聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来,例如:
Set
faceCards = EnumSet.of (JACK , QUEEN, KING); valueOf一一比from 和of 更烦琐的一种替代方法,例如:
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance 或者get Instance一-返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值,例如:
StackWalker luke = StackWalker.getInstance();
create 或者new Instance一一像instance 或者getInstance 一样,但create或者new Instance 能够确保每次调用都返回一个新的实例,例如:
StackWalker luke = StackWalker.getInstance();
getType一一像getInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型,例如:
FileStore fs = Files.getFileStore(Path);
newType一一像 new Instance 一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型,例如:
BufferedReader bufferedReader = Files.newBufferedReader(Path);
type-一-get Type 和new Type 的简版,例如:
List
litany = Collections.list(legacylitany);
简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。静态工厂经常更加合适,因此切忌第一反应就是提供公有的构造器, 而不先考虑静态工厂。