从接口、抽象类到工厂模式再到JVM来总结一些问题

时间:2021-04-14 02:01:59

俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!

涉及到的知识点总结如下:

  • 为什么使用接口?
  • 接口和抽象类的区别
  • 简单工厂模式总结
  • Java中new和newInstance的区别
  • Java的Class.forName(xxx);
  • Java里创建对象的几个方式总结
  • Java类加载机制总结
  • Java WEB的三层架构和MVC的关系
  • 工厂方法模式总结
  • 抽象工厂模式总结
  • 一道面试题的分析
  • 一个服务提供者框架的学习
  • 接口的另一常用法:策略模式
  • 参考资料

  先看这样一个场景:某个果园里现在有两种水果,一种是苹果,一种是香蕉,有客户想采摘园子里的水果,要求用get()方法表示即可,代码如下:

苹果类

public class Apple {
    public void get() {
        System.out.println("得到苹果");
    }
}

苹果类

香蕉类

public class Banana {
    public void get() {
        System.out.println("得到香蕉");
    }
}

香蕉类

客户端

    public static void one() {
        // 实例化一个apple
        Apple apple = new Apple();
        // 实例化一个banana
        Banana banana = new Banana();

        apple.get();
        banana.get();
    }

客户端

  苹果和香蕉各自维持一个属于自己的get()方法,直接使用了new运算符进行实例化对象的操作,之后分别调用自己的get()方法,中规中矩的实现过程,很好,我们完成了客户的任务!

  这时有了新的需求:我们需要用采摘到的水果做果汁,使用doJuice(对应的水果)方法表示,水果类的代码不变,客户端新加的其他代码如下:

    private static void doJuice(Apple apple) {
        apple.get();
        System.out.println("做成果汁");
    }

    private static void doJuice(Banana banana) {
        banana.get();
        System.out.println("做成果汁");
    }

客户端

    public static void oneA() {
        // 实例化一个apple
        Apple apple = new Apple();
        // 实例化一个banana
        Banana banana = new Banana();

        doJuice(apple);
        doJuice(banana);
    }

  好了,貌似任务完成了,现在果园又引进了新品种的水果:橘子,西瓜,柿子,荔枝,葡萄,哈密瓜,火龙果,鸭梨……好了,还是要采摘这些水果然后做果汁!那么如果还是用之前的代码实现,试想一下,除了必须add的新水果类之外,在客户端里还要为每一个水果类型分别添加对应的doJuice(水果)方法,然而水果那么多……apple调用doJuice(apple)方法,orange调用对应的它做果汁的方法……少一个就不行!怎么改进呢?

  好了,为了增加程序的灵活性,我们引进接口。修改为:

从接口、抽象类到工厂模式再到JVM来总结一些问题

只需要一个方法即可:

    private static void doJuiceB(Fruit fruit) {
        fruit.get();
        System.out.println("做成果汁");
    }

客户端

    private static void two() {
        // 使用接口的引用指向子类的对象,向上转型过程,用到了多态
        Fruit apple = new AppleA();
        Fruit banana = new BananaA();
        Fruit orange = new OrangeA();

        doJuiceB(apple);
        doJuiceB(banana);
        doJuiceB(orange);
    }

  事实上,想把各个水果都抽象化!我们可以选择抽象类或者接口去实现!而现在我们要创建不带任何方法定义和成员变量的抽象化的类,首选的应该是接口!

  因为接口不仅仅是简单的抽象,它比对类型的抽象更进一步,是一种更纯粹的抽象过程!因为接口没有任何实现,没有任何和接口相关联的存储,因此也就无法阻止多个接口的组合(多继承)。而我们抽象的其实更是对采集水果这个动作的抽象,那么用接口表示最好不过了。客户端里使用了Java的多态机制,我们把任何一个水果类对象当参数传入到doJuice方法里都是可行的,也是正确的!因为每次调用都把对应的水果类向上转型为接口类型——Fruit,基于这个设计,使得Java程序员不用再为类似的场景做出多余的努力!这就是使用接口的核心原因之一!因为这会使得程序变得非常灵活!而且通过继承还能对接口进行扩展!也就是旧的接口去extends新的接口,从而扩展行为是可行的。使用接口的另一个原因是和抽象类类似——避免某个类被实例化,告诉编译器和程序员,这个类不需要实例化,我们只是为了对某些行为做出规范,大家想用就去遵守这个规则好了!

  可以说,接口的作用就简单可以概括为两个,一是避免客户端去实例化某个类,二是向上转型的使用;

  1、避免客户端去实例化(创建)该类的对象,我就是要告诉编译器,告诉其他程序员,我这个类只是为了做行为的规则用的,是为了规范大家对某个行为的使用,如果有其他类型的类要实现这个行为,好了,请你按照我的规则来!因为事实上,程序里某个行为不一定是一个类去实现,那就是有很多地方有用到,大家需要统一标准,甚至有的编程语言(Object-C)已经不把接口叫 interface,直接叫 protocol,统一标准的目的,是让大家都知道这个是做什么,但是不用知道怎么做,故这个类型不需要客户端去实例化!这一点和抽象类是一致的,更通俗的说接口就是个招牌,比如说看到这个图标:

从接口、抽象类到工厂模式再到JVM来总结一些问题然后你就知道这是7-11便利店,这是类似于国内超市的地方,24小时营业的商店(绝大绝大多数),这个图标就是接口,我们远远的看到了这个接口,就知道这个店是711便利店(实现接口),好了这时候有人会问:那么为什么我们要去定义一个接口呢?这个店可以直接卖东西就得了呗(直接写实现方法),是的这个店可以直接卖东西的,不挂711牌子,那样我们就不能乍一看,或者看见店铺门就直接简单粗暴的知道这个店铺是24小时便利商店……要么我们沿着马路边走到每个店铺近前去观察,哪个是24小时营业的?(这就是反射),很显然这样一家家的问实在是非常麻烦(反射性能很差)。要么我们就记住,xxx路xxx号店铺是24小时营业的便利店哎!离着200米的xxxx号也是(硬编码),很显然这样我们要记住的很多东西(代码量剧增),而且如果有新的便利店开张,我们也不可能知道(不利于扩展)……也就是说店铺门前挂了这个招牌,我们不用进去问,甚至不用走到店铺门前,远远看一眼,看到了这个标志,就知道这个店铺是711便利店嘛,24小时营业的哦!

从接口、抽象类到工厂模式再到JVM来总结一些问题

  想要便利的话,就可以直接去……没有挂这个招牌,就算卖的东西和711一模一样,我们不进去问营业员就不会知道它的24小时营业特点!!!再举一个类似的例子,大家都知道吉祥馄饨,

