【问题标题】:Arguments Against Annotations反对注解的论点
【发布时间】:2010-12-13 03:13:39
【问题描述】:

我的团队正在迁移到 Spring 3.0,有些人希望开始将所有内容都迁移到 Annotations 中。当我看到一个具有这样的方法的类时,我的直觉(代码味道?)真的很糟糕:(只是一个例子 - 不是所有真正的注释)

@Transaction
@Method("GET")
@PathElement("time")
@PathElement("date")
@Autowired
@Secure("ROLE_ADMIN")
public void manage(@Qualifier('time')int time) {
...
}

我只是落后于时代,还是这一切在其他人看来都是一个可怕的想法?而不是使用像继承和多态这样的OO概念,现在一切都按照约定或通过注释。我只是不喜欢它。必须重新编译所有代码来更改 IMO 的配置似乎是错误的。但这似乎是一切(尤其是春天)的方式。我应该只是“克服它”还是应该回击并尝试让我们的代码尽可能地免费?

【问题讨论】:

  • 我同意你(和其他人)的看法。我不喜欢我的代码中的配置。这就像硬编码,它被删除到配置文件中......现在又被重新放入带有注释的代码中。我发现它也确实使代码文件膨胀,这无助于可读性。
  • 你落后了。 XML 编程不再酷了。
  • 显然是annotatiomania的情况
  • “我只是落后于时代,还是这一切在其他人看来都是一个可怕的想法?” - 这是常规的 OR 还是 XOR?
  • @Gandalf 我来自 PHP 背景,在那里我学习了 2 个框架:Symfony(使用大量注释)和 Laravel(不使用)。我很幸运能够将 Symfony 抛在脑后,但现在必须学习 Spring,一想到必须再次“拥抱”注释,我就会颤抖。我只用了一个星期左右的框架,但我已经可以看到他们“仅作为元数据服务”的原则是 bs。如果您没有编写正确的注释,您的代码将失败,期间。这意味着注释是语言的一部分,不是附带的。我更喜欢 10 年前的你 ;-)

标签: java coding-style annotations


【解决方案1】:

实际上,我认为您内心的不好感觉更多地与这种将配置与代码混合在一起的注释有关。

我个人感觉和你一样,我更愿意将配置(例如事务定义、路径元素、控制器应该映射到的 URL 等)留在代码库本身和外部 Spring 之外XML 上下文文件。

我认为,虽然这里的正确方法归结为意见和您喜欢哪种方法 - 我预测一半的社区会同意注释方法,而另一半会同意外部配置方法。

【讨论】:

  • 请看我发现,在我的工作中,我通常认为“更好”的程序员真的不喜欢注解——但其他人都喜欢注解。现在,既然这些人的想法和我一样,这可能就是我将他们视为“更好”的程序员的原因。
  • 注释还不错。在我看来,JPA 对它们的使用非常棒。但凡事都有时间和地点,如果你的价值超过两行,你可能做错了什么。
  • 我认为每个人都将与他们想法相同的人视为“更好”的人:)
  • @matt b - 是的,这就是我提到相关性的原因:)
  • 我同意不将配置与代码混合。此外,您将代码与特定的 IOC 实现耦合在一起。例如,如果您决定从 Spring 更改为 Guice(考虑到它的变化不大,但它可能会发生),那么您将不得不更改所有代码而不仅仅是配置
【解决方案2】:

也许您对遍布代码的冗余注释有疑问。使用 meta-annotations 可以替换多余的注释,并且您的注释至少是 DRY。

来自 Spring 博客:

