使用 IoC 反转控制的三种设计模式

摘要

对于许多开发人员来说,控制反演(IoC)都是一个模糊的概念,因为他们在现实世界中很少或没有被应用过。在最好的情况下,控制反演(IoC)可以加单的认为是等效于依赖注入(DI)。

对于许多开发人员来说,控制反演(IoC)都是一个模糊的概念,因为他们在现实世界中很少或没有被应用过。在最好的情况下,控制反演(IoC)可以加单的认为是等效于依赖注入(DI)。实际上,只有在翻转控制与依赖注入双方都只是反映翻转依赖管理控制的时候,才认为两者是等效的。虽然,依赖注入实际上是IoC的一种众所周知的形式。但是,事实上IoC却是一个相对更为广泛的软件设计范例,可以通过多种模式来进行实现。在本文中,我们将介绍依赖注入,观察者模式和模板方法模式如何实现控制反转的。

正如许多其他设计模式,是从各种各样的使用场景中总结出来的,IoC的实现方式,也是类似的一种适合开发者使用的折中方式:

  • 一方面,高度解耦组件的设计,以及将应用逻辑封装在一个单一的地方,是实现IoC的直接而又自然的一种方式。
  • 另一方面,上述实现需要至少需要构建一个间接层,然而在某些用例中,这可能又是一种过度设计了。

接下来,不妨看几个具体的实现,这将有助于您了解,如何在这些属性之间进行权衡折中。

IOC范式揭秘

控制反转是一种带有某些特征的模式。下面,给出了由Martin Fowler给出的一个IOC经典范例,该范例实现的功能是从控制台中收集用户数据。