从接口、抽象类到工厂模式再到JVM来总结一些问题

  接口也可以比喻为吉祥馄饨的连锁招牌,每个连锁店都有一样的馄饨菜单,一样的馄饨做法说明,一样的总部的馄饨配料……但是每个店铺实现的味道,每个店铺实现过程中的卫生情况……呵呵哒!大家就不知道了吧,只能直观的去看各个店铺的口碑(具体类对接口的不同实现),同样对上一个例子—711便利店来说,人们不需要去具体询问店员你们的店铺是24小时营业的便利店么?还是只是类似国内的小商店……人们只需要,也只能了解到711这个牌子的代表意思就足够了……映射到程序里,就是对具体的类来说,每一个方法是怎么实现的,调用者不知道,其实也不在乎!简单来说对调用者—它只需要知道接口的知识,也最多只能知道接口的知识,因此这是一个很好的抽象过程,和把不同层次的工作内容快速分离的过程!

  好了,到这里也许又有了新的疑问!为啥看到一些人写的代码,或者一些项目的源代码里即使只有一个类需要使用接口,也去不厌其烦的定义接口!而且从业务上看,未来也不太可能有其他类用这个接口,那定义这个接口意义在哪里?

  三个字:没卵用。

  《Thinking in Java》一书说到“确定接口是理想选择,因而应该总是选择接口而不是具体的类,这其实是一个诱惑!因为对于创建类,几乎任何时候,都可以创建接口来代替!许多人都陷入了这个陷阱,在编写代码的时候,只要有那么一丝可能就去肆无忌惮的创建接口……貌似是因为需要使用不同的具体实现,实际上已经是过度优化,已经变成了一种草率的设计!任何抽象都应该以真正的需求出发。必要的时候,应该是重构接口而不是到处添加额外级别的间接性,并因此带来额外的复杂性,恰当的原则应该是:优先想到使用类,从类开始,如果接口的设计需求变得非常明确了,好的,进行重构吧!记住,接口虽然是一个很重要的工具,但是极其容易被滥用!

  哦了!也许还会有疑问!有人说:我觉得抽象类完全可以替代接口,接口能做的抽象类都可以,而且抽象类还能包括实现。这比接口更强大……到这里,先打住这个提问,别忘了—Java不支持多继承!如果只是使用抽象类则必须继承abstract class,而Java只允许单继承,所以仅在这一点上,接口存在就已经十分有意义了,它解决了Java不允许多继承的问题。哦了,也许还会有人问,说:假如有一天Java允许多继承了,那是不是接口就可以淘汰了? 就像C++允许多继承……所以C++里就没有接口……还是说Java的设计者有其他的考虑,即便Java允许多继承,接口依然有它的意义?我想打死这个问问题的人……

  先让我们谈谈接口和抽象类的区别吧!接口和抽象类有什么区别?你选择使用接口和抽象类的依据是什么?

  接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对类型的抽象。抽象类表示这个对象是什么。接口表示这个对象能做什么……比如人分男人,女人这两个类,他们的抽象类是人,这种继承关系说明他们都是人,而人都可以吃东西,然而事实上狗也可以吃东西,我们就可以把“吃东西”这个动作定义成一个接口,然后其他需要的类去实现它,从而具备吃的行为。所以,在高级语言上,合理的继承设计就是:一个类只能继承一个类(抽象类)(正如人不可能同时是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口),这点是c++的缺陷。总结来说:

  第一点. 接口是抽象类的更高一级的抽象,接口没有任何相关自身的存储,接口中所有的方法默认都是抽象的,默认都是public的,而抽象类只是声明方法的存在而不去实现它的类,需要public方法,则需要程序员指定访问权限。

  第二点. 接口可以多继承,抽象类不行。

  第三点. 接口定义方法,不能实现,而抽象类可以实现部分方法,也就是抽象类可以同时存在普通方法和抽象方法。

  第四点. 接口中基本数据类型为public static, 而抽类象不是的。

  一句话区分:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为就高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对某动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度。

  前面说了那么多,首先谈到接口可以避免其他人去实例化该类,它的出现只是为了给大家统一制定规则的!从而可以很好的隔离工作内容,或者说分离不同业务的分工,这有利于程序的扩展……谈到了这个:接口在开发过程中可以快速分离工作内容,比如上层业务的开发者在写顾客购买商品需要支付这个业务逻辑的时候需要一个功能,就是连接数据库去访问顾客的商城钱包,但是他的工作专注于实现业务逻辑,不想分开精力去做底层实现,那么他只需要先定义一个接口,去定义一个规范,然后就可以继续他的业务逻辑代码了,而底层业务(算法,数据库连接,数据存储等)实现者可以根据这个接口,做他自己具体的实现,上层调用者不需要也不应该去知道底层实现,他只需要了解到接口这一级别即可。这样通过使用接口就可以快速的分离工作内容,达到团队并行工作的目的。此外,如果规范是通过接口定义的,那么当你这个功能有多个实现时,你只要实现了这个接口,那么可以快速的替换具体实现,做到代码层面的完全可以分离。总结起来就一句话:接口或者规范可以在开发过程中做到工作内容的分离。团队的人,A写接口,B写实现,C写实现……B、C就不用写接口,B、C看到A的接口就知道我要具体去实现什么功能,供上层调用者A使用即可。而对于A来说,我不给你们统一规定好了,你们怎么知道该如何去实现哪些具体内容……比如同样是登陆操作,A不统一规定为login(xxx);那么很有可能C写一套登录实现叫loginA,B写一套登录实现叫denglu……具体到程序里,就会让人困惑……且无法快速的替换不同的实现过程。

  更进一步,一个任务:A作为上层调用者,它需要一个算法方面的功能,但是A不需要去具体学习相关算法和实现,于是A就去写一个接口,而让B这个底层开发人员写实现,但是B恰恰今天不在公司,A明天要出差,任务后天就交工啦!那A今天必须把使用这个接口的代码写好,A写好接口,明天B把接口实现传个实例进来,任务ok,交工。故interface换个叫法就是contract,有点合同的意思。B实现了这个接口,代表B承诺能做某些事情。A需要一些能做某些事情的东西,于是A要求,必须实现了接口,才能被我调用。实际上也就是个“规范”。

  再进一步,想之前的水果的例子:客户想把果园采摘的水果做出果汁,客户作为上层调用者,他只需要水果去做出果汁,而水果具体怎么得到的,他不需要也没必要关心,因为前面刚刚提到的,客户没有必要为了喝果汁还花代价去亲自采摘水果……之前的设计是客户端有一个传入接口类型参数的doJuice(Fruit fruit);方法,客户端调用该方法动态的去做出不同水果的果汁,而不久后,果园升级为果厂了,厂子规定:这个采摘水果的方法是计算据操控的,完全自动化,属于商业机密,有技术壁垒的,不想泄漏出去,下面是代码改进之后:

public class AppleC implements FruitC {
    @Override
    public void get() {
        System.out.println("苹果");
    }
}

苹果

public class BananaC implements FruitC {
    @Override
    public void get() {
        System.out.println("香蕉");
    }
}

香蕉

public interface FruitC {
    void get();
}

接口

现在我们把采集水果的代码单独放到一个类里,我们叫它工厂类

public class FruitFactory {

    public static FruitC getApple() {
        return new AppleC();
    }

    public static FruitC getBanana() {
        return new BananaC();
    }
}

水果工厂

下面看客户端

    private static void three() {
        FruitC apple = FruitFactory.getApple();
        FruitC banana = FruitFactory.getBanana();

        doJuice(apple);
        doJuice(banana);
    }

    private static void doJuice(FruitC fruit) {
        fruit.get();
        System.out.println("做成果汁");
    }

客户端

从接口、抽象类到工厂模式再到JVM来总结一些问题  

  上面的设计,对于客户端来说,不再直接简单粗暴的new一个水果的实例,而是把生成水果的实例的过程放到一个单独的类,把这个实例化的过程隐藏了起来……我们叫它工厂类,这个设计也叫简单工厂模式——它解决的问题是如何去实例化一个合适的对象。简单工厂模式的核心思想就是:有一个专门的类来负责创建实例。具体来说,把产品看着是一系列的类的集合,这些类是由某个抽象类或者接口派生出来的一个对象树,而工厂类用来产生一个合适的对象来满足客户的要求,从而把对象的创建过程进行封装,如果简单工厂模式所涉及到的具体产品之间没有共同的逻辑,那么我们就可以使用接口来扮演抽象产品的角色,如果具体产品之间有逻辑上的联系,我们就把这些共同的东西提取出来,放在一个抽象类中,然后让具体产品继承抽象类,为实现更好复用的目的,共同的东西总是应该抽象出来的。在实际的的使用中,抽象产品和具体产品之间往往是多层次的产品结构,如图:

从接口、抽象类到工厂模式再到JVM来总结一些问题

  下面看看教科书的定义简单工厂模式属于类的创建型模式,也叫静态工厂方法模式,通过专门定义一个类来负责创建其他类的实例,目的是为了隐藏具体类的对象的创建过程,既不耽误对象的创建,也隐藏了创建过程!被创建的实例通常都具有共同父类  。本例子里,苹果和香蕉都有一个共同的父类——水果,此时我们专门定义一个类,负责创建其他类的实例,这个类叫简单工厂类,它有三个角色:

  1、工厂(Creator)角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。

  2、抽象产品(Product)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口,或者抽象类。

  3.具体产品(Concrete Product)角色:简单工厂模式所创建的具体实例对象,这些对象去继承或者实现抽象角色。

  不过,细细体味下,在工厂类里,我们的设计是针对每一个水果都有一个对应的获取水果的操作,这是一种很粗糙的设计,我们还可以更好,就是把每个get方法抽象为一个公用的get方法!代码如下:同样是有水果接口和具体的水果

public class AppleD implements FruitD {
    @Override
    public void get() {
        System.out.println("苹果");
    }
}

苹果

public class BananaD implements FruitD {
    @Override
    public void get() {
        System.out.println("香蕉");
    }
}

香蕉

public interface FruitD {
    void get();
}

接口

下面是产生水果的简单工厂类

public class FruitFactoryFour {
    public static FruitD getFruit(String type) {
        if ("apple".equalsIgnoreCase(type)) {
            return new AppleD();
        } else if ("banana".equalsIgnoreCase(type)) {
            return new BananaD();
        } else {
            System.out.print("error!");
        }

        return null;
    }
}

  这样稍微好了点儿,把每个水果对应的get方法抽象为一个公用的get方法,在这个get但是还不够!工厂类里的if判断来根据传入的参数去判断应该生成哪个水果的对象,并把这个对象返回(依然是向上转型的使用),客户端只需简单的进行调用即可。非常方便,也隐藏了具体产品的实例化过程,完美的完成了客户和水果厂的需求。可以充分的认为,简单工厂模式的核心是工厂类,这个类含有必要的逻辑判断(if-else),可以决定在什么时候创建哪一个类的实例,而调用者则可以免除直接创建对象的责任。简单工厂模式通过这种做法实现了对责任的分割,当系统引入新的产品的时候无需修改调用者!!!无需修改调用者!!!无需修改调用者!!!

  举个真实的例子:java.text.DateFormat类

从接口、抽象类到工厂模式再到JVM来总结一些问题

  “DateFormat 是Java的日期/时间格式化子类的抽象类,它以与语言无关的方式格式化并解析日期或时间。日期/时间格式化子类(如 SimpleDateFormat)允许进行格式化(也就是日期 -> 文本)、解析(文本-> 日期)和标准化。将日期表示为 Date 对象,或者表示为从 GMT(格林尼治标准时间)1970 年 1 月 1 日 00:00:00 这一刻开始的毫秒数。DateFormat 提供了很多类方法,以获得基于默认或给定语言环境和多种格式化风格的默认日期/时间 Formatter。格式化风格包括 FULL、LONG、MEDIUM 和 SHORT。方法描述中提供了使用这些风格的更多细节和示例。DateFormat 可帮助进行格式化并解析任何语言环境的日期。对于月、星期,甚至日历格式(阴历和阳历),其代码可完全与语言环境的约定无关。且格外说下,日期格式不是同步的,建议为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。”不过,还是先关注这个类的简单工厂设计模式的使用,如下源码,只留了静态工厂方法:

public abstract class DateFormat extends Format {

    public final static DateFormat getTimeInstance() {
        return get(DEFAULT, 0, 1, Locale.getDefault(Locale.Category.FORMAT));
    }

    public final static DateFormat getTimeInstance(int style) {
        return get(style, 0, 1, Locale.getDefault(Locale.Category.FORMAT));
    }

    public final static DateFormat getTimeInstance(int style,
                                                   Locale aLocale) {
        return get(style, 0, 1, aLocale);
    }

    public final static DateFormat getDateInstance() {
        return get(0, DEFAULT, 2, Locale.getDefault(Locale.Category.FORMAT));
    }

    public final static DateFormat getDateInstance(int style) {
        return get(0, style, 2, Locale.getDefault(Locale.Category.FORMAT));
    }

