kim.zhang

风在前,无惧!


  • 首页

  • 标签42

  • 分类12

  • 归档94

  • 搜索

面向抽象编程.md

发表于 2020-08-10 更新于 2021-11-21 分类于 思想
本文字数: 4.1k 阅读时长 ≈ 4 分钟

在编程过程中啊,很多时候我们是用面向对象的思想编程。这是我们在编程过程中很自然就会去应用的思想。比如我们来模拟一下英雄联盟这个游戏的应用场景。

现在呢,我们有一个需求:当用户输入一个英雄的名称后,释放英雄的一个技能。

面对这个需求,首先我们很容易想到把英雄这个类抽象出来,创建一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
public class Diana {
public void Q(){ System.out.println("Diana Q"); }
public void W(){
System.out.println("Diana W");
}
public void E(){
System.out.println("Diana E");
}
public void R(){
System.out.println("Diana R");
}
}

创建好了对象,我们就应该去使用它。大多数情况下,我们是去调用它的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Main {

public static void main(String[] args) {
String name = Main.acceptPleyerInput();
String name = Main.acceptPleyerInput();
switch (name){
case "Diana":
Diana diana = new Diana();
diana.R();
break;
case "Yasoo":
Yasoo yasoo = new Yasoo();
yasoo.R();
break;
case "Ez":
Ez ez = new Ez();
ez.R();
}
}


public static String acceptPleyerInput(){
System.out.println("please enter a hero name");
Scanner scanner = new Scanner(System.in);
return scanner.nextLine();
}
}

代码中的第6行,当我们需要使用到Diana这个对象的时候,我们就会去创建它。但是仔细一想,这样直接new对象真的好么?实现需求肯定是能实现的,但这不是我们追求的目标,我们的目标是不仅实现需求,还追求代码的可维护性。试想,如果有一天Diana这个字符串对应的不再是Diana这个对象了,那么我们是不是需要去修改第6行的代码,去创建别的对象?在代码量少,业务简单的情况下,你可能觉得还好,改了就改了呗。但是一旦这个类被多个类所调用呢?是不是就得在每一个使用了Diana对象的类中进行修改?这样是不是很难维护呢?

所以,我们提出一个概念:不要面向具体的对象编程,要面向抽象编程。那么问题来了,有什么办法可以让我们更好的去面向抽象编程呢?

  1. interface、abstract
  2. 工厂模式
  3. IOC / DI

我们先来看第一种:使用interface

先定义一个Hexo接口,所有的英雄类实现这个接口。

1
2
3
4
5
6
public interface Hero {
void Q();
void W();
void E();
void R();
}

在使用了接口之后,我们还是需要new出每个对象,而且也没有优化掉switch..case..,那么,我们为什么还要抽象出接口呢?这里给出结论:单纯地使用interface能统一方法的调用,但无法统一对象的实例化。相比较第一版的代码,原本在switch..case..中的方法调用优化了,但是switch..case..没有优化掉。仔细分析,这里的switch..case..正是执行了对象的实例化,正好印证了结论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
String name = Main.acceptPleyerInput();
Hero hero = null;
switch (name) {
case "Diana":
hero = new Diana();
break;
case "Yasoo":
hero = new Yasoo();
break;
case "Ez":
hero = new Ez();
break;
default:
throw new RuntimeException();
}
hero.R();
}

那么,只是简化了方法的调用,意义大吗?

当然了,如果有很多个英雄呢?那岂不是要调用很多次R()吗?所以,统一方法的调用还是很有意义的。

从上面的分析,interface可以解决的是方法统一调用的问题,其实,抽象的难点在于new对象的优化。只有一段代码中没有new的出现,才能保持代码的相对稳定,才能逐步实现OCP(开闭原则)。而对象的实例化是不可能消除的,只能将它实例化的过程转移到其他的代码片段中。接下来,我们就利用简单工厂模式来转移对象的实例化代码。