@Service
@Scope("request")
@Transactional(rollbackFor=Exception.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService {
}

@MyService
public class RewardsService {
…
}

由于 Java 的发展如此缓慢,人们将语言中缺少的更多功能放入注释中。这是一件好事,Java 可以以某种形式扩展,这是一件坏事,因为大多数注解都是一些解决方法并增加了复杂性。

【讨论】:

  • 我觉得我更不喜欢那个了。
  • 这里的问题是很多人不知道在编程语言中应该是什么而不是运行时库。让他们知道他们可以通过注释“扩展”语言会产生比恕我直言更多的问题。
  • 这个问题没有正确答案。使用 @Transactional 注释,如果无法注释您的类型,您将拥有 XML 配置或更强大的 AOP 解决方案。所以这个肯定是积极的。
  • 为什么您会对更改事务设置而不进行编译感兴趣,而您肯定必须通过 QA 构建/部署/测试周期来运行此类更改?
  • XML:查看多个位置以获得全貌。理想情况下,解决手头问题所需要知道的所有内容都可以放在一页上。如果您不需要配置,它只会增加复杂性。 AOP:AOP 非常强大。另一方面,它增加了复杂性(附加概念、新语言、复杂的运行时行为)。如果你用 AOP 替换 @Transaction 代理,你就是在用另一种更复杂的技术替换一个相当复杂的技术。
【解决方案3】:

我最初也对注释持怀疑态度,但看到它们在使用中,它们可能是一件很棒的事情。它们也可能被过度使用。

关于注解要记住的主要一点是它们是静态的。它们不能在运行时更改。任何其他配置方法(xml、代码中的自我描述等)都不会受到此影响。我在 SO 上看到人们在使用 Spring 时遇到问题,即在测试环境中注入测试配置,并且必须下拉到 XML 来完成它。

XML 不是多态的、继承的或其他任何东西,所以从这个意义上说,它不是倒退。

注解的优点是它可以让您对配置进行更多的静态检查,并且可以避免 XML 配置中的大量冗长和协调困难(基本上保持事物干燥)。

就像 XML 一样,注解可以被过度使用。重点是平衡每个人的需求和优势。注释,在某种程度上,它们为您提供更少冗长和 DRYer 代码,是一种可以利用的工具。

编辑:关于注释替换接口或抽象类的评论,我认为在框架边界上这是合理的在一个打算被数百甚至数千人使用的框架中在项目中,拥有一个接口或基类确实可以压缩一些东西(尤其是一个基类,虽然如果你可以使用注释来做到这一点,那么你没有理由不能使用常规接口来做到这一点。

考虑 JUnit4。以前,您必须扩展具有设置和拆卸方法的基类。就我的观点而言,它们是在接口上还是在基类中并不重要。现在我有一个完全独立的项目,它有自己的继承层次结构,他们都必须尊重这种方法。首先,它们不能有自己的冲突方法名称(在测试框架中没什么大不了的,但你明白我的意思)。其次,你有一个调用 super 的链条,因为所有的方法都必须是耦合的。

现在使用 JUnit4,您可以在层次结构中的不同类中拥有不同的 @Before 方法,并且它们可以相互独立。没有注释没有同样干燥的方法来完成这个。

从 JUnit 的开发者的角度来看,这是一场灾难。拥有一个可以调用 setUp 和 teardown 的已定义类型要好得多。但是框架的存在并不是为了框架开发者的方便,而是为了框架用户的方便而存在的。

如果您的代码不需要关心类型,所有这些都适用(也就是说,在您的示例中,无论如何都不会真正使用 Controller 类型)。那么你甚至可以说实现框架的接口比添加注释更容易泄漏。

但是,如果您要编写代码以在自己的项目中读取该注释,请远离。

【讨论】:

  • 我更大的问题甚至不在于配置方面,而是使用 @Controller 之类的东西,而不是使用名为 Controller 的接口/抽象类,然后扩展/实现它,这对我来说真的是糟糕的设计.如果您要忽略它,为什么还要使用 OO 语言?
【解决方案4】:

我个人觉得注释已经接管了太多,并且已经从它们最初的和超级有用的目的(例如,指示被覆盖的方法之类的小事)炸毁为这个疯狂的元编程工具。我觉得 JAva 机制不够健壮,无法在每个方法之前处理这些注释集群。 例如,这些天我正在与 JUnit 注释作斗争,因为它们以我不喜欢的方式限制了我

话虽如此,根据我的经验,基于 XML 的配置也不是很漂亮。所以引用南方公园的话,你是在巨型冲洗液和三文治之间做出选择。

我认为您必须做出的主要决定是您是否更愿意对 spring 配置进行非本地化(即维护两个文件而不是一个),以及您是否使用受益于注释。另一个重要的问题是,将使用或维护您的代码的开发人员是否真正了解注释。

【讨论】:

  • 唯一让 XML 配置文件不那么痛苦的是 IDE 的支持。如果没有 IDE 支持、没有类型安全、没有重构等以及所有这些字符串,那将是一场噩梦。不,真的,我觉得 XML 并不理想。但是你是对的,注释也不是很理想,至少不是所有的东西。
  • @Pascal:我主要使用的是 Hibernate,而不是 Spring,我发现 XML 版本是一个很大的痛苦,只要我只使用很少的几个,注释就容易一些。我对 Spring 的印象是,没有人理解的疯狂构造的可能性更高......
【解决方案5】:

就像很多事情一样,有利也有弊。在我看来,一些注解是好的,虽然有时感觉有过度使用注解的倾向,而一个普通的旧函数调用方法可能更好,并且作为一个整体,这可能会无意中增加认知负荷,因为它们增加了“做事”的方法。

让我解释一下。例如,我很高兴您提到了 @Transactional 注释。大多数 Spring 开发人员可能会了解并使用 @Transactional。但是有多少开发人员知道@Transactional 的实际工作原理?他们是否会在不使用 @Transactional 注释的情况下立即知道如何创建和管理事务?在大多数情况下,使用 @Transactional 可以让我更轻松地使用事务,但在特定情况下,当我需要对事务进行更细粒度的控制时,它会对我隐藏这些细节。所以在某种程度上它是一把双刃剑。

另一个例子是 Spring 配置类中的 @Profile。在一般情况下,它可以更容易地指定要加载 Spring 组件的配置文件。但是,如果您需要更强大的逻辑,而不仅仅是指定要加载组件的配置文件列表,则必须获取自己的 Environment 对象并编写一个函数来执行此操作。同样,大多数 Spring 开发人员可能会熟悉 @Profile,但这样做的副作用是他们不太熟悉它的工作原理,例如 Environment.acceptsProfiles(String...profiles) 函数。

最后,当注解不起作用时,可能更难理解原因,而且您不能只在注解上设置断点。 (例如,如果您忘记了配置中的@EnableTransactionManagement,会发生什么?)您必须找到注释处理器并对其进行调试。使用函数调用方式,当然可以在函数中设置断点。

【讨论】:

    【解决方案6】:

    注释必须谨慎使用。它们对某些人有好处,但并非对所有人都有好处。至少 xml 配置方法将配置保存在一个(或多个)文件中,而不是遍布各处。这将引入(我喜欢称之为)糟糕的代码组织。如果配置分布在数百个文件中,您将永远无法看到配置的全貌。

    【讨论】:

      【解决方案7】:

      注解通常会引入不属于此类依赖项的依赖项。

      我有一个类碰巧具有类似于 RDBMS 模式中表的属性的属性。该类是在考虑到这种映射的情况下创建的。类和表之间显然存在关系,但我很高兴使类不受任何声明该关系的元数据的影响。此类在完全不同的系统中引用表及其列是否正确?我当然不反对将两者联系起来并让彼此无法理解的外部元数据。我得到了什么?源代码中的元数据似乎并不提供类型安全或映射​​一致性。任何可以分析 JPA 注释的验证工具同样可以很好地分析休眠映射文件。注释没有帮助。

      在一份合同中,我创建了一个 maven 模块,其中包含来自现有包的接口实现包。不幸的是,这个新包是单体构建中的众多目录之一。我将其视为与其他代码不同的东西。尽管如此,团队正在使用类路径扫描,所以我必须使用注释才能将我的组件连接到系统中。在这里我不希望集中配置;我只是想要外部配置。 XML 配置并不完美,因为它将依赖关系与组件实例化混为一谈。鉴于 Rod Johnson 不相信基于组件的开发,这是公平的。尽管如此,我再次觉得注释对我没有帮助。

      让我们将其与不困扰我的东西进行对比:TestNG 和 JUnit 测试。我在这里使用注释是因为我知道我正在使用 TestNG 或 JUnit 来编写这个测试。如果我将一个替换为另一个,我明白我将不得不执行一个代价高昂的转换,这将偏离测试的重写。

      无论出于何种原因,我接受 TestNG、JUnit、QUnit、unittest 和 NUnit 拥有我的测试类。在任何情况下,JPA 或 Hibernate 都不拥有那些碰巧映射到表的域类。在任何情况下,Spring 都不拥有我的服务。我控制我的逻辑和物理包装,以隔离依赖其中任何一个的单元。我想确保离开一个人不会因为它留下的所有依赖关系而让我瘫痪。说再见总是比离开容易。在某些时候,离开是必要的。

      【讨论】:

        【解决方案8】:

        现在是 2018 年,这一点仍然很重要。

        我对注释的最大问题是你不知道注释在做什么。您正在切断一些调用方代码并将其隐藏在与被调用方断开连接的地方。

        引入注解是为了使语言更具声明性和更少程序化。但是,如果您将大部分功能转移到注释中,那么您实际上是在将您的代码切换到另一种语言(而不是一种很好的语言)。编译时检查很少。这篇文章表达了同样的观点:https://blog.softwaremill.com/the-case-against-annotations-4b2fb170ed67

        “将所有内容移到配置中,这样人们就不必学习如何编码”的整个启发式已经失控了。工程经理没有思考。

        例外:

        • JUnit
        • JAX-RS

        【讨论】:

          【解决方案9】:

          检查这些类似问题的答案

          What are the Pros/Cons of Annotations (non-compiler) compared to xml config files

          Xml configuration versus Annotation based configuration

          基本上归结为:同时使用。他们都有用例。不要将注释用于无需重新编译所有内容就应该保持可配置的内容(尤其是可能您的用户应该能够配置而无需您重新编译所有内容的内容)

          【讨论】:

          • 唯一可以配置的例子是@Secure("ROLE_ADMIN"),但是知道xml迷他们甚至会把@Method("GET")放在xml中。
          【解决方案10】:

          我认为这在一定程度上取决于您何时开始编程。就个人而言,我认为他们很可怕。主要是因为它们具有一些准“含义”,除非您碰巧意识到所讨论的注释,否则您将无法理解。因此,它们自己形成了一种新的编程语言,并使您远离 POJO。与(比如说)普通的旧 OO 代码相比。第二个原因——它们可以阻止编译器为你做你的工作。如果我有一个庞大的代码库并且想要重构某些东西或重命名某些东西,我理想情况下希望编译器抛出所有需要更改的东西,或者尽可能多地。注释应该就是这样。一个注解。不是代码行为的核心。它们最初被设计为在编译时可以选择省略,这会告诉你你需要知道的一切。

          是的,我知道 XML 配置也会受到同样的影响。这并没有让事情变得更糟,只是同样糟糕。至少我可以假装忽略这一点——它不会在每一个方法或参数声明中都盯着我看。

          考虑到选择,我实际上更喜欢可怕的旧 J2EE 远程/家庭接口等(最初受到 Spring 人的批评),因为这至少让我了解正在发生的事情,而无需研究 @CoolAidFrameworkThingy 及其弱点。 框架人员的问题之一是他们需要将您与他们的框架联系起来,以使整个企业在财务上可行。这与很好地设计框架(即让它尽可能独立且可从您的代码中移除)不一致。

          不幸的是,注释很流行。所以你很难阻止你的团队使用它们,除非你对代码审查/标准等感兴趣(而且,过时了!)

          我读到 Stroustup 在 C++ 中留下了注释,因为他担心它们会被误用。有时事情会朝着错误的方向发展几十年,但你可以希望事情能及时完成。

          【讨论】:

            【解决方案11】:

            我认为注释与度量一起使用是很好的。 @WebService 之类的注解在部署和运行时做了很多工作,但它们不会干扰类。 @Cachexxx 或 @Transactional 显然会通过创建代理和许多工件来干扰,但我认为它们是受控制的。

            当使用带有注释和 CDI 的 Hibernate 或 JPA 时,事情开始变得混乱。注释增长了很多。

            IMO @Service 和 @Repository 是 Spring 在您的应用程序代码中的干扰。它们使您的应用程序依赖于 Spring,并且仅供 Spring 使用。

            Spring Data Graph 的案例是另一回事。例如,@NodeEntity 在构建时向类添加方法以保存域对象。除非你有 Eclipse 和 Spring 插件,否则你会出错,因为这些方法在源代码中不存在。

            在对象附近配置有它的好处,而且是单一的配置点。注释可以衡量,但它们并不适用于所有事情,当注释行与源代码行一样多时,它们肯定会很糟糕。

            我认为 Spring 走的路是错误的;主要是因为在某些情况下,没有其他方法可以做这些有趣的事情。这就好像 Spring 想要进行极限编码,同时他们将开发人员锁定在 Spring 框架中。可能 Java 语言需要另一种方式来做一些事情。

            【讨论】:

              【解决方案12】:

              根据我的经验,注释很糟糕:

              • 无法在注释中强制执行类型安全
              • 序列化问题
              • 交叉编译(例如 javascript)可能是个问题。
              • 需要注释的库/框架会从外部库中排除未注释的类。
              • 不可覆盖或互换
              • 您的项目最终会严重依赖需要注释的系统

              如果 Java 有类似“方法文字”的东西,你可以在相应的注解类中注解一个类。 如下所示: 以 javax.persistence 和以下带注释的类为例:

              @Entity
              class Person
              {
                  @Column
                  private String firstname;
                  public String getFirstname() { return firstname; }
                  public void setFirstname(String value) { firstname = value; }
              
                  @Column
                  private String surname;
                  public String getSurname() { return surname; }
                  public void setSurname(String value) { surname = value; }
              
              }
              

              我建议使用映射类,而不是注释:

              class PersonEntity extends Entity<Person> {
                  @Override
                  public Class<Person> getEntityClass() { return Person.class;}
              
                  @Override
                  public Collection<PersistentProperty> getPersistentProperties() {
                       LinkedList<PersistentProperty> result = new LinkedList<>();
                       result.add(new PersistentProperty<Person>(Person#getFirstname, Person#setFirstname);
                       result.add(new PersistentProperty<Person>(Person#getSurname, Person#setSurname);
                       return result;
                  }
              }
              

              这个伪 java 代码中虚构的“#”符号表示一个方法文字,当在给定类的实例上调用该方法时,会调用该实例的相应委托(自 java 8 起用“::”签名)。 “PersistentProperty”类应该能够强制方法字面量引用给定的泛型参数,在本例中为 Person 类。

              这样,您可以获得比注释更多的好处(例如子类化您的“注释”类),并且您没有上述任何缺点。 你也可以有更多特定领域的方法。 唯一的预注释是,通过注释,您可以快速查看是否忘记包含属性/方法。但这也可以通过 Java 中更好的元数据支持来更简洁和更正确地处理(例如像 Protocolbuffers 中的必需/可选)

              【讨论】:

                猜你喜欢
                • 2021-11-30
                • 2014-03-01
                • 1970-01-01
                • 1970-01-01
                • 2011-08-05
                • 2011-03-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多