何时(不)使用Java抽象类_抽象类

 

抽象类是许多面向对象语言的核心特性,例如Java。也许是因为这个原因,他们往往被过度使用,实际上被误用了。在本文中,我们将使用一些模式和反模式的示例来说明何时使用抽象方法,何时不使用。

虽然本文从Java的角度介绍了该主题,但它也与大多数其他面向对象的语言相关,即使那些没有抽象类概念的语言也是如此。为此,让我们快速定义抽象类。如果您已经知道抽象类是什么,请跳过以下部分。

定义抽象类

从技术上讲,抽象类是一个无法直接实例化的类。相反,它被设计为可以 实例化的具体类的扩展 。抽象类可以 - 通常也可以 - 定义一个或多个抽象方法,这些抽象方法本身不包含主体。相反,需要具体的子类来实现抽象方法。

让我们编写一个简单的例子:

 
  1. public abstract class Base {

  2. public void doSomething() {

  3.    System.out.println("Doing something...")

  4. }

  5. public abstract void doSomethingElse();

  6. }

请注意 doSomething() - 一个非抽象方法 - 实现了一个主体,而 doSomethingElse() - 一个抽象方法 - 没有实现主体。您无法直接实例化Base实例。试下下面的这段代码,你的编译器会报错:

 
  1. Base  b  =  new  Base();

事实上,你需要Base的一个子类,如下所示:

 
  1. public class Sub extends Base {

  2.    public abstract void doSomethingElse() {

  3.        System.out.println("Doin' something else!");

  4.    }

  5. }

请注意该doSomethingElse() 方法的实现 。

并非所有面向对象语言都具有抽象类的概念。当然,即使在没有这种支持的语言中,也可以简单地定义一个目的是被子类实现的类,并定义空方法或抛出异常的方法,作为子类重写的“抽象”方法。

瑞士军刀式的Controller

让我们来看看我经常遇到的抽象类的常见滥用。我一直感到内疚; 你可能也有。虽然这种反模式几乎可以出现在代码库中的任何地方,但我倾向于在控制器层的模型 - 视图 - 控制器(MVC)框架中看到它。出于这个原因,我称之为瑞士军刀式的Controller。

反模式很简单:许多子类只与它们位于技术堆栈中的位置相关,从一个公共抽象基类扩展而来。此抽象基类包含任意数量的共享“实用程序”方法。子类从自己的方法中调用实用程序方法。

瑞士军刀式的Controller 通常会这样存在:

  1. 开发人员使用Jersey 等 MVC框架开始构建Web应用程序 。

  2. 由于他们使用MVC框架,他们在UserController 类中使用端点方法支持他们的第一个面向用户的网页 。

何时(不)使用Java抽象类_Java_02

  1. 开发人员创建第二个网页,因此将新端点添加到控制器。一位开发人员注意到两个端点执行相同的逻辑 - 比如,在给定一组参数的情况下构造URL - 并将该逻辑移动到其中的单独 constructUrl() 方法中 UserController。

何时(不)使用Java抽象类_抽象类_03

  1. 团队开始研究面向产品的页面。开发人员创建第二个控制器, ProductController以便不将所有方法塞入单个类中。

  2. 开发人员认识到新控制器可能还需要使用该 constructUrl() 方法。与此同时,他们意识到 嘿!这两个类是控制器! 因此,必须与自然相关。因此,他们创建一个抽象 BaseController 类,移动 constructUrl() 到它,并添加 extends BaseController 到的类定义 UserController 和 ProductController。

何时(不)使用Java抽象类_Java_04

  1. 重复此过程,直到 BaseController 有十个子类和75个共享方法。

何时(不)使用Java抽象类_抽象类_05现在,有很多有用的方法可供具体类控制器使用,只需直接调用即可。所以有什么问题?

第一个问题是设计问题。事实上,所有这些不同的控制器彼此无关。它们可能位于我们堆栈的同一层,并可能执行类似的技术角色,但就我们的应用而言,它们用于不同的目的。然而,我们现在将它们锁定在一个相当随意的对象层次结构中。

第二个更实用。当你第一次需要使用 除控制器以外的其他地方的75个共享方法之一时,你会发现它 ,并且你发现自己实例化了一个控制器类来实现它。

 
  1. String url = new UserController().constructUrl(key, value);

您将创建一系列有用的方法,现在需要控制器实例才能访问。你的第一个想法可能是这样的, 嘿,我可以在控制器中使用静态方法,并像这样使用它:

 
  1. String url = UserController.constructUrl(key, value);

这不是更好,实际上,更糟糕。即使您没有实例化控制器,您仍然将控制器绑定到其他类。如果您需要在DAO层中使用该方法,该怎么办?您的DAO层应该对您的控制器一无所知。更糟糕的是,在引入一堆静态方法时,您已经使测试和模拟变得更加困难。

在此强调交互流程非常重要。在此示例中,直接调用其中一个具体子类的方法。然后,在某些时候,此方法调用抽象基类中的一个或多个实用程序方法。

何时(不)使用Java抽象类_Java_06

