注释 是在 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 开发人员的关键工具。将其从我们手中夺走并赋予我们“配置机制”是不可原谅的罪行。
标签2: Java教程地址:https://www.cundage.com/article/jcg-java-annotations-big-mistake.html