    public final static DateFormat getDateInstance(int style,
                                                   Locale aLocale) {
        return get(0, style, 2, aLocale);
    }

    public final static DateFormat getDateTimeInstance() {
        return get(DEFAULT, DEFAULT, 3, Locale.getDefault(Locale.Category.FORMAT));
    }

    public final static DateFormat getDateTimeInstance(int dateStyle,
                                                       int timeStyle) {
        return get(timeStyle, dateStyle, 3, Locale.getDefault(Locale.Category.FORMAT));
    }

    public final static DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale) {
        return get(timeStyle, dateStyle, 3, aLocale);
    }

    public final static DateFormat getInstance() {
        return getDateTimeInstance(SHORT, SHORT);
    }
}

随便看一个方法,比如

    public final static DateFormat getTimeInstance(int style) {
        return get(style, 0, 1, Locale.getDefault(Locale.Category.FORMAT));
    }

简单工厂模式的影子,方法里面根据恰当的时候传入的参数,返回对应的对象……如果要格式化一个当前语言环境下的日期,可使用某个静态工厂方法:

  myString = DateFormat.getDateInstance().format(myDate);

要格式化不同语言环境的日期,可在 getDateInstance() 的调用中指定它。

 DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.FRANCE);

使用 getDateInstance 来获取该国家/地区的标准日期格式。另外还提供了一些其他静态工厂方法。使用 getTimeInstance 可获取该国家/地区的时间格式。使用 getDateTimeInstance 可获取日期和时间格式。可以将不同选项传入这些工厂方法,以控制结果的长度(从 SHORT 到 MEDIUM 到 LONG 再到 FULL)。确切的结果取决于语言环境,但是通常:

  • SHORT 完全为数字,如 12.13.52 或 3:30pm
  • MEDIUM 较长,如 Jan 12, 1952
  • LONG 更长,如 January 12, 1952 或 3:30:32pm
  • FULL 是完全指定,如 Tuesday、April 12、1952 AD 或 3:30:42pm PST。

  以上……可以去看JDK文档和源代码,不再赘述。

简单工厂模式的优缺点分析: 

优点:工厂类是整个模式的关键所在。它包含必要的判断逻辑,能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。用户在使用时可以直接根据工厂类去创建所需的实例,而无需了解这些对象是如何创建以及如何组织的。有利于整个软件体系结构的优化。

缺点:由于工厂类集中了所有实例的创建逻辑,这就直接导致一旦这个工厂出了问题,所有的客户端都会受到牵连;而且由于简单工厂模式的产品室基于一个共同的抽象类或者接口,这样一来,但产品的种类增加的时候,即有不同的产品接口或者抽象类的时候,工厂类就需要判断何时创建何种种类的产品,这就和创建何种种类产品的产品相互混淆在了一起,违背了单一职责,导致系统丧失灵活性和可维护性。而且更重要的是,简单工厂模式违背了“开放封闭原则”,就是违背了“系统对扩展开放,对修改关闭”的原则,因为当我新增加一个产品的时候必须修改工厂类,相应的工厂类就需要重新编译一遍。

  一句话:虽然简单工厂模式分离产品的创建者和消费者,有利于软件系统结构的优化,但是由于一切逻辑都集中在一个工厂类中,导致了没有很高的内聚性,同时也违背了“开放封闭原则”。另外,简单工厂模式的方法一般都是静态的,而静态工厂方法是无法让子类继承的,因此,简单工厂模式无法形成基于基类的继承树结构。

  其实说句实话,到了这里,其实又要想了,不要过度的优化,不要为了使用设计模式而使用设计模式!!!如果是业务比较简单的场景,这样的简单工厂模式还是非常好用的!不过无论如何,这样繁琐的if-else判断还是不太好,一旦判断条件稍微多了点儿,那么多的if-else写起来就非常繁琐,即使业务量少也不是最好的!那怎么办呢?别忘了Java的反射机制!我们继续变化:

一样的接口和对应水果的实现类

public interface FruitE {
    void get();
}

public class BananaE implements FruitE {
    @Override
    public void get() {
        System.out.println("香蕉");
    }
}

public class AppleE implements FruitE {
    @Override
    public void get() {
        System.out.println("苹果");
    }
}

新的工厂类

public class FruitFactoryFive {
        public static FruitE getFruit(String type) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class fruit = Class.forName(type);
        return (FruitE) fruit.newInstance();
    }
}

通过传入的对应水果的参数,通过Java的反射机制来动态的生成实例。在客户端这样调用:

    private static void five() throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        FruitE apple = FruitFactoryFive.getFruit("simpleFactory.five.AppleE");
        FruitE banana = FruitFactoryFive.getFruit("simpleFactory.five.BananaE");

        apple.get();
        banana.get();
    }

  必须注意:传入的字符串,必须是对应实现类的全名(带包路径的类全称),否则报错“java.lang.ClassNotFoundException: ”。我们发现工厂类的实现非常简便和灵活了,使用Java的反射机制省去判断的步骤,比之前的繁琐而重复的if-else判断要好点儿了,工厂的扩展性很强!但是依然不完美,客户端缺少调用的灵活性,客户端必须传入严格对应类名的字符串,甚至还要包含完整的包名,才能实例化对应的类……稍微差一点儿都会失败。故还是前面说的,简单的业务,一般使用if-else的方式,不用考虑大小写,传入字符串即可,而稍微复杂点儿的,可以变为反射的方式实现,而反射实现工厂类,对于客户端又显得调用上比较不方便,那么这样岂不是很纠结了……

  别急,先看看这样的一个问题:为什么有时候用newInstance方法实例化对象,而不是new关键字呢?两个方式有什么区别呢?

  相同点:两种方式都可以创建一个类的实例。

  不同点:newInstance是通过反射创建对象的,在创建一个类的对象的时候,你可以对该类一无所知,一些开源框架,比如Spring内部,大都是通过反射来创建实例的,当然这种方法创建对象的时候必须拥有该类的句柄,甚至必要的时候还要有相关的权限设置(比如无参构造函数是私有的),而句柄是可以动态载入的,实际上JVM内部也是这样加载类的。该方法创建对象的时候,只会调用该类的无参构造方法,不会调用其他的有参构造方法,new关键字的后面接类名参数,是最常见的创建实例的方式,但是必须要知道一个明确的类才能使用。用new这个关键字的话,是首先调用new指令创建一个对象,然后调用构造方法来初始化这个对象,比如这样一句代码:

AppleE appleE = new AppleE();

