Java 注解是一个大错误?初次引入jdk引发讨论

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(483)   2023-10-19 07:14:57

注释 是在 Java 5 中引入的,我们都很兴奋。如此出色的工具可以缩短代码!不再有 Hibernate/Spring XML 配置文件!只是注释,就在我们需要它们的代码中。不再有marker interfaces,只有一个runtime-retained reflection-discoverable注解!我也很兴奋。此外,我制作了一些大量使用注释的开源库。以jcabi-aspects为例。但是,我不再兴奋了。而且,我认为注解是 Java 设计中的一个大错误。

长话短说,注释存在一个大问题——它们鼓励我们在对象的外部实现对象功能,这违反了封装<的原则/跨度>。该对象不再是实体的,因为它的行为不完全由它自己的方法定义——它的一些功能保留在别处。为什么不好?让我们看几个例子。

@Inject

假设我们用 @Inject 注释一个属性:

import javax.inject.Inject;
public class Books {
  @Inject
  private final DB db;
  // some methods here, which use this.db
}

然后我们有一个知道要注入什么的注入器:

Injector injector = Guice.createInjector(
  new AbstractModule() {
    @Override
    public void configure() {
      this.bind(DB.class).toInstance(
        new Postgres("jdbc:postgresql:5740/main")
      );
    }
  }
);

现在我们通过容器创建类 Books 的实例:

Books books = injector.getInstance(Books.class);

Books 不知道如何以及由谁将类 DB 的实例注入它。这将在幕后发生,不受其控制。注射会做到这一点。它可能看起来很方便,但这种态度会对整个代码库造成很大的损害。失去了控制(不是倒置,而是失去了!)。该对象不再负责。它不能对发生在它身上的事情负责。

相反,这是应该如何完成的:

class Books {
  private final DB db;
  Books(final DB base) {
    this.db = base;
  }
  // some methods here, which use this.db
}

本文解释了为什么依赖注入容器首先是一个错误的想法:依赖注入容器是代码污染者。注释基本上激发我们制作容器并使用它们。我们将功能移到对象之外,并将其放入容器或其他地方。那是因为我们不想一遍又一遍地复制相同的代码,对吧?没错,复制是不好的,但撕裂一个物体更糟。更糟。 ORM (JPA/Hibernate) 也是如此,其中正在积极使用注释。查看这篇文章,它解释了 ORM 的错误之处:ORM 是一种令人反感的反模式。注释本身并不是关键的激励因素,但它们通过将对象拆开并将零件放在不同的地方来帮助和鼓励我们。它们是容器、会话、管理器、控制器等。

@XmlElement

当您想将 POJO 转换为 XML 时,这就是 JAXB 的工作方式。首先,将 @XmlElement 注释附加到 getter:

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Book {
  private final String title;
  public Book(final String title) {
    this.title = title;
  }
  @XmlElement
  public String getTitle() {
    return this.title;
  }
}

然后,您创建一个编组器并要求它将类 Book 的实例转换为 XML:

final Book book = new Book("0132350882", "Clean Code");
final JAXBContext ctx = JAXBContext.newInstance(Book.class);
final Marshaller marshaller = ctx.createMarshaller();
marshaller.marshal(book, System.out);

谁在创建 XML?不是 book。类 Book 之外的其他人。这是非常错误的。相反,这应该是这样做的。首先,对XML一无所知的类:

class DefaultBook implements Book {
  private final String title;
  DefaultBook(final String title) {
    this.title = title;
  }
  @Override
  public String getTitle() {
    return this.title;
  }
}

然后,装饰器 将它打印到 XML:

class XmlBook implements Book{
  private final Book origin;
  XmlBook(final Book book) {
    this.origin = book;
  }
  @Override
  public String getTitle() {
    return this.origin.getTitle();
  }
  public String toXML() {
    return String.format(
      "<book><title>%s</title></book>",
      this.getTitle()
    );
  }
}

