Java注释是一个大错误

注释是在Java 5中引入的,我们都为之兴奋。 如此出色的工具可以缩短代码! 不再有Hibernate / Spring XML配置文件! 只是注释,就在我们需要它们的代码中。 没有更多的标记接口 ,只有运行时保留的 反射可发现注释! 我也很兴奋。 此外,我制作了一些开源库,这些库大量使用注释。 以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编织器 ,该编织器从技术上将我们的代码转换为如下形式:

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();
    }
  }
}

我简化了在失败时重试方法调用的实际算法,但是我确定您能理解。 AOP引擎AspectJ使用@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个赞
  • 为何吸气与反吸是反模式? (第4场网络研讨会); 2015年7月1日; 3095次点击; 53个赞

翻译自: https://www.javacodegeeks.com/2016/11/java-annotations-big-mistake.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值