反编译的字节码文件:

  // access flags 0x9
  public static main([Ljava/lang/String;)V throws java/lang/IllegalAccessException java/lang/ClassNotFoundException java/lang/InstantiationException
   L0
    LINENUMBER 27 L0
    NEW simpleFactory/five/AppleE
    DUP
    INVOKESPECIAL simpleFactory/five/AppleE.<init> ()V
    ASTORE 1

看到先调用new指令生成一个对象

NEW simpleFactory/five/AppleE

然后调用dup指令复制对象的引用,最后调用Object的构造方法进行对象的初始化。

INVOKESPECIAL simpleFactory/five/AppleE.<init> ()V

要明确,对于newInstance,newInstance 不是关键字,newInstance 是java反射框架中类对象(Class)创建新对象的方法,方法签名:Object java.lang.Class.newInstance();如:

Class clazz = String.class;
Object newInstance = clazz.newInstance();

newInstance() 方法其实经常见于工厂设计模式中,在该模式中,工厂类的该方法返回一个工厂bean。如:

Factory factory = new Factory();
Object obj = factory.newInstance();

  严格意义上来讲,new和newInstance这两者没有可比较性,因为一个是Java的关键字,有明确的用法和定义。一个是经常使用,但非标准的方法名称。但是事实上,用newInstance与用new是有区别的,前面说了,区别在于创建对象的方式不一样,前者是使用类加载机制,那么为什么会有这两种创建对象的方式?

  这个就要从可伸缩、可扩展,可重用等软件思想上解释了。Java中工厂模式经常使用newInstance来创建对象,因此从为什么要使用工厂模式上也可以找到具体答案。 例如:

Class c = Class.forName(“A”);
factory = (AInterface)c.newInstance();

AInterface是A类实现的接口,详细的拆分一下:

String className = "A";
Class c = Class.forName(className);
factory = (AInterface)c.newInstance();

进一步,如果下面这样写:

String className = readXMl();//从xml 配置文件中获得A类的句柄
Class c = Class.forName(className);
factory = (AInterface)c.newInstance();

上面代码,利用配置文件就消灭了A类名称,优点:无论A类怎么变化,上述代码不变,甚至可以更换A的兄弟类B , C , D....等,只要他们继承Ainterface就可以。

  从jvm的角度看,我们使用new的时候,这个要new的类可以没有被虚拟机加载,但是使用newInstance时候,就必须保证:

  1、这个类已经被加载。

  2、这个类已经连接了。

而完成上面两个步骤的正是Class的静态方法forName,这个静态方法调用了启动类加载器(就是加载java API的那个加载器)。有上面jvm上的理解,那么我们可以这样说,newInstance实际上是把new这个方式分解为两步,即首先调用class的加载方法加载某个类,然后实例化,这样分步的好处是显而易见的,我们可以在调用Class的静态加载方法forName时获得更好的灵活性,提供给了我们降耦的手段。

  既然提到了类的加载机制和Java的反射框架,那么就顺势总结下:

  先看一个新问题,Java中创建(实例化)对象的五种方式?

  Java中使用new和Class.forName()在类被加载的时候有什么区别?两者是否一样?

  

  前面看到了简单工厂模式作为一个最基本和最简单的设计模式,却有着非常广泛的应用,再看一个例子:JDBC操作数据库。

JDBC是SUN公司提供的一套数据库编程接口API,它利用Java语言提供简单、一致的方式来访问各种关系型数据库。Java程序通过JDBC可以执行SQL语句,对获取的数据进行处理,并将变化了的数据存回数据库,因此JDBC是Java应用程序与各种关系数据进行对话的一种机制。用JDBC进行数据库访问时,要使用数据库厂商提供的驱动程序接口与数据库管理系统进行数据交互。客户端要使用使用数据时,只需要和工厂进行交互即可,这就导致操作步骤得到极大的简化,操作步骤按照顺序依次为:

  1、注册并加载数据库驱动,一般使用Class.forName();

  2、创建与数据库的链接Connection对象;

  3、创建SQL语句对象preparedStatement(sql);

  4、提交SQL语句,根据实际情况使用executeQuery()或者executeUpdate();

  5、显示相应的结果;

  6、关闭数据库;

如图:

从接口、抽象类到工厂模式再到JVM来总结一些问题

通用的类图如下:

从接口、抽象类到工厂模式再到JVM来总结一些问题

说白了就是在JavaEE 的 dao 层应用了简单工厂模式。

  这里插一句,我又想起了以前学习JavaEE的一些疑问:Java web 中dao 层和service层都使用接口,是否是为使用接口而使用接口?

     其实我个人认为,一些程序员确实没有搞懂为什么用接口,以至于逢类就要有接口!一些业务不复杂的场景下,真的没有必要这样做呢!但是心里要明白:前面都说了很多例子和理论了,不过学习就是不断重复,归纳的过程,再重复一下,引用网上:使用接口是为了调用与实现解耦,带来的好处是可以各干各的了,带来的坏处是从一个概念变成了两个概念,增加了系统的复杂度。衡量一下在具体场景中是弊大于利还是利大于弊,就可以做选择了。当然,在大部分场景下,还要考虑一个因素,就是你会不会写接口。没有良好接口设计能力的人,写出来的接口抽象不合理,等于没写,什么好处都得不到,只有坏处,这种情况下干脆别写。那怎么衡量你会不会写接口呢,我的经验是,至少见过一次写了接口后得到明确好处的例子。

  什么情况下需要各干各的?

  最简单的场景,写接口的是A,写实现的是B。当然大多数类似情况没必要真的建一个interface然后再让人家去implements,把方法的第一行写好,注释写好,代码提交上,里面的内容让B去填就行了。另一种情况,调用代码先于实现代码编写。比如A开发的是struts这种东西,那A先得搞个Action接口。再一种情况,多种业务的模式类似。此时这个接口类实际上相当于某一层的抽象。定义出一个层后,有多种实现,然后通过向调用端注入不同的实现类,实现不同的逻辑。如果这种注入不能在编译期完成的话,也就需要用接口抽象一下。

  dao这玩意儿是做数据库读写的。对应一下上面那几种情况:你作为项目架构师想写两行代码就让小弟加班干活然则自己去泡妹子的话,可能需要写个interface里面几个抽象的insert、delete之类的方法;项目在快速原型阶段如果客户满意就掏钱买oracle如果客户不满意就得免费MySQL的话,你可能需要定义个dao接口然后先用内存数据库写点能让原型跑起来的实现,等一切有定论了再说;每个类都有一个dao,每个dao都有crud基本方法的话你可能需要定义一个通用Dao接口然后搞点代码技巧不用一个个的去写体力代码从此登上人生巅峰。所以dao接口还是有用的。

  service,这玩意儿更得具体问题具体分析。不去抠理论的话,什么是service,我的理解就是一段实现了某个逻辑的代码组合。所以service是比dao更抽象的概念,严格来讲dao就是一种service。只不过在java web开发中,dao是个人人都得写的东西,所以都拿出来单说了。因此,后面说的service跟dao没有本质分别。

  从工序上说,你在写上一层的时候,会用到下一层提供的逻辑,具体表现形式就是各种各样的service类和里面的方法。上一层开搞的时候,一定会知道的一个事是下一层会干什么事,比如“将传入编号对应的人员信息设置为离职”,但下一层的代码不一定已经一行一行写出来了。所以这会儿需要有个接口,让写上层代码的人先能把代码写下去。有各种理由可以支持这种工序的合理性,比如一般来说,上一层的一行代码会对应下一层的好多行代码,那先让写上层代码的人写一遍,解决高端层面的bug,会提高很多效率。

  再从抽象角度说,不同业务模块之间的共用,不一定是共用某段代码,也可能是共用某段逻辑框架,这时候就需要抽象一个接口层出来,再通过不同的注入逻辑实现。比如模块1是登记学生信息,模块2是新闻发布,看上去风马牛不相及。但分析下来如果两个模块都有共同点,顺序都是;1、验证是否有权限 2、验证输入参数是否合法 3、将输入参数转化为业务数据 4、数据库存取 5、写log,那就可以写一个service接口,里面有上述5个方法,再分别写两个service实现。具体执行的时候,通过各种注入方法,直接new也好,用spring注入也好,实现不同的效果。

  当然上面的这种情况很少有人这么干,因为已经普遍到这个程度,再精化精化就是struts了,java web的各种mvc框架都提供机制给你干这个事。但是每个项目或产品,都应该可以用类似的思路抽象出一些东西,如果抽象合理,会很大程度的提高项目架构的合理性。这些搞定,那写个接口然后实现个mock用于单元测试这种事,信手拈来。

  说实话,总结到这里,都是之前的种种的疑问的解答和对概念理解的升华,也是为了复习用,但是说来说去就是那些东西,高内聚,低耦合,开闭原则,单一职责原则,面向接口编程原则,业务和数据分离,工作内容能分离,解耦,封装,多态,代码隐藏……其实就是反复这些东西的举例,早已经在十几年前就让前辈和大牛们玩烂的东西,可悲……比如记得刚开始阅读《Thinging in Java》一书还有过这样的疑问:

  1、“接口与具体实现分离”,这里的具体实现到底是指什么?

  说白了,这里还是要联系到接口的重要使用目的之一:向上转型,利用接口可以被多个类去实现的特征来分离工作内容,分离不同的业务逻辑!而去灵活的插拔不同的但可以替换的实现方法!例如,有不同的动物,叫声不一样,我们只需要定义一个”叫声(xxx)“方法,而让牛、羊、青蛙等等去具体实现这个“叫声(xxx)”方法,我们调用的时候只需要 动物.叫声(xxx)就能发出对应动物的叫声了,详细看前面的总结。还有接口的一个重要使用目的之二:提供行为的统一的约束,避免实例化!也就是说和具体实现要分离!在这里只再简单重复一些内容:接口往往定义的是一些方法、行为,在设计原则里面有一条“单一职责“的原则,接口的作用只是提供一些方法、行为给你,它是不关心你是怎么使用的。就像电脑的USB接口,我们不需要关心这个USB接口是怎么实现的,我们只需能够使用这个USB接口。

  2、封装之后的类,源代码不是还是能被客户端程序员看到吗?数据域和方法还是暴露了啊,根本没有起到隐藏的作用。

  客户端不等于客户端程序员,可见性也不是针对程序员的,不要以自我为中心,其实很多问题,站在计算机的角度去看就一目了然了!客户端是指调用它的类或者具体对象,例如私有域是不对具体的对象暴露,封装性能够保证外部的对象或者实例不能修改它,从而保证了类的安全封装的作用不是指真的把所有代码实现都让客户端程序员看不到,这个隐藏的目的是让客户端在调用方法等行为时能够按照编写API 的程序员制定的规则来:一个变量不能让人家随便 objectA.x = ... 就修改了,也不能随便用 object.x 就能获得值,赋值要通过 setter,获得值需要 getter方法,变量相应的就要通过 private 来隐藏,举一个形象的例子:

从接口、抽象类到工厂模式再到JVM来总结一些问题

  这里是形容程序员给用户留了误操作的坑,但我觉得用来描述因封装不当、没有对类做好隐藏而导致 API 使用问题也是可以的:假设有一个类自行车,它有两个成员变量——*(wheel)和踏板(pedal)。你希望客户端调用的时候是『踩踏板』,也可能会提供『换*』这个方法,但决不能让用户可能在『踩踏板』的同时『换*』,甚至像图上那样把棍子插*的缝隙里,那么你就需要把*这个变量设为 private 隐藏起来,不让用户*获取,得按照你的getter 的规则来(比如骑车的时候就只返回 null,停止的时候就可以返回正常让用户可以做换*之类的工作)。setter 的作用也是类似的,都是为了保证客户端调用时能够遵循 API 设计者的规则,否则调用时就会出现各种不可控的乱象,轻则让调用者骂『这是哪个 SB 设计的 API,代码写起来太难管理了』,重则会出现很多意想不到的 bug,(也许实际上的封装应该是对*再做进一步的封装,那么就要用内部类的方式)。总之看书的时候对有些想很久也不能明白的东西不用太钻牛角尖,先照着书上说的做,等你写了足够的代码经验多一点了,自然就能理解一些东西为什么要那样做了。

  言归正传,继续之前的说,结合Java WEB的设计,具体说接口的作用,说了dao里的JDBC使用简单工厂模式操作数据库。业务逻辑层service……那么自然要谈谈MVC模式:三层构架和 MVC 不同吗?

  JavaWeb 开发中,服务器端通常分为表示层、业务层、持久层,这就是所谓的三层架构:

1、表示层负责接收用户请求、转发请求、显示数据等;

2、业务层负责组织业务逻辑;

3、持久层负责持久化业务对象。

这三个分层,每一层都有不同的模式,即架构模式,如下图。

从接口、抽象类到工厂模式再到JVM来总结一些问题

MVC是三个单词model,view,control的首字母缩写,意思是数据模型,视图,流程控制器这样三个意思,之前最开始学JavaWEB的时候,认为MVC就是JavaWEB里的三层架构,后来又认识到这样的想法不对,升华到认为MVC是表示层的东东,表示层最常用的架构模式就是MVC。因此,MVC是三层架构中表示层最常用的架构模式。PS:这个话题其实十年前Java 社区都已经讨论烂了,我真是悲剧,因为后来还一直认为MVC 意思是模型层、视图层、控制器层,其实哪里来的这么多层?是不是还要来个Service层、DAO层、DTO 层?这是不正确的认识……

  MVC就是模型、视图、控制器。而层的英文是tier(物理上)、layer(逻辑上),既是层就有类似计算机网络那种从上到下的递进关系,而模型、视图、控制器是没有从上到下的明确关系的,MVC并非谁创造的理论,它只是被赋予一个世界通用的名字,任何有经验有追求的程序员即使完全不知道MVC这个东西,都会走向MVC。

  故我的认知最后升华为 MVC 是模型,视图,控制器,和三层架构不是一个玩意!他们更不是什么视图层,模型层,控制器层!JavaWEB的三层架构是一个设计上的分层思想,三层架构设计中的每层都有自己的架构的模式,架构的模式就是每层的套路(类似武术里每种拳法或者脚法的套路),每一层都有自己的套路,就是所谓的架构模式。

  表示层最常用的套路就是MVC ,MVC就是表示层的一种架构模式。它不是设计模式!这是不同的概念!硬要扯上关系,只能说MVC——这个企业级开发架构中的表示层常用的架构模式包含了多个设计模式的思想,比如MVC架构模式是多种设计模式的复合,也可以说是复合模式的体现吧,它包含观察者模式,其中 View 、 Model 关系中加入的控制器就是为了把模型和视图分离,Model  和 Controller  之间是策略模式,表示对同样的数据可以有不同的处理策略(算法),Model 和View 之间是观察者模式,View 表示观察者, Model是主题,View订阅了一个主题(Model)……MVC 在实际环境中有很多的变体和改进,只要体会其中思想,无需严格跟概念相同,比如常见的有经典MVC和Web开发的MVC架构模式。PS:JavaWEB的学习一定要理解MVC模式!

  举个登录的例子,也就是Web MVC架构模式的应用:

1、View是返回给登录界面(比如浏览器)的视图对象,客户端只能看到视图view,不能和视图view交互,更看不到Model!(PS,这里一定注意,view不是指的界面!

2、Model是登录功能的计算模型,提供登录功能的所有接口的功能,比如网络请求处理,输入数据的校验,数据存储等

3、Controller是view和Model的桥梁,将主动通知 View,并将Model的结果反馈到View

从接口、抽象类到工厂模式再到JVM来总结一些问题

  登录用户也就是浏览器客户端(还是那句话,很多时候以计算机的角度看程序更容易理解程序)是和控制器Controller 交互的!而客户端看到的是 View ,它看不到model!客户端里输入用户名,密码》http请求》触发Controller去调用Model的请求数据的校验接口,Model 校验不符合,触发Controller选择登录失败的View返回给客户端,用户就看到一个登录失败的提示,之后客户端里输入正确的密码,重复之前的操作,如model校验success了,则触发Controller对对应策略的选择,再调用model的登录接口来请求登录(策略设计模式),model和数据存储系统交互,完毕之后触发controller更新view(观察者设计模式)……用户就看到登录成功的提示了。

  PS:下面是软件设计上经典的MVC架构模式:也要稍微的理解一下,还是那句话,法无定法,心中有法!

  从接口、抽象类到工厂模式再到JVM来总结一些问题

表示层告一段落。

  业务层的架构模式有事务脚本模式、领域模型模式等等,企业应用中最关键的显然是业务层。而对于初学者来说,事务脚本模式是最为简单,最容易掌握的,如果开发团队面向对象设计能力一般,而且业务逻辑相对简单,业务层一般都会采用事务脚本模式,因为简单!(如果业务逻辑复杂,用事务脚本模式就很不明智了,简单点讲,就是违背了单一职责设计原则,Service类成为万能的上帝,承担了太多职责……)那么什么是事务脚本模式呢?

  事务,就是表示层的一个请求,所谓脚本就是一个方法或者一个函数,所谓事务脚本就是将一次请求封装为一个方法或者一个函数,所谓事务脚本模式,就是将业务层的对象分为三类,按照下图的方式组织起来(简单的学生管理业务层UML类图设计):

从接口、抽象类到工厂模式再到JVM来总结一些问题

在事务脚本模式中,有三类对象。其中,Service类封装业务流程(或者说是纯粹的界面上的业务流程),DAO类只是负责对数据存储的操作,也就是封装对持久层的访问,DTO(Student)封装业务的实体对象,负责业务层内数据的传输流动。各个对象之间的关系如上图所示,service包含一个service接口和对应的service接口的实现类,dao类似,而dao和service是一种聚合关系,service类聚合了dao类。对于聚合:聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与cpu、公司与员工的关系等,表现在代码层面,和关联关系是一致的,只能从语义级别来区分;

从接口、抽象类到工厂模式再到JVM来总结一些问题PS;聚合关系的类图

对于DTO类,在dao和service中都有关系,属于依赖关系,即:dao,service依赖DTO类来传输数据对象,依赖:是类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖,表现在代码层面为:类B(DTO)作为参数被类A(dao,service)在某个method使用;这就是所谓业务逻辑的组织方式。

  还是有疑问:为什么要用Service接口和DAO接口?

  还得回到最基本的面向对象设计原则上去,有三条与此相关:

1、开闭原则

2、依赖倒转原则

3、里氏替换原则

还记得依赖倒转原则:高层不依赖于低层,二者都依赖于抽象,也就是面向接口编程。用Service接口是让表示层不依赖于业务层的具体实现。用DAO接口是让业务层不依赖于持久层的具体实现有了这两个接口,Spring IOC容器才能发挥作用。

从接口、抽象类到工厂模式再到JVM来总结一些问题

举个例子,用DAO接口,那么持久层用Hibernate,还是用iBatis,还是 JDBC,随时可以替换,不用修改业务层Service类的代码。dao,service使用接口的意义就在此。

  持久层的架构模式有入口模式(iBatis、JDBC是入口模式的典型实践)、数据映射器模式(Hibernate即是数据映射器架构模式的典型的实践)等等。

  一般来说,框架 > 架构模式> 设计模式 > 设计原则。打个比方,Hibernate是一个持久层框架,是数据映射器架构模式的具体实现,实现时用到了工厂模式等设计模式,体现了依赖倒转原则、开闭原则、里氏替换等等设计原则。SpringMVC 是一个客户端 MVC 框架,是 MVC架构模式的一种实现,实现时用到……设计模式等,体现了……设计原则,诸如此类。

  好了,前面谈了那么多,貌似有些跑,继续说之前提到的简单工厂模式,严格来说,这不算一个设计模式……而且在业务量比较多了之后,它也有诸多的隐患,由于工厂类集中了所有实例的创建逻辑,这就直接导致一旦这个工厂出了问题,所有的客户端都会受到牵连;而且由于简单工厂模式的产品基于一个共同的抽象类或者接口,这样一来,但产品的种类增加的时候,即有不同的产品接口或者抽象类的时候,简单工厂类就需要维护大量的if-else判断!比如导入导出功能,就拿导出功能来说。有这么一个需求:XX系统需要支持对数据库中的员工薪资进行导出,并且支持多种格式如:HTML、CSV、PDF等,每种格式导出的结构有所不同,比如:财务跟其他人对导出薪资的HTML格式要求可能会不一样,因为财务可能需要特定的格式方便核算或其他用途。如果使用简单工厂模式,则工厂类必定过于臃肿。因为简单工厂模式只有一个工厂类,它需要处理所有的创建的逻辑。假如以上需求暂时只支持3种导出的格式以及2种导出的结构,那工厂类则需要6个if else来创建6种不同的类型。如果日后需求不断增加,则后果不堪设想,违背了单一职责,导致系统丧失灵活性和可维护性。而且更重要的是,简单工厂模式违背了OCP——“开放封闭原则”,就是违背了“系统对扩展开放,对修改关闭”的原则,因为当我新增加一个产品的时候必须修改工厂类,相应的工厂类就需要重新编译一遍。

  还是举那个水果的例子:之前水果园子里只有苹果和香蕉两种水果,现在我们又新加一个梨的品种,代码如下:

public class FruitFactoryA {
    public static FruitA getFruit(String type) {
        if ("apple".equalsIgnoreCase(type)) {
            return new AppleA();
        } else if ("banana".equalsIgnoreCase(type)) {
            return new BananaA();
        } else if ("pear".equalsIgnoreCase(type)) {
            return new PearA();
        } else {
            System.out.print("error!");
        }

        return null;
    }
}

简单工厂类

客户端:

public class Main {
    public static void main(String[] args) {
        FruitA apple = FruitFactoryA.getFruit("apple");
        FruitA banana = FruitFactoryA.getFruit("banana");
        FruitA pear = FruitFactoryA.getFruit("pear");

        // 不太好,没有检测null异常,演示
        apple.get();
        banana.get();
        pear.get();
    }
}

  确实如上面谈的,简单工厂设计模式违背了OCP原则,当然还有单一职责原则(虽然这里没具体体现)。那么如何改进呢?也就是说,如何才能实现我们的工厂类的代码不去修改,关闭修改,而开放扩展!那么我们就可以把工厂的职责抽象一下!抽象if-else判断逻辑:把每一个判断都抽象为一个工厂类,而这些具体的工厂类统一抽象为一个接口来约束具体子类去生产产品,这就是传说中的——工厂方法模式,它同样属于类的创建型模式,又被称为多态工厂模式 。

  工厂方法模式的意义是定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类当中。核心工厂类不再负责产品的创建,这样核心类成为一个抽象工厂角色,仅负责具体工厂子类必须实现的接口(仅仅起到一个约束生产动作的作用),这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。代码如下:

  1、抽象工厂角色:工厂方法模式的核心,任何工厂类都必须实现这个接口。

public interface FactoryA {
    FruitA getFruit();
}

  2、具体工厂( Concrete  Creator)角色;具体工厂类是抽象工厂的一个实现,负责实例化产品对象。

public class AppleFactoryA implements FactoryA {
    @Override
    public FruitA getFruit() {
        return new AppleA();
    }
}

public class BananaFactoryA implements FactoryA {
    @Override
    public FruitA getFruit() {
        return new BananaA();
    }
}

public class PearFactory implements FactoryA {
    @Override
    public FruitA getFruit() {
        return new PearA();
    }
}

  3、抽象(Product)角色;工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。

public interface FruitA {
    void get();
}

  4、具体产品(Concrete Product)角色;工厂方法模式所创建的具体实例对象

public class AppleA implements FruitA {
    @Override
    public void get() {
        System.out.println("苹果");
    }
}

public class BananaA implements FruitA {
    @Override
    public void get() {
        System.out.println("香蕉");
    }
}

public class PearA implements FruitA {
    @Override
    public void get() {
        System.out.println("梨");
    }
}

客户端

public class Main {
    public static void main(String[] args) {
        // 得到对应的水果的工厂
        FactoryA appleF = new AppleFactoryA();
        FactoryA bananaF = new BananaFactoryA();
        FactoryA peatF = new PearFactory();

        // 通过各个工厂去得到对应的水果
        FruitA apple = appleF.getFruit();
        FruitA banana = bananaF.getFruit();
        FruitA pear = peatF.getFruit();

        apple.get();
        banana.get();
        pear.get();
    }
}

  如上代码实现,如果以后有新的水果橘子出现,那么我们不用修改工厂类,只需要add一个橘子以及橘子的工厂类且橘子工厂同时去遵循工厂的接口即可,客户端调用就哦了,而其他已经写好的工厂类无需修改!完全符合OCP原则,同时每个具体工厂子类只负责对应水果的生成,也遵守了单一职责原则!类图如下:

从接口、抽象类到工厂模式再到JVM来总结一些问题

  小结:简单工厂模式和工厂方法模式

  工厂方法模式与简单工厂模式在结构上的不同不是很明显。工厂方法类的核心是一个抽象工厂类,而简单工厂模式把核心放在一个具体类上。 工厂方法模式之所以有一个别名叫多态性工厂模式是因为具体工厂类都有共同的接口,或者有共同的抽象父类。系统扩展需要添加新的产品对象时,仅仅需要添加一个具体对象以及一个具体工厂对象,原有工厂对象不需要进行任何修改,也不需要修改客户端原有的代码,很好的符合了“开放-封闭”原则。而简单工厂在添加新产品对象后不得不修改工厂方法,扩展性不好。工厂方法模式退化后可以演变成简单工厂模式!

  JAVA的API里使用了工厂方法模式的也很多很多,常用的比如:Calendar类,该类是一个抽象类,它为特定瞬间与一组诸如 YEARMONTHDAY_OF_MONTHHOUR日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。如图:

从接口、抽象类到工厂模式再到JVM来总结一些问题

源码部分:

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
    public static Calendar getInstance()
    {
        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }
}

我们看的,该节选的片段有一个静态的getInstance()方法,return了一个具体的日历对象,Calendar是工厂方法模式里的抽象工厂类(这里使用的抽象类表示)。而具体工厂子类(简单举几个例子)有JapaneseImperialCalendar,GregorianCalendar……

    private static Calendar createCalendar(TimeZone zone,
                                           Locale aLocale)
    {
        CalendarProvider provider =
            LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
                                 .getCalendarProvider();
        if (provider != null) {
            try {
                return provider.getInstance(zone, aLocale);
            } catch (IllegalArgumentException iae) {
                // fall back to the default instantiation
            }
        }

        Calendar cal = null;

        if (aLocale.hasExtensions()) {
            String caltype = aLocale.getUnicodeLocaleType("ca");
            if (caltype != null) {
                switch (caltype) {
                case "buddhist":
                cal = new BuddhistCalendar(zone, aLocale);
                    break;
                case "japanese":
                    cal = new JapaneseImperialCalendar(zone, aLocale);
                    break;
                case "gregory":
                    cal = new GregorianCalendar(zone, aLocale);
                    break;
                }
            }
        }
        if (cal == null) {
            // If no known calendar type is explicitly specified,
            // perform the traditional way to create a Calendar:
            // create a BuddhistCalendar for th_TH locale,
            // a JapaneseImperialCalendar for ja_JP_JP locale, or
            // a GregorianCalendar for any other locales.
            // NOTE: The language, country and variant strings are interned.
            if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
                cal = new BuddhistCalendar(zone, aLocale);
            } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                       && aLocale.getCountry() == "JP") {
                cal = new JapaneseImperialCalendar(zone, aLocale);
            } else {
                cal = new GregorianCalendar(zone, aLocale);
            }
        }
        return cal;
    }

  上面的代码属于private,是属于getInstance方法里的。也就是说Calendar这个抽象工厂类返回具体子类的对象,JDK里这样说的:“与其他语言环境敏感类一样,Calendar 提供了一个类方法 getInstance,以获得此类型的一个通用的对象。CalendargetInstance 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化……”,客户端通过该方法返回对应的日历工厂,客户端再通过这些日历工厂去生成对应的日历产品……将来如果需要支持某个其他地区的特殊历法,程序员除了必要的add对应的日历工厂并extends Calendar,且add对应的日历产品之外,只需要增加Calendar的getInstance方法的新的逻辑,但Calendar的使用者无需承担这种变化的影响,符合OCP。

  好了貌似我们的问题又解决了!不过,现在又有了新的需求:果厂里新进了一批进口水果的种:进口香蕉,进口苹果,进口梨,同样的采集水果,之前的程序咋弄?我们之前的程序只是对工厂进行了抽象,使得不同的产品对应各自的工厂,而这里的产品仅仅是国内水果,现在涉及到了进口水果,现在有了两大类的产品,每个产品又分为不同的等级!我们叫它产品族。

  先熟悉下产品族的概念:所谓产品族,是指位于不同产品等级结构中,功能相关联的产品组成的家族,每一个产品族中含有产品的数目与产品等级结构的数目是相等的。产品的等级结构与产品族将产品按照不同方向划分,形成一个二维的坐标系,横轴表示产品的等级结构,纵轴表示产品族只要指明一个产品所处的产品族以及它所属的等级结构,就可以唯一的确定这个产品。