现在,为了以 XML 格式打印这本书,我们执行以下操作:

String xml = new XmlBook(
  new DefaultBook("Elegant Objects")
).toXML();

XML 打印功能在 XmlBook 中。如果你不喜欢装饰器的想法,你可以将 toXML() 方法移动到 DefaultBook 类中。这并不重要。重要的是功能始终位于它所属的位置——对象内部。只有对象知道如何打印自己 到 XML。没有其他人!

@RetryOnFailure

这是一个例子(来自我自己的图书馆):

import com.jcabi.aspects.RetryOnFailure;
class Foo {
  @RetryOnFailure
  public String load(URL url) {
    return url.openConnection().getContent();
  }
}

编译后,我们运行一个所谓的 AOP weaver,它在技术上将我们的代码变成这样的东西:

class Foo {
  public String load(URL url) {
    while (true) {
      try {
        return _Foo.load(url);
      } catch (Exception ex) {
        // ignore it
      }
    }
  }
  class _Foo {
    public String load(URL url) {
      return url.openConnection().getContent();
    }
  }
}

我简化了在失败时重试方法调用的实际算法,但我相信你明白了。 AspectJ,AOP 引擎,使用 @RetryOnFailure 注释作为信号,通知我们类必须被包装进入另一个。这是在幕后发生的。我们没有看到实现重试算法的补充类。但是 AspectJ 编织器生成的字节码包含类 Foo 的修改版本。

这正是这种方法的错误所在——我们看不到也不控制该补充对象的实例化。对象组合是对象设计中最重要的过程,隐藏在幕后的某个地方。你可能会说我们不需要看它,因为它是补充的。我不同意。我们必须了解我们的对象是如何组成的。我们可能不关心它们是如何工作的,但我们必须看到整个合成过程。

一个更好的设计看起来像这样(而不是注释):

Foo foo = new FooThatRetries(new Foo());

然后,FooThatRetries的实现:

class FooThatRetries implements Foo {
  private final Foo origin;
  FooThatRetries(Foo foo) {
    this.origin = foo;
  }
  public String load(URL url) {
    return new Retry().eval(
      new Retry.Algorithm<String>() {
        @Override
        public String eval() {
          return FooThatRetries.this.load(url);
        }
      }
    );
  }
}

现在,Retry 的实现:

class Retry {
  public <T> T eval(Retry.Algorithm<T> algo) {
    while (true) {
      try {
        return algo.eval();
      } catch (Exception ex) {
        // ignore it
      }
    }
  }
  interface Algorithm<T> {
    T eval();
  }
}

代码更长吗?是的。它更清洁吗?多很多。后悔两年前开始接触jcabi-aspects的时候没看懂。

底线是注释不好。不要使用它们。应该用什么代替?对象组成。

有什么比注释更糟糕的呢? 配置。例如,XML 配置。 Spring XML 配置机制是糟糕设计的一个完美例子。我已经说过很多次了。让我再重复一遍——Spring Framework 是 Java 世界中最糟糕的软件产品之一。如果你能远离它,你就会帮自己一个大忙。

OOP 中不应有任何“配置”。如果它们是真实的对象,我们就无法配置我们的对象。我们只能实例化它们。而最好的实例化方法是运算符new。该运算符是 OOP 开发人员的关键工具。将其从我们手中夺走并赋予我们“配置机制”是不可原谅的罪行。

  • Java 注解是一个大错误(网络研讨会 #14); 2016 年 5 月 4 日; 744 次浏览; 13 个赞
  • 依赖注入容器是个坏主意(网络研讨会 #9); 2015 年 12 月 1 日; 1264 次浏览; 19 个赞
  • 为什么 Getters-and-Setter 是一种反模式? (网络研讨会 #4); 2015 年 7 月 1 日; 3095 次浏览; 53 个赞
标签2: Java教程
地址:https://www.cundage.com/article/jcg-java-annotations-big-mistake.html

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...