首先,我们创建一个工厂,该工厂用来产生Hero的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BeanFactory {
public static Hero getHero(String name){
Hero hero;
switch (name) {
case "Diana":
hero = new Diana();
break;
case "Yasoo":
hero = new Yasoo();
break;
case "Ez":
hero = new Ez();
break;
default:
throw new RuntimeException();
}
return hero;
}
}

有了工厂之后,我们不再需要在main方法中使用switch..case..了。这段代码利用interface+工厂模式,已经将对象的实例化分离到工厂中,对main方法来说,它就是稳定的一段代码了吗?不一定,稳定是相对的。这段代码虽然分离了对象的实例化代码,但是它是巧妙地利用了BeanFactory的静态方法。假如BeanFactory不是静态的了,那么调用getHero()就需要变化。从另外一个角度来说,我们实现了与具体对象(通过new出来的对象)的低耦合,但是我们耦合了工厂对象。

1
2
3
4
5
public static void main(String[] args) {
String name = Main.acceptPleyerInput();
Hero hero = BeanFactory.getHero(name);
hero.R();
}

在使用了interface+简单工厂模式后,main方法已经是相对稳定的了。但是代码中总是会存在不稳定,我们要做的是隔离这些不稳定的代码,使其他代码相对稳定。假如这个BeanFactory能生产出任何的对象,那我们岂不是就不用修改main方法里的BeanFactory了吗,因为无论什么样的对象,工厂都能生产出来。其实,这就类似于Spring中ApplicationContext。

在这里停下来想一想,到底是什么导致了代码的不稳定呢?主要是两个方面:

1.用户的输入信息变化了,这和本例是一致

2.用户的需求变化了

假如说用户输入的不再是英雄的名字(字符串),而是能直接输入一个对象的话,那我们就消除了new对象,从而消除了switch..case…其实这在java中是可以利用反射实现的,只不过要求用户输入的必须是带上包名的完整的类的名称。

1
2
3
4
5
6
7
8
9
10
public class BeanFactory {
public static Hero getHero(String name) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 完整的类名称
name = "v1." + name;
// 通过反射创建对象
Class<?> clazz = Class.forName(name);
Object obj = clazz.newInstance();
return (Hero) obj;
}
}

我们利用了反射消除了switch..case这种根据用户输入字符串new对象的方式。但是要注意的是,我们需要对用户输入的字符串进行处理,处理成完整的类的名称。当用户输入字符串后,就相当于我们拿到了对应的对象。这里就类似于Spring中利用配置文件来反射对象。

上面的代码,我们利用了静态的BeanFactory,实际上已经实现了OCP原则。但唯一遗憾的是,如果BeanFactory变动了(比如原来的getHero方法改变了),那我们还是需要修改main方法的。那有什么办法再把BeanFactory分离出去,更好地实现OCP原则吗?方法是有的,那就是Spring的IOC。在之前的代码中,我们都是主动地去要,主动地调用BeanFactory的方法。一旦改变,就会违反OCP。而使用Spring的IOC,我们是被动地接收,不再主动地去要。这样就分离了BeanFactory,也就使得OCP更加稳定,这就是所谓的控制反转。反转的是资源(不仅仅只是包括对象)的获取途径,从主动获取反转为被动接收。

新的问题又出现了?怎么被动接收呢?通过方法传参?

这就涉及到了另外一个概念———依赖注入(DI),通常我们是在类的层面注入。就比如这里的main方法,虽然是在main方法中使用,但是通常是在main方法这个类中注入BeanFactory,这样main方法也可以去调用。

常用的两种依赖注入的方法:

1.属性注入

2.构造器注入

一毛也是爱~
Kim.Zhang 微信支付

微信支付

# 思想
文件复制.md
应对变化的两种方案.md
Kim.Zhang

Kim.Zhang

且行且珍惜
94 日志
12 分类
42 标签
E-Mail Weibo
粵ICP备19091267号 © 2019 – 2022 Kim.Zhang | 629k | 9:32
本站总访问量 4 次 | 有 309 人看我的博客啦 |
博客全站共176.7k字
载入天数...载入时分秒...
0%