从接口、抽象类到工厂模式再到JVM来总结一些问题

  国产水果和进口水果就是产品族(纵坐标)表示多个产品等级结构,苹果,香蕉,鸭梨就是产品的等级结构。这个业务就相对复杂了,所以之前的模式就变得不好用。如果硬要使用,那就是在具体的苹果工厂类里再add一个新的方法——返回进口苹果类的实例,同时也为进口苹果add对应的进口苹果类,同理对于香蕉,鸭梨也是一样的,好像也没什么问题,但是呢,现在需求又变了,我们的果园厂子增加了温室种植技术,现在有了一种新的产品——温室种植水果!如何写代码?自然就要在具体的苹果工厂类里再次add一个新的方法——返回温室苹果类的实例,同时也要增加温室苹果这个新的产品……明显违背了OCP原则。而且苹果工厂类既能生成进口苹果也能生产国产苹果,违背了单一职责原则。

  引入抽象工厂模式——抽象工厂模式是所有形态的工厂模式中最为抽象和一般性的。抽象工厂模式可以向客户端提供一个接口,使得客户端在不必指定产品的具体类型的情况下,能够创建多个产品族的产品对象。抽象工厂模式与工厂方法模式的最大区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则需要面对多个产品等级结构。

  在什么情况下应当使用抽象工厂模式?系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。

  比如XX应用系统有快捷版跟标准版两个版本,这就是两个产品族,而具体使用哪一个产品族是实施人员在部署的时候设置后台参数来决定的。同属于同一个产品族的产品是在一起使用的,这一约束必须在系统的设计中体现出来。比如:快捷版数据保存在XML文件中,而标准版数据保存在数据库中。将UserDAO和DeptDAO看作是这两个版本(产品族)拥有的产品,而抽象工厂有两个具体工厂实现类ShortcutFactory、StandardFactory负责分别创建快捷版ShortcutUserDAO、ShortcutDeptDAO和标准版StandardUserDAO、StandardDeptDAO。当用户操作删除一个部门DeptDAO的时候,先需要删除这个部门下的所有用户UserDAO。

  假如不使用抽象工厂可能会出现如下情况:后台删除用户时调用的是快捷版的ShortcutUserDAO类,删除部门时调用的是StandardDeptDAO类。这样就会出现问题。使用抽象工厂,那抽象工厂的具体实现类会帮你创建一个产品族中的一系列产品对象,如标准版的StandardFactory工厂会创建StandardUserDAO、StandardDeptDAO,快捷版的ShortcutFactory工厂会创建ShortcutUserDAO、ShortcutDeptDAO,起到了一定的约束作用,它所创建的都是同一个产品族中的一系列产品对象。防止出现上面例子中创建不同产品族中产品所带来的问题。

  比如,水果厂有进口水果,国产水果两个产品族,而具体获得哪个产品族是客户端调用决定的,同属于一个产品族的产品是在一起使用的,比如进口苹果,进口橘子,国产苹果,国产橘子等……苹果,橘子是这个两个产品族(y轴)拥有的产品等级(x轴),用户(客户端)可以调用某个产品族的某个产品,之前的工厂方法模式,就对应一个产品族的设计模式,只有一个水果工厂去维持各水果实体类(产品等级),只有x轴,没有y轴,客户端采集进口苹果,调用的是原先国产苹果工厂里新add的进口苹果生产方法……很别扭。而采用抽象工厂模式,就需要维持一个y轴,那么就是解耦各个产品族,提供一个接口给客户端,让客户端能在不指定具体类型的前提下,创建多个产品族……代码如下:

一个水果的接口,维持一个获得水果的规则,所有产品等级对象的父类(接口),它负责描述所有实例所共有的公共接口。

public interface Fruit {
    void get();
}

对应产品等级的结构:苹果和香蕉组成一个产品族的产品等级,这里升华为水果的抽象类,是具体产品的父类

public abstract class AppleA implements Fruit {
    // 因为横向x轴的产品等级,有苹果,香蕉,但是多了纵向的其他产品族的苹果,香蕉,那么产品的抽象要进一步体现出来,苹果类变为
    // 抽象基类,分别去维持多个和苹果相关的产品族对应的产品等级
    public abstract void get();
}

public abstract class BananaA implements Fruit {
    public abstract void get();
}

具体的产品

public class ForeignApple extends AppleA {
    @Override
    public void get() {
        System.out.println("进口苹果");
    }
}

public class ForeignBanana extends BananaA {
    @Override
    public void get() {
        System.out.println("进口香蕉");
    }
}

public class HomeApple extends AppleA {
    @Override
    public void get() {
        System.out.println("国产苹果");
    }
}

public class HomeBanana extends BananaA {
    @Override
    public void get() {
        System.out.println("国产香蕉");
    }
}

抽象工厂类——抽象工厂模式的核心,包含对多个产品等级结构的声明,任何具体工厂类都必须实现这个接口。

