简单工厂模式
1)场景问题
在Java应用开发环境中,要“面向接口编程”。接口的职责就在于“对对象的行为进行封装”;
接口和抽象类的区别
接口是一个特殊的抽象类:二者相比较而言,一般优先选择接口。
在既要定义子类行为,又要为子类提供公共服务的情况下,使用抽象类;
//客户端
public class Client{
public static void main(String[] args){
Api api = new Impl();
api.test1("Lee");
}
}
//接口类
interface Api{
public void test1(String s);
}
//实现类
class ApiImpl implements Api{
public void test1(String s){
System.out.println(s);
}
}
上述实现过程中,我们不但知道了接口,同时还知道了具体的实现就是ApiImpl。而事实上,客户端根本就不应该知道具体的实现类是Impl;
那如何在不具体的使用实现类的情况下完成功能实现呢?
2)解决方案
简单工厂模式:提供一个创建对象实例的功能,而无须关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。
public class Client{
public static void main(String[] args){
Api api = new Factory().CreateApi(1);
api.test("Lee");
}
}
class Factory{
public static Api CreateApi(int condition){
Api api = null;
if(condition == 1){
api = new ApiImplA();
}else if(condition == 2){
api = new ApiImplB();
}
return api;
}
}
interface Api{
public void test(String s);
}
class ApiImplA{
public void test(String s){
System.out.println(s+" A");
}
}
class ApiImplB{
public void test(String s){
System.out.println(s+" B");
}
}
3)模式讲解
然而简单工厂模式的缺点也很明显,就是一旦增加了新的实现类,我们都必须去改写工厂类。这并不满足六大原则中的开闭原则;那如何实现增加类而不修改类呢?
一个解决的办法就是使用配置文件,当有了新的实现类,只要在配置文件中配置上新的实现类就好了。在简单工厂的方法里面,可以使用反射,当然也可以使用IOC/DI类实现。
这里要提到的两个核心操作就是读取properties文件,获取文件中的键值对;
然后根据键值对和反射机制来动态创建类;
package Factory;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class Factory {
private Api createApi() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
//读取properties文件
Properties properties = new Properties();
InputStream inputStream = new BufferedInputStream(new FileInputStream(this.getClass().getResource("").getPath()+"Factory.properties"));
properties.load(inputStream);
inputStream.close();
//利用反射机制来创建配置文件中的类
return (Api)Class.forName(properties.getProperty("Impl")).newInstance();
}
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
Api api = new Factory().createApi();
System.out.println(api.test("Lee"));
}
}
interface Api {
String test(String s);
}
class ApiImplB implements Api{
@Override
public String test(String s) {
return s+"-B";
}
}
class ApiImplA implements Api{
@Override
public String test(String s) {
return s+"-A";
}
}
然后新建一份Factory.properties文件
Impl=Factory.ApiImplB
简单工厂模式的优缺点
帮助封装
解耦
增加客户端的复杂度,需要让客户端理解各个参数的具体功能及含义,这会增加客户端的使用难度,也部分暴露了内部实现,这种情况可以选用可配置方式来实现;
4)思考
简单工厂的本质思想:选择实现,在不向客户端暴露具体方法的模式下,实现“选择创建对象”
何时选用简单工厂呢?
如果想要完全封装隔离具体实现,让外部只能通过接口来操作封装体,那么可是选用;
如果想要把对外开放的接口集中管理和控制,可是选用;
外观模式
1.场景问题:
当客户端调用某一个的A,B,C等功能模块时,需要了解各个模块的功能及组合关系,就像我们自己组装电脑,想要完成组装,就需要对电脑的各个组件有所了解。而组装电脑还有另一种方式,就是请特定的组装公司对电脑进行组装,自己不去了解组件。这就是“外观模式”(facade)
待解决的问题:
客户端为了使用模块功能,需要与系统内部的多个模块交互。
这对于客户端而言,是个麻烦,使得客户端不能简单的使用功能。而且,如果其中的某个模块发生了变化,还可能会引起客户端也要随着变化。
那么如何实现,才能让系统外部的客户端在使用子系统的时候,既能简单的使用这些子系统内部的模块功能,而又不用客户端与系统内部的多个模块交互呢?
2.解决方案:
使用外观模式来解决问题:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
这里需要理解两个词,一个是界面,一个是接口;
1.界面
界面是相对于类来说的,一个类的public方法,可以看作是这个类的外观。从类的外部来看,类的public方法就是这个类的界面。因为外部看不见类的内部实现,只能看到public方法。
2.接口
这里提到的接口主要是外部和内部交互的通道,通常是指一些方法,可以是类的方法,也可以是interface的方法。
仔细分析上面的问题,客户端想要操作更简单点,那可以根据客户端的需求来定制一个符合客户端要求的方法和接口,然后让客户端来调用这些接口,剩下的事情客户端就不用管它了,这样一来,事情就简单了。而这个特定的类,就被称为外观。
外观模式的主要思想就是:简化调用;
与中介者模式的区别,外观模式的调用是单向的,而中介者模式的调用的多个模块之间的。
外观模式结构:
public class Client{
public static void main(String[] args){
new Facade().test();
}
}
class Facade{
public void test(){
AModuleApi a = new AModuleImpl();
a.testA();
BModuleApi b = new BModuleImpl();
b.testB();
CModuleApi c = new CModuleImpl();
c.testC();
}
}
interface AModuleApi{
void testA();
}
class AModuleImpl implements AModuleApi{
public void testA(){};
}
interface BModuleApi{
void testB();
}
class BModuleImpl implements BModuleApi{
public void testB();
}
interface CModuleApi{
void testB();
}
class CModuleImpl implements CModuleApi{
public void testB();
}
外观模式的优缺点:
优点:
松散耦合;
简单易用;
划分访问层次;
缺点:
过多的Facade类容易让人迷惑,到底是调用Facade好呢,还是直接调用模块好;
适配器模式
1.场景问题:
如何让新的的接口可以适应原有的类的接口需要呢?
解决办法:
采用一个转接线类,转接线可以将新的接口是配成原有的接口。这个转接线类就是——适配器;
2.适配器模式
将一个类的接口转换成客户希望的另一个接口,也就是说,基于原有的接口实现符合当前调用的接口;
适配器的思想:转换匹配,复用功能
适配器的结构
public class Client{
Target target = new Adapter();
target.request();
}
interface Target{
void request();
}
//原接口
class Adaptee{
public void specificRequest(){}
}
//适配后的接口
class Adapter implements Target{
//原有的接口
private Adaptee adaptee;
public Adapter(Adaptee adaptee){this.adaptee = adaptee;}
//适配
public void request(){adaptee.specificRequest();}
}
适配器模式的优缺点:
优点:
良好的复用性
更好的扩展性
缺点:
适配器过多时,会让系统非常的零乱,不容易整体把控
单例模式
1.场景问题:
读取配置文件时,会使用到配置文件的读取对象。如果系统中多次使用到配置文件对象的内容,那就需要多次创建文件对象,这样就会造成内存浪费。也就是说,对于系统而言,配置文件只需要加载一次就够了,只需要一个实例对象就够了。
那么问题来了,该如何实现上述的思想呢?
2.解决方案:
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
应用单例模式的思路:
一个类能够被创建多个实例,问题就在于类的构造方法是公开的,也就是可以让类的外部来通过构造方法创建多个实例。也就是说,把创建实例的方法交给别人,那么就没法去控制外部创建的实例个数。
要想控制一个类只被创建一个实例,那么首要问题就是要把创建实例的权限收回来。让类自身来控制实例的创建,然后将这个创建方法公开就行了。
懒汉式的单例模式:时间换空间;
每次用到的时候,都需要进行判断,看看是否需要创建。如果一直没有被调用,则可以节约时间。
饿汉式的单例模式:空间换时间;
不用进行判断,直接创建,节省时间开销;不管是否使用,都会创建;
线程安全
饿汉式的单例模式是线程安全的;因为JVM只会创建一次
饿汉式不是线程安全的。那如何保证饿汉式的线程安全呢?
1.public static synchronized Singleton getInstance() 在单例全局切入点使用synchronize关键字,但这样会降低整个的访问速度,而且每次都要进行判断
2.双重校验锁
public Singleton getInstance(){
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
}
}
这种实现方式可是保证线程安全,而且性能上也影响不大;这个相对于第一种方法的改进就是,可以屏蔽掉一部分安全的线程,当不安全成分产生时,再进入同步代码块进行判断;这样可以节省同步时所浪费的时间;
但是volatile关键字可能会屏蔽掉JVM中一些必要的代码优化,所以运行效率并不高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重校验锁”来实现线程安全的单例,但并不建议大量采用,可以根据实际情况来定;
3.改进饿汉式——实现又可以实现延迟加载,又可以实现线程安全
思路:采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式,会在类装在的时候就初始化对象,不管你需不需要。
如果有一种方法能够让类装在的时候不去初始化对象,那不就解决问题了吗?
一种可行的方式就是采用类级内部类。在这个类级内部类里面去创建对象实例,这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而实现延迟加载和线程安全;
public class Singleton{
private statice class SingletonHolder(){
private static Singleton singletion = new Singleton();
}
private Singleton(){}
public Singleton getInstance(){
return SingletonHolder.singletion;
}
}
单例模式的本质:控制实例数目
单例模式的结构:
1)懒汉式
public class Singleton{
private static Singleton uniqueInstance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(uniqueInstance == null){uniqueInstance = new Singleton();}
return uniqueInstance;
}
public void singletionOperation(){}
private String singletionData;
public String getSingletonData(){return singletionData;}
}
2)饿汉式
public class Singleton{
private static Singleton uniqueInstance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){return uniqueInstance;}
public void singletionOperation(){}
private String singletionData;
public String getSingletonData(){return uniqueInstance;}
}
3)线程安全的延迟加载单例
public class Singleton{
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton{}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
工厂方法模式
场景问题:
在导出数据时:因为所导出的约定格式不同,需要使用不同的方法。
无论是导出什么样的格式,最后导出的都是一个文件,而且系统并不知道究竟要导出什么样的文件,因此需要有一个统一的接口,来描述系统最后生成的对象,并操作输出的文件。
//定义导出文件操作接口
public interface ExportFile{
//data 内容
//返回导出是否成功
public boolean export(String data);
}
对于实现该接口的对象而言,它并不知道如何实现。
解决办法:
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化那一个类,Factory Method使一个类的实例化延迟到其子类。
主要思想:是让父类在不知道具体实现的情况下,完成自身的功能调用;而具体的实现延迟到子类来实现。
工厂方法模式,就是我们日常开发中的用到的接口层和接口实现层,不过又有区别。
标准的工厂方法模式的组成除了方法提供者,还应该有方法操作者。也就是说,工厂方法一般是不允许外部客户端直接调用的,而是要通过方法操作者来使用方法。
在工厂方法模式里面,客户端要么使用Creator对象,要么使用Creator创建的对象,一般客户端不直接使用工厂方法。当然也可以把工厂方法暴露给客户端操作,但是一般不这么做。
工厂方法模式的结构
public interface Product{ ... }
public class ConcreteProduct implements Product{ ... }
public abstract class Creator{
protected abstract Product factoryMethod();
public void someOperation(){
Product product = factoryMethod();
}
}
public class ConcreteCreator extends Creator{
protected Product factoryMethod(){
return new ConcreteProduct();
}
}
工厂方法模式的优缺点:
优点:
可以在不知具体的情况下编程
更容易扩展对象的新版本
连接平行的类层次
缺点:
具体产品对象和工厂方法的耦合性。在工厂方法模式中,产品提供者和产品制造者耦合。
抽象工厂模式:
所解决的是接口间的关联性
抽象工厂模式的结构:
Abstract Factory:抽象工厂,定义创建一系列产品对象的操作接口;
Concrete Factory:具体的工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建
Abstract Product:定义一类产品对象的接口
Concrete Product:具体的产品实现对象,通常在具体工厂里面,会选择具体的产品实现对象,来创建符合抽象工厂定义的方法返回的产品类型的对象
Client:客户端,主要使用抽象工厂来获取一系列所需要的产品对象,然后面向这些产品对象的接口编程,以实现需要的功能。
public class Client{
public static void main(String[] args){
AbstractFactory af = new ConcreteFactory();
af.createProductA();
af.createProductB();
}
}
interface AbstractFactory{
AbstractProductA createProductA();
AbstractProductA createProductB();
}
interface AbstractProductA{
}
interface AbstractProductB{
}
class ProductA implements AbstractProductA{
}
class ProductB implements AbstractProductB{
}
class ConcreteFactory implements AbstractFactory{
public AbstractProductA createProductA(){return new ProductA();}
public AbstractProductA createProductB(){return new ProductB();}
}
该模式中的最终产出是产品,而产品的组合是由下一层的组件构成。抽象方法模式可以建立基层组件间的关系,使得产品是最佳组件组合;
抽象工厂模式的局限:
产品的抽象工厂与组件耦合,当组件增加或者减少时,作为产品抽象工厂的接口,就需要增加或者减少方法。
那如何解决呢?
我们可以将产品抽象工厂不参与组件的组合,而是将组合的操作分发到实现类中去完成,只需要在产品抽象类中获得一个产品就够。接口如下
public interface AbstractFactory{
//type 用于指定客户所需要的产品
public Object createProduct(int type);
}
抽象工厂模式的优缺点
优点:
分离接口和实现
容易切换产品簇
缺点:
类层次复杂
生成器模式
场景问题:
构建不同的文件的步骤都是一样的。都是一次构建文件头,文件体和文件尾。无论是构建Txt文件还是XML文件,步骤不变,所以应该将步骤提炼出来,形成共同的处理过程。
如何实现呢?
解决方案:
生成器模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
生成器模式的结构:
public interface Builder{
public void buildPart();
}
public class ConcreteBuilder implements Builder{
private Product resultProduct;
public void buildPart(){
return resultProduct;
}
public void buildPart(){};
}
public interface Product{}
public class Director{
private Builder builder;
public Director(Builder builder){this.builder=builder;}
public void construct(){
builder.buildPart();
}
}
生成器的优点
松散耦合
更容易的改变产品的内部表示
更好的复用性
生成器的思想:分离整理构建算法和部件构造