1.WHAT-面向抽象编程的例子

1.1 面向对象

一般来说,面向对象编程是我们比较常见的,即将一个实际的项目分成多个类(对象),赋予这些对象属性和方法,从而实现编程。

比如,我们要编写一个校园管理平台,分别管理老师和学生,非常直观地我们就能把老师类和同学类给归纳出来,老师有教学的职能(方法)有性别年龄(属性),学生则有念书的职能(方法),也有性别年龄(属性)。

1.2 面向对象进阶到面向抽象

在我看来,面向对象其实就是:归纳总结。我们程序猿将产品经理提出的直观需求进行归纳总结,可以得到归纳的类。如果我们能把归纳的类再进一步地归纳(或者说抽象),我们就得到了一个抽象类。

同一个例子,除了老师类,学生类,我们还可以归纳一个“人”类,这个“人”类有着一个人应具备的属性(如性别,年龄,身高,体重)和能力(如吃饭,睡觉)。
(P.S:当然,归纳出一个抽象类只是面向抽象的一部分,并不是面向抽象的全部。)

2.WHY-为什么要面向抽象编程

2.1 代码编写

在代码编写层面,有过编程经验的我相信都会或多或少经历过:将公用的部分抽离出来,这样就不用重复编写代码。例如组件、包,我们不需要去编写重复性极强的代码。

2.2 代码维护

在代码编写层面,更多还是面向对象的思维在起作用。针对一个项目,我们正常的思维往往是自上而下的,因此先归纳出一个抽象类,再得到具体类,这是很正常的思维逻辑,这样写出来的代码,层次分明,但还是远远不够。

一个大型项目,往往难的不是它的开发,而是它的维护。大型项目里,即使我们用抽象类的方式将类进行了归纳总结,但类与类之间仍然不可避免地有着非常强的依赖关系。

举个栗子:我们A类抽象成IA类,但我们仍然要去实例化类A,而实例化类A的启动类B,依然引用了A,启动类B和A的依赖性仍然存在。(java的多态)

2.2.1 开闭原则(OCP)

首先,要想更好地实现代码维护,我们就要遵循开闭原则:允许拓展,禁止修改。
image.png
见上图,上图的四个齿轮是我们的四个类,它们互相依赖从而推动程序的运行。但产品经理有一天突然提出了一个新需求,这个需求可能我们直接修改类A就实现了,然而类A一旦发生变动,我们的类B,类D也得跟着动,然后连带着C也可能要跟着动,这样的变动是灾难性的。

由此可见,要遵循开闭原则,并不是一件容易的事情,这必须是在设计代码框架时就得思考清楚。那么到底怎么才能做到开闭原则呢?

2.2.2 面向抽象编程

我们需要明白:变化(产品经理)是固然存在的,是不可磨灭的。代码想要做到只拓展,完全不修改是不可能的。因此,遵循OCP原则,我们应该做到的是:
1.将代码的变化隔离至一处。
2.降低类与类之间的关联性,让每个类可以独立变化而不影响到其他类的使用。

而面向抽象编程正是将这些变化抽象,将类与类关联性降低。它有三个方面:
1.用变量抽象化“值”的变化;
2.用Interface接口抽象化“类”的变化;
3.用一个容器来封装类的实例化,再用反射机制来抽象化类的实例化。

2.2.3 工厂模式

面向抽象编程的第一个方面在日常编程中其实是非常常用的,像写配置文件,此处就不再多做赘述。

而第二点,上文已经提到过了,用一个抽象类来实现多个不同的具体类。具体在java中,就是用多态的形式引用类。

Fu fu = new Zi1()// new Zi2()

最主要还是第三点:用容器封装类的实例化。在类已经被抽象化的前提下,类的实例化成了类与类之间相互依赖的关键所在,正是因为一个类直接在另一个类中进行了实例化,两个类才产生耦合。

image.png
见上图,还是四个齿轮,但我这次在四个齿轮之间加入了一个大齿轮,今后不再是小齿轮互相推动运转,而是大齿轮带动四个小齿轮进行运转。显而易见,小齿轮之间的关联性已经被降低非常多了,之后不管是修改小齿轮的内部,或者是要更换小齿轮,都比原来的风险要小的多。