实际上,在这个例子中,从来没有需要抽象的基本控制器类。每个共享方法应该已经移动到适当的服务层类(如果它负责业务逻辑)或者实用程序类(如果它提供一般的补充功能)。当然,如上所述,实用程序类仍应是可实例化的,而不是简单地用静态方法填充。

何时(不)使用Java抽象类_Java_07

现在,有一组实用方法可以被任何可能需要它们的类重用。此外,我们可以将这些方法分解为相关的组。上图描绘了一个名为的类 UrlUtility, 它可能只包含与创建和解析URL相关的方法。我们也可以使用与字符串操作相关的方法创建一个类,另一个使用与我们的应用程序当前经过身份验证的用户相关的方法等。

另请注意,此方法也非常适合组合而不是继承的原则。

继承和抽象类是一个强大的构造。因此,许多例子都被滥用,瑞士军刀式的Controller就是一个常见的例子。实际上,我发现抽象类的大多数典型用法都可以被认为是反模式,抽象类有一些很好的用法。

模板方法

话虽如此,让我们看一下模板方法 设计模式描述的最佳用途之一 。我发现模板方法模式是一个鲜为人知的 - 但更有用 - 的设计模式。

您可以阅读有关模式如何在许多地方工作的信息。它最初是在 Gang of Four Design Patterns 一书中描述的; 现在可以在网上找到许多描述 。让我们看看它与抽象类的关系以及如何在现实世界中应用它。

为了保持一致性,我将描述使用MVC控制器的另一个场景。在我们的示例中,我们有一个应用程序,其中存在一些不同类型的用户(现在,我们将定义两个: employee 和 admin)。在创建任一类型的新用户时,根据我们创建的用户类型,存在细微差别。例如,分配角色需要以不同方式处理。除此之外,过程是一样的。此外,虽然我们预计新用户类型不会爆炸,但我们会不时要求我们支持新类型的用户。

在这种情况下,我们 将 要开始为我们的控制器的抽象基类。由于无论用户类型如何,创建新用户的整个过程都是相同的,因此我们可以在基类中定义该过程一次。任何不同的细节都将降级为具体子类将实现的抽象方法:

 
  1. public abstract class BaseUserController {

  2.    // ... variables, other methods, etc

  3.    @POST

  4.    @Path("/user")

  5.    public UserDto createUser(UserInfo userInfo) {

  6.        UserDto u = userMapper.map(userInfo);

  7.        u.setCreatedDate(Instant.now());

  8.        u.setValidationCode(validationUtil.generatedCode());

  9.        setRoles(u);  // to be implemented in our subclasses

  10.        userDao.save(u);

  11.        mailerUtil.sendInitialEmail(u);

  12.        return u;

  13.    }

  14.    protected abstract void setRoles(UserDto u);

  15. }

然后,我们只需要为每个用户类型扩展一次BaseUserController :

 
  1. @Path("employee")

  2. public class EmployeeUserController extends BaseUserController {

  3.    protected void setRoles(UserDto u) {

  4.        u.addRole(Role.employee);

  5.    }

  6. }

  7. @Path("admin")

  8. public class AdminUserController extends BaseUserController {

  9.    protected void setRoles(UserDto u) {

  10.        u.addRole(Role.admin);

  11.        if (u.hasSuperUserAccess()) {

  12.            u.addRole(Role.superUser);

  13.        }

  14.    }

  15. }

每当我们需要支持新的用户类型时,我们只需创建一个新的子类 BaseUserController 并setRoles() 适当地实现该 方法。让我们将这里的互动与我们与瑞士军队控制员看到的互动进行对比。

何时(不)使用Java抽象类_抽象类_08

使用模板方法方法,我们看到调用者(在这种情况下,MVC框架本身 - 响应Web请求 - 是调用者)调用抽象基类中的方法,而不是具体的子类。这一点在我们已经使子setRoles() 方法中实现的方法受到保护的事实中表明了 这一点。换句话说,大部分工作在抽象基类中定义一次。只有那些需要专业化的工作部分才能创建具体的实现。

经验法则

我喜欢将软件工程模式简化为简单的经验法则。当然,每条规则都有例外。但是,它能帮助我快速判断使用特定的设计是否是朝着正确的方向发展。

事实证明,在考虑使用抽象类时,有一个很好的经验法则。问问自己:类的调用者是否会调用在抽象基类中实现的方法,或者在具体子类中实现的方法?

如果它是前者,那么您打算只公开在抽象类中实现的方法- 可能性是您创建了一组良好的,可维护的类。

如果是后者,调用者将调用子类中实现的方法,而子类又调用抽象类中的方法。瑞士军队的反模式正在形成的可能性很大。

希望这些可以帮到你!请在下面的评论中告诉我们你的想法。

原文链接:https://dzone.com/articles/when-to-use-java-abstract-classes

作者:Dave Taubler

译者:xuli

号外:最近整理了之前编写的一系列内容做成了PDF,关注我并回复相应口令获取:

001 领取:《Spring Boot基础教程》

- 002 领取:《Spring Cloud基础教程》

更多内容陆续奉上,敬请期待 何时(不)使用Java抽象类_Java_09

 

- END -

 

 近期热文:

 

何时(不)使用Java抽象类_抽象类_10

 

何时(不)使用Java抽象类_抽象类_11