public static void main(String[] args) {
    while (true) {
        BufferedReader userInputReader = new BufferedReader(
                new InputStreamReader(System.in));
        System.out.println("Please enter some text: ");
        try {
            System.out.println(userInputReader.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个用例中,在main方法中进行流程控制:在无限循环调用中,读取用户输入,并将读取的内容输出到控制台上。完全又main方法控制何时去读取用户输入,何时去输出。

考虑下,上述程序的一个新版本,该版本中需要通过图形界面中的文本框来收件用户输入,另外还有个按钮,该按钮上绑定有一个action监听器。这样的话,用户每次点击按钮,输入的文本由监听器收集并打印到面板。

在这个版本的程序中,它实际上是由事件监听器模型(在这种情况下,这是框架)的控制下,调用开发者编写的用于读取和打印用户输入的代码。简单地说,框架将调用开发人员的代码,而不是其他方式。该框架实际上是一个可扩展的结构,它为开发人员提供了一组注入自定义代码段的切入点。

这种情况下,控制已经被有效的反转了。

从更通用的角度来看,由框架定义的每个可调用扩展点(以接口实现,实现继承(也称为子类)的形式)是IoC的一种明确定义的形式。

看下,下述这个简单的Servlet例子:

public class MyServlet extends HttpServlet {

    protected void doPost(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // developer implementation here
    }

    protected void doGet(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // developer implementation here
    }

}

此处,HttpServlet类(属于框架)是完全控制程序的元素,而不是MyServlet这个子类。在由servlet容器创建之后,当收到servlet的GET和POST的HTTP请求,doGet()和doPost()方法中的代码会分别自动调用。

与典型的继承方式相比,即子类是控制的元素,而不是基类,该例中,控件已经被反转了。

事实上,servlet的方法是模板方法模式的实现,稍后我们再深入讨论。

使用那些通过提供可扩展API,秉承开闭原则的框架时,使用框架的开发人员的角色,最终被归结为定义自己的一组自定义类,即开发人员要么通过实现框架提供的一个或多个接口方式,要么通过继承现有基类的方式。反过来,类的实例却是直接框架进行实例化,并且这些事例是被框架调用的。

此处引用Fowler的话:

该框架调用开发人员,而不是开发人员调用该框架。

因此,IoC通常被称为好莱坞原则:

不要打电话给我们,我们会打电话给你。

IOC的实现方式

该问题上,显而易见的是,实现控制反转是有几种不同方法的。我们不妨来总结一下,那些常见的实现方式。

注入依赖实现IOC

如前所述,注入依赖是IOC的一种实现方式,而且是最常见的一种面向对象设计方式。但是,思考一下:注入依赖究竟是如何达到控制反转效果的呢?

为了回答这个问题,我们给出如下一个原始的例子:

public interface UserQueue {

    void add(User user);

    void remove(User user);

    User get();

}

public abstract class AbstractUserQueue implements UserQueue {

    protected LinkedList<User> queue = new LinkedList<>();

    @Override
    public void add(User user) {
        queue.addFirst(user);
    }

    @Override
    public void remove(User user) {
        queue.remove(user);
    }

    @Override
    public abstract User get();

}

public class UserFifoQueue extends AbstractUserQueue {

    public User get() {
        return queue.getLast();
    }

}

public class UserLifoQueue extends AbstractUserQueue {

    public User get() {
        return queue.getFirst();
    }

}

UserQueue 接口定义了公共的API,用于在一个队列中去存放User对象(为了简单明了,此处忽略User的具体实现)。AbstractUserQueue则是为后续的继承类,提供了一些公用的方法实现。最后的UserFifoQueue 和 UserLifoQueue,则是分别实现了FIFO 和 LIFO 队列。

这是,实现子类多态性的一种有效方式。但是这具体用什么来买我们好处呢?实际上,好处还是蛮多的。

通过创建一个依赖于UserQueue抽象类型(也称为DI术语中的服务)的客户端类,可以在运行时注入不同的实现,无需会重构使用客户端类的代码:

public class UserProcessor {

    private UserQueue userQueue;

    public UserProcessor(UserQueue userQueue) {
        this.userQueue = userQueue;
    }

    public void process() {
        // process queued users here
    }

}

UserProcessor展示了,注入依赖确实是IOC的一种方式。

我们可以通过一些硬编码方式 如 new 操作,直接在构造函数中实例化在UserProcessor中获取对队列的依赖关系。但是,这是典型的代码硬编程,它引入了客户端类与其依赖关系之间的强耦合,并大大降低了可测性。耳边警钟声声想起啦!不是吗?是的,这样设计真的很挫。

该类在构造函数中声明对抽象类 UserQueue 的依赖。也就是说,依赖关系不再通过 在构造函数中使用 new 操作, 相反,通过外部注入的方式,要么使用依赖注入框架(如CDI和谷歌的Guice),要么使用factory或builders模式。

简而言之,使用DI,客户端类的依赖关系的控制,不再位于这些类中;而是在注入器中进行:

public static void main(String[] args) {
     UserFifoQueue fifoQueue = new UserFifoQueue();
     fifoQueue.add(new User("user1"));
     fifoQueue.add(new User("user2"));
     fifoQueue.add(new User("user3"));
     UserProcessor userProcessor = new UserProcessor(fifoQueue);
     userProcessor.process();
}

上述方式达到了预期效果,而且对UserLifoQueue的注入也简单明了。显而易见,DI确实是实现IOC的一种方式(该例中,DI是实现IOC的一个中间层)。

观察者模式实现IOC

直接通过观察者模式实现IOC,也是一种常见的直观方式。广义上讲,通过观察者实现IOC,与前文提到的通过GUI界面中的action监听器方式类似。但是在使用action监听器情况下,只有在特定的用户事件发生时(点击鼠标,键盘或窗口事件等),才会发生调用。观察者模式通常用于在模型视图的上下文中,跟踪模型对象的状态的变迁。

在一个典型的实现中,一到多个观察者绑定到可观察对象(也称为模式术语中的主题),例如通过调用addObserver方法进行绑定。一旦定义了被观察者和观察者之间的绑定,则被观察者状态的变迁都会触发调用观察者的操作。

为了深入了解这个概念,给出如下例子:

@FunctionalInterface
public interface SubjectObserver {

    void update();

}

值发生改变时,会触发调用上述这个很简单的观察者。真实情况下,通常会提供功能更丰富的API,如需要保存变化的实例,或者新旧值,但是这些都不需要观察action(行为)模式,所以这里举例尽量简单。

下面,给出一个被观察者类:

public class User {

    private String name;
    private List<SubjectObserver> observers = new ArrayList<>();

    public User(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
        notifyObservers();
    }

    public String getName() {
        return name;
    }

    public void addObserver(SubjectObserver observer) {
        observers.add(observer);
    }

    public void deleteObserver(SubjectObserver observer) {
        observers.remove(observer);
    }

    private void notifyObservers(){
        observers.stream().forEach(observer -> observer.update());
    }
}

User类中,当通过setter方法变更其状态事,都会触发调用绑定到它的观察者。

使用主题观察者和主题,以下是实例给出了观察方式:

public static void main(String[] args) {
    User user = new User("John");
    user.addObserver(() -> System.out.println(
            "Observable subject " + user + " has changed its state."));
    user.setName("Jack");
}

每当User对象的状态通过setter方法进行修改时,观察者将被通知并向控制台打印出一条消息。到目前为止,给出了观察者模式的一个简单用例。不过,通过这个看似简单的用例,我们了解到在这种情况下控制是如何实现反转的。
第 15 段(可获 1.19 积分)

观察者模式下,主题就是起到”框架层“的作用,它完全主导何时何地去触发谁的调用。观察者的主动权被外放,因为观察者无法主导自己何时被调用(只要它们已经被注册到某个主题中的话)。这意味着,实际上我们可以发现控制被反转的”事发地“ – - – 当观察者绑定到主题时:

user.addObserver(() -> System.out.println(
            "Observable subject " + user + " has changed its state."));

上述用例,简要说明了为什么,观察者模式(或GUI驱动环境中的action监听器)是实现IoC的一种非常简单的方式。正是以这种分散式设计软件组件的形式,使得控制得以发生反转。

通过模板方法模式实现IoC

模板方法模式实现的思想是在一个基类中通过几个抽象方法(也称算法步骤)来定义一个通用的算法,然后让子类提供具体的实现,这样保证算法结构不变。

我们可以应用这个思想,定义一个通用的算法来处理领域实体:

public abstract class EntityProcessor {

    public final void processEntity() {
        getEntityData();
        createEntity();
        validateEntity();
        persistEntity();
    }

    protected abstract void getEntityData();
    protected abstract void createEntity();
    protected abstract void validateEntity();
    protected abstract void persistEntity();

}

processEntity() 方法是个模板方法,它定义了处理实体的算法,而抽象方法代表了算法的步骤,它们必须在子类中实现。通过多次继承 EntityProcessor 并实现不同的抽象方法,可以实现若干算法版本。

虽然这说清楚了模板方法模式背后的动机,但人们可能想知道为什么这是 IoC 的模式。

典型的继承中,子类调用基类中定义的方法。而这种模式下,相对真实的情况是:子类实现的方法(算法步骤)被基类的模板方法调用。因此,控制实际是在基类中进行的,而不是在子类中。
这也是 IoC 的典型例子,通过分层结构实现。这种情况下,模板方法只是可调的扩展点的一个漂亮的名字,被开发者用来管理自己的一系列实现。

来自作者更多的文章

  • Validate Object Graphs with Java Bean Validation’s @Valid Annotation
  • Java Bean Validation’s validateProperty() and validateValue() in Three Minutes

总结

尽管控制反转普遍存在于 Java 的生态系统中,特别是很多框架普遍采用了依赖注入,但对于多数开发者来说,这个模式仍然很模糊,对其应用也受限于依赖注入。在这篇文章中,我展示了几种实际可用的实现 IoC 的方法,阐明了这一概念。

  • 依赖注入:从客户端获得依赖关系的控制不再存在于这些类中。它存由底层的注入器 / DI 框架来处理。
  • 观察者模式:当主体发生变化时,控制从观察者传递到主体。
  • 模板方法模式:控制发生在定义模板方法的基类中,而不是实现算法步骤的子类中。

像往常一样,怎样以及何时使用 IoC 是通过对每个用例的分析来决定的,不要为了 IoC 而 IoC。

IT家园
IT家园

网友最新评论 (0)