通过一个例子让你理解多态、控制反转和依赖注入的强大之处与重要性

257 阅读3分钟

多态与依赖注入

多态

什么是多态

菜鸟教程说:Java 多态是同一个行为具有多个不同表现形式或形态的能力。 多态就是同一个接口,使用不同的实例而执行不同操作,编译器会根据实际情况选择调用子类的方法。

多态的简单示例

每个字都认识,合在一起就有点不明白了。用代码来展现多态的话,它的效果就是这样的:

class Animal {
    public void makeSound() {
        System.out.println("动物发出叫声");
    }
}

class Cat extends Animal {
    public void makeSound() {
        System.out.println("喵喵喵");
    }
}

class Dog extends Animal {
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Cat();
        Animal animal2 = new Dog();

        animal1.makeSound(); // 输出 喵喵喵
        animal2.makeSound(); // 输出 汪汪汪
    }
}

可以发现,同样是 Animal 类,调用 makeSound 方法后却有了不同的结果,这就展现了多态!通俗的理解就是:“同一个类型有多种形态”

多态的实际运用

看完上面这个例子你可能会想,emmmm……这有什么用??我写系统又不需要输出“喵喵喵”

让我再给你举个例子。我们在写代码时常会遇到许多前置的校验,例如校验用户 Token 以判断用户是否已经登录,检查用户权限判断用户是否具有访问 API 的能力,检查用户是否在黑名单里以判断用户是否拦截用户请求……我们可以将这种种校验抽成接口 IPreAuthHandler:

// 前置校验器
public interface IPreAuthHandler {  
  
    /**  
     * 判断是否需要进行校验。暂时忽略入参,关注多态
     * @return  
     */  
    boolean checkNeedAuth();  
  
    /**  
     * 对一个Method对象进行注解检查。暂时忽略入参,关注多态 
     */  
    void doAuth() throws AuthenticationException;  
}

紧接着我们可以实现接口,例如验证 Token 的实现类和验证用户权限的实现类:

public class UserTokenAuthHandler implements IPreAuthHandler {  
  
    @Override  
    public boolean checkNeedAuth() {  
        // 检查是否需要验证
    }  
    @Override  
    public void doAuth() {  
	// 验证 UserToken 的逻辑
    }    
}

public class UserPremissionAuthHandler implements IPreAuthHandler {  
  
    @Override  
    public boolean checkNeedAuth() {  
        // 检查是否需要验证
    }  
    @Override  
    public void doAuth() {  
	// 验证用户权限的逻辑
    }    
}

上面的接口和实现类都没有设计方法参数,主要是因为实现该功能需要用到反射,会让例子变得复杂。所以这里就省略参数,我们只需要关注多态!

我们可以使用 List<IPreAuthHandler> 将所有用于验证的 Handler 归在一起:

public class FooService {
    private List<IPreAuthHandler> preAuthHandlers;

    public FooService() {
        preAuthHandlers = new ArrayList<>(2);
        preAuthHandlers.add(new UserTokenAuthHandler);
        preAuthHandlers.add(new UserPremissionAuthHandler);
    }

    public void doSomething() {
    	preAuth();
    	... // 详细的业务逻辑
    }

    private void preAuth() {
    	// 执行所有前置校验
    	for (IPreAuthHandler authHandler : preAuthHandlers) {
    	    if (authHandler.checkNeedAuth()) {
    	    	authHandler.doAuth();
    	    }
    	}
    }
}

可以发现,虽然我们有两个不同的 IPreAuthHandler 实现类,但通过多态,我们可以只关注父类,而忽略子类。多态让我们在调用同一个相同的父类时可以根据子类实现的不同而得到不同的结果,这使得我们可以用统一的方式使用 IPreAuthHandler,所以我们可以直接在 for 循环里调用两个、乃至无数个 IPreAuthHandler 方法

通过这个例子不难发现,多态是实现封装抽象的利器。我们通过接口与多态,将校验代码都封装到了一个个实现类里,对于使用接口的人而言,它只需要像我一样用 for 循环调用就好了,就算你有一百个、一千个实现类,我也压根不用去操心你是怎么实现的,我只管调用。这不就是封装吗?