public interface FruitFactory {
    // 一个抽象的工厂类(这里是接口)去维持产品族——y轴
    // 每一个工厂子类(产品族)都有对应的获得产品等级的方法
    Fruit getApple();

    Fruit getBanana();
}

具体工厂类是抽象工厂的一个实现,负责实例化某个产品族中的产品等级的对象。

public class ForeignFruitFactory implements FruitFactory {
    @Override
    public Fruit getApple() {
        return new ForeignApple();
    }

    @Override
    public Fruit getBanana() {
        return new ForeignBanana();
    }
}

public class HomeFruitFactory implements FruitFactory {
    @Override
    public Fruit getApple() {
        return new HomeApple();
    }

    @Override
    public Fruit getBanana() {
        return new HomeBanana();
    }
}

客户端

public class Main {
    public static void main(String[] args) {
        // 获得某一个产品族
        FruitFactory fruitFactory = new ForeignFruitFactory();

        // 获得该产品族下的产品等级
        Fruit apple = fruitFactory.getApple();
        apple.get();

        Fruit banana = fruitFactory.getBanana();
        banana.get();

        // 获得国产水果产品族
        FruitFactory homeFruitFactory = new HomeFruitFactory();
        Fruit apple1 = homeFruitFactory.getApple();
        apple1.get();

        Fruit banana1 = homeFruitFactory.getBanana();
        banana1.get();
    }
}

  当我们引入温室水果的时候,我们除了必须要建立温室苹果类,温室香蕉类去继承对应的水果抽象类之外,只需要再建立一个温室水果工厂类去实现抽象工厂接口即可,已经存在的代码不需要修改!

  抽象工厂模式中的方法对应产品等级结构,具体子工厂对应不同的产品族。

从接口、抽象类到工厂模式再到JVM来总结一些问题

  抽象工厂模式的优点:

  1、分离接口和实现,客户端使用抽象工厂来创建需要的对象,而客户端根本就不知道具体的实现是谁,客户端只是面向产品的接口编程而已。也就是说,客户端从具体的产品实现中解耦。

  2、使切换产品族变得容易,因一个具体的工厂实现代表的是一个产品族,切换产品族只需要切换一下具体工厂。

  抽象工厂模式的缺点:不太容易扩展新的产品,如需要给整个产品族添加一个新的产品,那么就需要修改抽象工厂,这样就会导致修改所有的工厂实现类。比如增加橘子这个产品等级……也就是说纵向不怕扩展,横向不方便扩展。

  

  我们再看看JDK里哪些API使用了抽象工厂模式,常见有:DocumentBuilderFactory 使用了抽象工厂模式:使应用程序能够从 XML 文档获取生成 DOM 对象树的解析器。

  从接口、抽象类到工厂模式再到JVM来总结一些问题

public abstract class DocumentBuilderFactory extends Object

  DOM:Document Object Model的缩写,即文档对象模型。XML将数据组织为一颗树,所以DOM就是对这颗树的一个对象描叙。通俗的说是通过解析XML文档,为XML文档在逻辑上建立一个树模型,树的节点是一个个对象。我们通过存取这些对象就能够存取XML文档的内容。我们来看一个简单的例子,看看DocumentBuilderFactory 是如何使用的抽象工厂模式来操作一个XML文档的。这是一个XML文档:

<?xml version="1.0" encoding="UTF-8"?>
 <messages>
   <message>Good-bye serialization, hello Java!</message>
 </messages>

  我们需要把这个文档的内容解析到Java对象中去供程序使用,利用JAXP,首先我们需要 DocumentBuilderFactory 建立一个解析器工厂,以利用这个工厂来获得一个具体的解析器对象,获取 DocumentBuilderFactory 的新实例。此 static 方法创建一个新的工厂实例。

  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();  

  该静态方法源码

    public static DocumentBuilderFactory newInstance() {
        return FactoryFinder.find(
                /* The default property name according to the JAXP spec */
                DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"
                /* The fallback implementation class name */
                "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
    }

  我们在这里使用 DocumentBuilderFacotry 抽象类的目的是为了创建与具体解析器无关的程序,当 DocumentBuilderFactory 类的静态方法 newInstance() 被调用时,它根据一个系统变量来决定具体使用哪一个解析器。在应用程序获取对 DocumentBuilderFactory 的引用后,它可以使用工厂来配置和获取解析器实例。 又因为所有的解析器都服从于JAXP所定义的接口,所以无论具体使用哪一个解析器,代码都是一样的,所以当在不同的解析器之间进行切换时(各个解析器就是不同的产品族,比如有微软的解析器,有IBM的解析器……),只需要更改系统变量的值,而不用更改任何代码。这就是抽象工厂所带来的好处。

  当获得一个工厂对象后,使用它的静态方法newDocumentBuilder()方法可以获得一个DocumentBuilder对象,获取此类实例之后,将可以从各种输入源解析 XML。这些输入源有 InputStreams、Files、URL 和 SAX InputSources。

DocumentBuilder db = dbf.newDocumentBuilder();   

  这个对象代表了具体的DOM解析器(具体的某个产品族)。但具体是哪一种解析器,微软的或者IBM的,对于程序而言并不重要。然后利用这个解析器来对XML文档进行解析:

Document doc = db.parse("xxx.xml");   

  DocumentBuilder的parse()方法接受一个XML文档名作为输入参数,返回一个Document对象,这个Document对象就代表了一个XML文档的树模型。以后所有的对XML文档的操作,都与解析器无关,直接在这个Document对象上进行操作就可以了。而具体对 Document操作的方法,就是由DOM所定义的了。   

  综合对比工厂模式的三个类型:简单工厂模式,工厂方法模式,抽象工厂模式

  简单工厂模式:

从接口、抽象类到工厂模式再到JVM来总结一些问题

  工厂方法模式,有了进步,把简单工厂类进行改进,提升为一个抽象类(接口),把对具体产品的实现交给对应的具体的子类去做。解耦多个产品之间的业务逻辑。

从接口、抽象类到工厂模式再到JVM来总结一些问题

  前面都是针对一个产品族的设计,如果有多个产品族的话,就可以使用抽象工厂模式。

  抽象工厂模式的工厂不在是维护一个产品等级的某个产品(或者说一个产品结构的某个产品更好理解),而是维护产品结构里的所有产品(横向x轴),具体到代码就是多个方法去对应产品等级结构的各个产品,而具体的工厂类实现该抽象工厂接口,去对应各个产品族。每一个具体工厂对一个产品族,获得该产品族的产品结构(所有产品)!

从接口、抽象类到工厂模式再到JVM来总结一些问题

  抽象工厂模式中的方法对应产品等级结构,具体子工厂对应不同的产品族。

  想到一个面试题: 写一个简单的计算器,满足加减乘除运算。记得大一的时候学c语言,这就是一个课后的小实验,记得当时用c语言是这样设计的:

  逻辑比较简单:先接手计算数据的输入,进行计算,返回结果,下面用Java代码写下,面向过程版

public class Main {
    public static void main(String[] args) {
        // 必须先接手计算数据的输入
        // 进行计算
        // 返回计算结果
        System.out.println("******计算器********\n请输入第一个数:");
        Scanner scanner = new Scanner(System.in);
        String num1 = scanner.nextLine();

        System.out.println("请输入运算符:");
        String operation = scanner.nextLine();

        System.out.println("请输入第二个数:");
        String num2 = scanner.nextLine();

        System.out.println("开始计算。。。。。。");
        double result = 0;

        if ("+".equals(operation)) {
            result = Double.parseDouble(num1) + Double.parseDouble(num2);
        } else if ("-".equals(operation)) {
            result = Double.parseDouble(num1) - Double.parseDouble(num2);
        } else if ("*".equals(operation)) {
            result = Double.parseDouble(num1) * Double.parseDouble(num2);
        } else if ("/".equals(operation)) {
            if (Double.parseDouble(num2) != 0) {
                result = Double.parseDouble(num1) / Double.parseDouble(num2);
            } else {
                System.out.println("除数不能为0!");

                return;
            }
        }

        System.out.println(num1 + operation + num2 + " = " + result);
    }
}

面向过程的简单计算器

  面向过程的设计,本身没有错,但是如果面试的是java的相关职位,使用一门面向对象的语言这样写是非常危险的。更重要的是,一旦程序扩展,这样的代码是没有办法维护的,且代码不能很好的重用。缺点:完全面向过程设计,所有逻辑都集中在一个类(方法、函数里),缺少代码的重用……PS:这里的除法0异常检测也是考点之一!

  面向对象的设计:把各个操作抽象为一个个的类,加法类,减法类,乘法类,除法类……每个运算类的职责就是进行属于自己的运算符的计算,客户端去调用对应的运算类即可。代码如下:

public abstract class Operation {
    private double num1;

    private double num2;

    public double getNum1() {
        return num1;
    }

    public void setNum1(double num1) {
        this.num1 = num1;
    }

    public double getNum2() {
        return num2;
    }

    public void setNum2(double num2) {
        this.num2 = num2;
    }

    public abstract double getResult();
}

  具体的运算符子类

public class Add extends Operation {
    @Override
    public double getResult() {
        return this.getNum1() + this.getNum2();
    }
}

加法

  只用加法举例,其他省略。

  客户端片段:

        // 必须先接手计算数据的输入
        // 进行计算
        // 返回计算结果
        System.out.println("******计算器********\n请输入第一个数:");
        Scanner scanner = new Scanner(System.in);
        String num1 = scanner.nextLine();

        System.out.println("请输入运算符:");
        String operation = scanner.nextLine();

        System.out.println("请输入第二个数:");
        String num2 = scanner.nextLine();

        System.out.println("开始计算。。。。。。");
        double result = 0;
        // 类型转换
        double a = Double.parseDouble(num1);
        double b = Double.parseDouble(num2);

        if ("+".equals(operation)) {
            Operation o = new Add();
            o.setNum1(a);
            o.setNum2(b);

            result = o.getResult();

  写到这里,貌似比之前也没什么大的改变,只是使用了面向对象的一丢丢,使用了类……在客户端还是需要显式的去new对应的运算类对象进行计算,客户端里还是维护了大量的业务逻辑……继续改进,使用工厂模式——简单工厂模式:

public abstract class Operation {
    private double num1;

    private double num2;

    public double getNum1() {
        return num1;
    }

    public void setNum1(double num1) {
        this.num1 = num1;
    }

    public double getNum2() {
        return num2;
    }

    public void setNum2(double num2) {
        this.num2 = num2;
    }

    public abstract double getResult();
}

public class Add extends Operation {
    @Override
    public double getResult() {
        return this.getNum1() + this.getNum2();
    }
}

public class Sub extends Operation {
    @Override
    public double getResult() {
        return this.getNum1() - this.getNum2();
    }
}

简单工厂类(也可以使用反射机制)

public class OpreationFactory {
    public static Operation getOperation(String operation) {
        if ("+".equals(operation)) {
            return new Add();
        } else if ("-".equals(operation)) {
            return new Sub();
        }

        return null;
    }
}

调用者(客户端)

public class Main {
    public static void main(String[] args) {
        // 必须先接手计算数据的输入
        // 进行计算
        // 返回计算结果
        System.out.println("******计算器********\n请输入第一个数:");
        Scanner scanner = new Scanner(System.in);
        String num1 = scanner.nextLine();

        System.out.println("请输入运算符:");
        String operation = scanner.nextLine();

        System.out.println("请输入第二个数:");
        String num2 = scanner.nextLine();

        System.out.println("开始计算。。。。。。");
        double result = 0;
        // 类型转换
        double a = Double.parseDouble(num1);
        double b = Double.parseDouble(num2);

        Operation oper = OpreationFactory.getOperation(operation);

        // TODO 有空指针异常隐患
        oper.setNum1(a);
        oper.setNum2(b);
        result = oper.getResult();

        System.out.println(num1 + operation + num2 + " = " + result);
    }
}

  这样写,客户端(调用者)无需反复修改程序,也不需要关注底层实现,调用者只需要简单了解或者指定一个工厂的接口,然后去调用即可一劳永逸,而底层的修改不会影响调用者的代码结构——解耦!且每个类都各司其职,单一职责,看着还可以!但是这时候面试官说了,给我增加开平方运算!好了,回想之前的简单工厂设计模式,每次增加新的产品(开方)都需要去修改原来的代码——简单工厂类,这样不符合OCP!那么自然想到了工厂方法模式:

public interface OpreationFactory {
    Operation getOperation();
}

public class AddFactory implements OpreationFactory {
    @Override
    public Operation getOperation() {
        return new Add();
    }
}

只举个加法的例子得了

public abstract class Operation {
    private double num1;

    private double num2;

    public double getNum1() {
        return num1;
    }

    public void setNum1(double num1) {
        this.num1 = num1;
    }

    public double getNum2() {
        return num2;
    }

    public void setNum2(double num2) {
        this.num2 = num2;
    }

    public abstract double getResult();
}

public class Add extends Operation {
    @Override
    public double getResult() {
        return this.getNum1() + this.getNum2();
    }
}

调用者

public class Main {
    public static void main(String[] args) {
        // 必须先接手计算数据的输入
        // 进行计算
        // 返回计算结果
        System.out.println("******计算器********\n请输入第一个数:");
        Scanner scanner = new Scanner(System.in);
        String num1 = scanner.nextLine();

        System.out.println("请输入运算符:");
        String operation = scanner.nextLine();

        System.out.println("请输入第二个数:");
        String num2 = scanner.nextLine();

        System.out.println("开始计算。。。。。。");
        double result = 0;
        // 类型转换
        double a = Double.parseDouble(num1);
        double b = Double.parseDouble(num2);

        // TODO 这里又需要判断了
        if ("+".equals(operation)) {
            // 得到加法工厂
            OpreationFactory opreationFactory = new AddFactory();
            // 计算 +
            Operation oper = opreationFactory.getOperation();
            oper.setNum1(a);
            oper.setNum2(b);
            result = oper.getResult();
        }

        // ......

        System.out.println(num1 + operation + num2 + " = " + result);
    }
}

  真尼玛日乐购了!工厂方法模式虽然避免了每次扩展运算的时候,都修改工厂类,但是把判断的业务逻辑放到了客户端里!这简直了!各有缺点吧……不要为了面向对象而面向对象!不要为了面向对象而面向对象!不要为了面向对象而面向对象!不过还是可以改进的……使用反射动态加载类,且对重复代码进行提炼和封装,代码如下(简单举两个:+和-):

public interface OpreationFactory {
    Operation getOperation();
}

public class AddFactory implements OpreationFactory {
    @Override
    public Operation getOperation() {
        return new Add();
    }
}

public class SubFactory implements OpreationFactory {
    @Override
    public Operation getOperation() {
        return new Sub();
    }
}

public abstract class Operation {
    private double num1;

    private double num2;

    public double getNum1() {
        return num1;
    }

    public void setNum1(double num1) {
        this.num1 = num1;
    }

    public double getNum2() {
        return num2;
    }

    public void setNum2(double num2) {
        this.num2 = num2;
    }

    public abstract double getResult();
}

public class Add extends Operation {
    @Override
    public double getResult() {
        return this.getNum1() + this.getNum2();
    }
}

public class Sub extends Operation {
    @Override
    public double getResult() {
        return this.getNum1() - this.getNum2();
    }
}

public enum  Util {
    MAP {
        // TODO 写在配置文件里
        @Override
        public Map<String, String> getMap() {
            Map<String, String> hashMap = new HashMap<>();
            hashMap.put("+", "compute.object.AddFactory");
            hashMap.put("-", "compute.object.SubFactory");

            return hashMap;
        }

        public double compute(double a, double b, OpreationFactory opreationFactory) {
            Operation oper = opreationFactory.getOperation();
            oper.setNum1(a);
            oper.setNum2(b);

            return oper.getResult();
        }
    };

    public abstract Map<String, String> getMap();

    public abstract double compute(double a, double b, OpreationFactory opreationFactory);
}

public class Main {
    private static double result = 0;

    private static double a;

    private static double b;

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // 必须先接手计算数据的输入
        // 进行计算
        // 返回计算结果
        System.out.println("******计算器********\n请输入第一个数:");
        Scanner scanner = new Scanner(System.in);
        String num1 = scanner.nextLine();

        System.out.println("请输入运算符:");
        String operation = scanner.nextLine();

        System.out.println("请输入第二个数:");
        String num2 = scanner.nextLine();

        System.out.println("开始计算。。。。。。");

        a = Double.parseDouble(num1);
        b = Double.parseDouble(num2);

        Class clazz = Class.forName(MAP.getMap().get(operation));
        result = MAP.compute(a, b, (OpreationFactory) clazz.newInstance());

        System.out.println(num1 + operation + num2 + " = " + result);
    }
}

  好了,到这里也差不多了,虽然还有很多问题……关键是思想的掌握!

  前面提到了,接口不仅是为了避免该类被实例化,只是为了统一规则,而且接口的使用还有一个更加重要的原因是可以使用向上转型!利用接口可以让多个类去实现的特征去动态的替换不同的实现方式。比如方法的参数类型设定的是接口类型,传入的参数类型可以是实现该接口的任一个类的类型……客户端可以非常灵活的调用该方法,那么就产生了一个设计模式:是接口比较常见的用法—策略模式。还是一个问题:接口的常用用法都有什么,举例策略设计模式

 
  前面是从接口和抽象类说起,再总结到简单工厂方法模式——也叫静态工厂方法模式,其实严格来说,这不算是一个设计模式,只不过大家都习惯这样说而已。而且简单工厂、工厂方法、抽象工厂模式里得到对象的方式都非常简陋,而一般开发里也不是经常使用反射机制,这里还是有可以改进的余地的,我们可以使用静态工厂方法来代替直接new对象这个过程,这里要总结一个开发的小框架:服务提供者框架,这里面的静态工厂方法是不能和工厂模式混为一谈的。
 
 
参考资料:旁征博引众多经典文献和大师的书籍,和CSDN,知乎,ImportNew等高质量文章,以及加上自己的编程经验的总结得到此文——只用于个人知识的总结复习和分享反馈之用。且实在太多,无法一一署名,抱歉。