回到代码上来,这个大齿轮,其实就是一个容器,用这个容器实现类与类之间的关联。而要实现这个容器,我们可以采用工厂模式+反射机制

用具体的代码来实现一下工厂模式+反射机制:
假设我们需要做一个英雄联盟的选英雄,并释放R技能的简单demo。
单纯的工厂模式:

// 主控类
public class Main {
  public static void main(String[] args) throws Exception {
      String name = Main.getPlayerInput();
      Hero hero = HeroFactory.getHero(name);
      hero.r();
  }

  private static String getPlayerInput(){
      System.out.println("Enter a Hero's Name");
      Scanner scanner = new Scanner(System.in);
      String name = scanner.nextLine();
      return name;
  }
}
// 工厂类
public class HeroFactory {
  public static Hero getHero(String name) throws Exception {
      Hero hero;
      switch (name){
          case "Diana":
              hero = new Diana();
              break;
          case "Irelia":
              hero = new Irelia();
              break;
          case "Camile":
              hero = new Camile();
              break;
          default:
              throw new Exception();
      }
      return hero;
  }
}

乍看上去,好像我们通过抽象所有英雄有了一个Hero类,同时用工厂来生成它,已经很好地封装了,但是,我们每一次添加英雄,都需要去变动这个工厂,这个工厂作为我们的“大齿轮”,显然还不够资格,它连结了各个英雄类,直接修改它依然有影响连结的风险。
(感觉这个例子可能说服力不太强,因为各个英雄类之间关联性本身就不大,看起来修改工厂也问题不大,我只能直接从齿轮直观来解释了,不知道大家有无更好的例子- -。)。

工厂模式+反射机制:

public static Hero getHero(String name) throws Exception {
    String classStr = "reflect.hero." + name;
    Class<?> cla = Class.forName(classStr);
    Object obj = cla.newInstance();
    return (Hero) obj;
}

单纯的工厂模式,每一次都需要的变动是因为实例化过程还未被抽象,若是加上了java的反射机制,那就不同了,类的实例化完全取决于用户输入的英雄名(或者说配置文件中的英雄名)。无论我们新增任何英雄还是要调整任何英雄,该工厂都无需发生变化,

显而易见,这个工厂基本实现了我们图中的“大齿轮”,同时,我们可以看到,这样的一个大齿轮,很好地把我们控制代码运行的类和实现业务逻辑的类实现了分离————控制代码:MAIN+FACTORY;业务代码:各个英雄类DIANA+IRELIA+CAMILE。

因此,我们能将OCP原则换一种方式理解:尽量不要修改负责控制的代码,只修改业务逻辑的代码

3.IOC控制反转

假设有一个A类需要引用B类,那么一般来说,我们会在A类中实例化B类;而引入一个容器,将A类,B类都事先引入容器中,再由容器把B类注入到A类中,这就实现了IOC控制反转。

从这个概念来看,工厂模式+反射机制,这种模式看似已经实现了IOC控制反转(将英雄类注入到控制类之中)。但是实际情况中,生成类的工厂并非上面的例子那么简单,它可能需要根据某些特定的条件来判断注入哪一个类,还需要考虑如何实例化类:是否带参数,参数还可能发生变动。换句话说,我们的工厂不可能是一成不变的。

既然工厂还存在如此繁琐的业务逻辑变化,我们自然不能直接将工厂放入控制的代码中(即直接引用HEROFACTORY)。

同时,工厂模式+反射虽然隔离了类的实例化,但仍然没脱离正向思维:控制类(MAIN)需要英雄类(HERO),才去工厂中生成我们要的英雄。而我们希望的是:容器一开始就将英雄类注入到控制类之中,这才是IOC控制反转。

因此,我们需要Springboot框架,来帮我们扫描类生成bean,然后将bean注入到对应的控制类中。此处不过多做展开,待下次再记录一下对Springboot框架的相关理解。

自己的疑问:

1.工厂模式+反射已经解决了控制类不发生变动了,那么Spring框架相对于它的优势到底在哪呢?因为Spring框架还需要先将类生成bean加入到IOC容器之中,还多了这么一步,所以感觉较之工厂模式+反射来说还更加繁琐。


ljhtx
1 声望0 粉丝