至于抽象就更好理解了,一百个、一千个接口实现类都是同样的调用方法,那我就是将这成百上千的实现类都抽象为同一种类型,用统一的方式调用它们,这也是我可以用 for 循环实现这些实现类的前提。

依赖注入

有人会质疑上面的例子:我在使用 IPreAuthHandler 的时候,我还是需要去一个个实例化它们呀!那我这不还是要知道我要用什么实现类吗?知道用什么实现类的前提不就是我知道这个实现类是干什么的吗?何来的封装和抽象?

public class FooService {
    private List<IPreAuthHandler> preAuthHandlers;
    // 不可避免的实例化?
    public FooService() {
        preAuthHandlers = new ArrayList<>(2);
        preAuthHandlers.add(new UserTokenAuthHandler);
        preAuthHandlers.add(new UserPremissionAuthHandler);
    }
}

这时候就体现了 Spring 控制反转 (IoC) 与依赖注入 (DI) 的强大之处了。控制反转和依赖注入是 Spring 的核心。Spring 会在项目启动时,会将被注解标记的类实例化,并将实例化得到的对象放到 Spring 容器中。这其实就是把我们要用到的对象都集合起来放到一个箩筐里,要用的时候就拿出来用。

初学 Spring 的人常会疑惑:控制反转有什么用?为什么不让我自己实例化对象,要让 Spring 帮我?但用过一段时间 Spring 后我们不难发现,控制反转真的很省事,它不仅可以让我们省去一段段 new 的代码,还可以让 Spring 帮我们进行依赖注入,让我们“想用就用”,不用关心对象怎么来。但除了省事,它们还有一个重要的优点:解耦。

控制反转和依赖注入是如何解耦的?让我继续沿用刚刚的例子。使用 Spring 框架后,我们就可以为实现类添加注解,让它们在项目启动时创建,这样的话我就不需要一个个去 new 了。而在使用的时候,我们可以直接通过构造方法注入。此时 Spring 会根据多态,将符合类型要求的对象注入进来,也就是说,如果我在构造方法上声明了“我需要获取多个 IPreAuthHandler 实现类“,那么所有接口实现类都会被注入进来。

@Component
public class UserTokenAuthHandler implements IPreAuthHandler {  
    ...
}
@Component
public class UserPremissionAuthHandler implements IPreAuthHandler {  
    ...   
}
@Service
public class FooService {
    private List<IPreAuthHandler> preAuthHandlers;
    // 构造方法注入
    public FooService(List<IPreAuthHandler> handlers) {
    	this.preAuthHandlers = handlers;
    }
    // 统一的调用方式
    private void preAuth() {
    	// 执行所有前置校验
    	for (IPreAuthHandler authHandler : preAuthHandlers) {
    	    if (authHandler.checkNeedAuth()) {
    	    	authHandler.doAuth();
    	    }
    	}
    }
}

我们还可以借助 Lombok 省略构造方法,让代码变得更简洁:

@Service
@AllArgsConstructor // 通过 Lombok 自动创建构造方法
public class FooService {

    private List<IPreAuthHandler> preAuthHandlers;
	
    // 统一的调用方式
    private void preAuth() {
    	// 执行所有前置校验
    	for (IPreAuthHandler authHandler : preAuthHandlers) {
    	    if (authHandler.checkNeedAuth()) {
    	    	authHandler.doAuth();
    	    }
    	}
    }
}

现在让我们回来一开始的问题:我们还需要关心子类是什么吗?还需要关心子类的功能是检验Token还是检验权限的吗?我们还需要一个个实例化对象吗?都不需要!借助 Spring,我们很好地发挥了接口的封装、继承、多态和抽象的特点,完完全全地隐藏掉了实现类的底层逻辑,对外仅仅暴露一个统一的接口,让我们可以用统一的方式去使用无数个实现类。

如今你是否理解了多态、控制反转与依赖注入的强大之处?