unity模型聚焦功能代码
在本文中,我将讨论代码生成,为什么需要代码生成以及如何进行代码生成。 我将描述使代码生成成为必要和一些理论(不是太多)的一般问题。 我还将讨论软件开发的不同阶段,在这些阶段中可以以编程方式生成源代码,并比较不同的方法。 我还将描述在特定阶段生成代码的特定工具的体系结构和想法(尤里卡时刻的种类)。
为什么我们生成代码
问题是要生成代码还是不生成代码。 我们可以问莎士比亚。 就像《哈姆雷特》中那样,实践给出了答案。 我们确实生成代码。 许多开发人员生成代码。 即使我们不喜欢那样。 工具生成的代码给我们带来了一些不好的感觉。 它使我们感到某些事情不是专业的,或者至少不是最优的。 但这就是生活。 只有少数几项是最佳的,我们仍然必须忍受这些。 自动代码生成也是如此。
自动生成代码的主要原因是我们并不了解。 手动代码生成可能很麻烦且容易出错,并且语言,框架或仅凭我们的经验和知识无法提供简单的解决方案。 这篇文章的主要内容一开始就是在这里。 在决定使用代码生成之前,我们必须确定原因,为什么需要它。
除非确实需要,否则不应该生成代码
很奇怪的说法,尤其是当我“推广”一个完全针对Java代码生成的FOSS工具时。 我知道并且仍然声明,您必须手动编写所有代码。 不幸的是,或者为了使用该工具,在很多情况下不能选择手动代码生成,或者至少自动代码生成似乎是一个更好的选择。
为什么要产生?
如果最好的选择是生成源代码,那么系统中就会出现错误或至少不是最优的。
- 创建代码的开发人员低于标准水平,
- 编程语言是低于标准的语言,或者
- 在环境方面,一些框架是低于标准的。
不要感到冒犯。 当我谈论“低于标准的开发人员”时,我并不是说你。 最后,您远高于一般开发人员,但并非最不重要,这是因为您开放并且对新事物感兴趣,这是您正在阅读本文所证明的。 但是,在编写代码时,您还应该考虑普通的开发人员Joe或Jane,他们将在将来的某个时候维护您的程序。 而且,普通开发人员有一个非常特殊的功能:他们不好。 它们也不错,但是,顾名思义,它们是平均水平。
标配开发商的传奇
您可能会发生几年前发生在我身上的事情。 它像下面这样。
解决了一个问题,我创建了一个微型框架。 并不是像Spring或Hibernate这样的框架,因为单个开发人员无法开发类似的框架。 (尽管其中有些人甚至在专业环境中尝试也没有停止,这是矛盾的,因为它不是专业的。)您需要一个团队。 我创建的是一个单一的类,该类正在做一些反射“魔术”,将对象转换为地图并返回。 在此之前,我们需要所有需要此功能的类中的toMap()和fromMap()方法。 它们是手动创建和维护的。
幸运的是我并不孤单。 我有一个团队。 他们告诉我取消编写的代码,并继续手动创建toMap()和fromMap() 。 原因是必须由跟随我们的开发人员来维护代码。 而且我们不知道它们,因为甚至没有选择它们。 他们可能仍在大学学习。 他们甚至可能没有出生。 我们知道一件事:他们将是普通的开发人员,而我创建的代码比普通的技能还需要一点点时间。 但是,即使维护容易出错,维护手工制作的toMap()和fromMap()方法也不需要掌握一般技能。 但这只是成本问题,需要在质量检查方面投入更多的资金,并且比雇用高级开发人员要少得多。
您可以想象我的矛盾情绪,因为我出色的(讽刺的)代码被赞美我的自我所拒绝。 我必须说,他们是对的。
低于标准框架
好吧,从这个意义上讲,许多框架都低于标准水平。 “ sub-par”一词也许并不是最好的。 例如,您从WSDL文件生成Java代码。 为什么框架会生成源代码而不是Java字节码? 有充分的理由。
生成字节码很复杂,需要特殊知识。 它具有与此相关的成本。 它需要一些字节码生成库,例如Byte Buddy。 对于使用代码的程序员而言,调试起来更加困难,并且它与JVM版本有关。 如果代码是作为Java源代码生成的,即使它是用于Java的更高版本的,并且项目使用的是滞后版本,则机会更好,项目可以以某种方式降级所生成的代码(如果这是Java)源如果它是字节码。
还请参见: 使用Quarkus的Java粒子加速
不及格语言
显然,在这种情况下,我们不是在谈论Java,因为Java是世界上最好的,没有更好的东西。 还是? 如果有人声称任何编程语言都是完美的,请忽略该人。 每种语言都有优点和缺点。 Java也不例外。 如果您考虑到该语言是20多年前设计的,并且根据其开发哲学,它向后兼容非常严格,则仅表示在某些领域应该使用其他语言更好。
考虑一下在Object类中定义的equals()和hashCode()方法,这些方法可以在任何类中重写。 没有什么发明能覆盖所有这些。 重写的实现是相当标准的。 实际上,它们是如此的标准,以至于集成开发环境均支持为其生成代码。 我们为什么要为其生成代码? 为什么它们不以某种声明的方式成为语言的一部分? 这些问题应该有很好的答案,因为在语言中实现这样的事情实际上并不是什么大问题,但事实并非如此。 必须有一个很好的理由,我不是最值得写的人。
还有另一个示例:lambda表达式。 仅在五年前,它才被引入该语言。 在此之前,程序员必须使用其他东西,例如匿名类。 让我们撇开一个事实,即匿名类的性能和字节代码实现与lambda表达式的性能和字节代码实现不同。 可以使用代码生成器将lambda表达式引入版本8之前的语言中,该代码生成器从某种简化形式生成匿名类。 由于语言的发展,某些功能(例如lambda,多行字符串,开关表达式)已成为语言的一部分或不属于该语言。 尚未被证明非常好的功能无法用Java之类的语言实现。 业界应该有其他语言的实验,开发人员必须接受其他语言的功能。 到那时,它可以用Java实现,然后将在其中保留数十年,甚至可能长达几个世纪。
作为本部分的摘要:如果您不能依赖手动生成的代码,则可以确保某些东西不合格。 这不是可耻的。 这就是我们一般的职业。 大自然就是这样。 没有理想的解决方案,我们必须妥协。
现在我们来看看一些理论,它解释了为什么会这样。
冗余码
我们必须生成代码的根本原因和真正原因是冗余。 低于标准的开发人员,语言或框架或其他任何东西都是由于冗余。 Wikipedia页面将冗余代码定义为:
在计算机编程中,冗余代码是计算机程序中不必要的源代码或已编译代码,例如…
这只是我要谈论的冗余的一部分。 实际上,这是冗余的最后一种类型,并且如果通过代码生成来回答这种冗余,则这是不生成代码时的最佳示例。 在本文中,我在信息论的意义上更多地提到冗余。 看一下Wikipedia页面:
在信息论中,冗余度度量集合X的熵H(X)与其最大可能值log(| A_X |)之间的分数差。
对于我们来说,这是一个非常精确但非常不可用的定义。 幸运的是,页面继续并显示:
非正式地,它是用于传输某些数据的浪费“空间”的数量。 数据压缩是减少或消除不必要的冗余的一种方法。
换句话说,以某种形式编码的某些信息如果可以压缩,则是多余的。 例如,下载和压缩古典英语小说《白鲸记》的文本会将其大小缩小到原始文本的40%。 用Apache Commons Lang的源代码做同样的事情,我们得到20%。 绝对不是因为这个“不必要的计算机程序代码”。 这是其他一些“必要的”冗余。 英语和其他语言是多余的,编程语言是多余的,事实就是这样。
冗余级别
然后,下一个问题是这些是否是冗余的唯一原因。 答案是,我们可以确定六个不同级别的冗余,包括已经提到的那些。
0.自然
这是英语或其他任何自然语言的冗余。 这种冗余是很自然的,我们已经习惯了。 冗余随着语言的发展而发展,它有助于帮助理解嘈杂的环境。 我们不想消除这种冗余,因为如果这样做,我们最终可能会读取一些二进制代码。 对于我们大多数人来说,这并不是很吸引人。 这就是人类和程序员的大脑工作方式。
1.语言
编程语言也是多余的。 它比它所基于的自然语言还要多余。 额外的冗余是因为关键字的数量非常有限。 这使得Java的压缩率从60%提高到80%。 其他语言,例如Perl,则密度较大,可惜它们的可读性较差。 但是,这也是我们不愿战斗的冗余。 减少来自编程语言冗余的冗余肯定会降低可读性,从而降低可维护性。
2.结构
冗余的另一个来源已经与语言无关。 这是代码结构冗余。 例如,当我们有一个带有一个参数的方法时,则调用此方法的代码片段也应使用一个参数。 如果方法更改了更多参数,则调用该方法的所有位置也必须更改。 这是来自程序结构的冗余,这不仅是我们不希望避免的事情,而且在不丢失信息和那样的代码结构的情况下也无法避免。
3.域诱导
当业务域可以以简洁明了的方式描述但编程语言不支持这种描述时,我们谈论的是域引起的冗余。 编译器就是一个很好的例子。 此示例是大多数程序员都熟悉的技术领域。 可以使用BNF格式以清晰美观的形式编写无上下文语法语法。 如果我们用Java创建解析器,肯定会更长。 由于BNF形式和Java代码的含义相同,并且Java代码明显更长,因此从信息论的角度来看,我们可以确定Java代码是多余的。 这就是为什么我们有针对此示例域的工具,例如ANTLR,Yacc和Lex以及其他一些工具的原因。
另一个示例是Fluent API。 可以对流利的API进行编程,以实现几个接口,这些接口可以指导程序员完成可能的链接方法调用序列。 编写流畅的API是一种漫长而又难以维护的方法。 同时,流利的API语法可以用正则表达式很好地描述,因为流利的API用有限状态语法描述。 正则表达式列出了描述替代方法,序列,可选调用和重复方法的方法,比该方法的Java实现更具可读性,更短且更少冗余。 这就是为什么我们拥有诸如Java :: Geci Fluent API生成器之类的工具的原因,这些工具将方法调用的正则表达式转换为fluent API实现。
在这个区域中,减少冗余可能是理想的,并且可能导致更易于维护和更具可读性的代码。
4.语言进化
语言演化冗余类似于域诱导的冗余,但它独立于实际的编程域。 这种冗余的来源是编程语言的弱点。 例如,Java不会自动为字段提供getter和setter。 如果您查看C#或Swift,它们就会显示。 如果我们需要用Java编写它们,则必须为其编写代码。 它是样板代码,是语言的弱点。 同样,在Java中,没有声明性的方法来定义equals()和hashCode()方法。 可能会有更高版本的Java可以解决该问题。 查看Java的过去版本,创建匿名类肯定比编写lambda表达式要多余。 Java不断发展,并将其引入了该语言。
域引起的冗余与语言演化引起的冗余之间的主要区别在于,虽然不可能用通用编程语言解决所有编程域,但是语言演化肯定会消除语言短缺带来的冗余。 随着语言的发展,我们在IDE和Lombok等程序中都有代码生成器来解决这些问题。
5.程序员诱导
这种冗余与代码冗余的经典含义相关。 这是程序员无法生成足够好的代码,并且程序中存在不必要和过多的代码结构,甚至复制粘贴代码的时候。 典型的例子是前面提到的“不合格开发者的传说”。 在这种情况下,代码生成可能是一个折衷方案,但通常是一个错误的选择。 从项目经理的角度来看,这可能是好的。 他们关心开发商的成本,因此他们可能决定只雇用便宜的开发商。 另一方面,在程序员级别,这是不可接受的。 如果您选择生成代码或编写更好的代码,则必须选择后者。 您必须学习和发展自己,以便可以开发更好的代码。
什么时候生成代码?
在讨论了代码冗余的不同级别或原因之后,下一个问题是如何生成代码。 就语言进化而言,答案很简单。 我们使用了一些工具。 让我们使用它们,它们将随时生成代码。 为了消除或减少域引起的冗余,我们创建了Java :: Geci框架,该框架使程序员可以编写自己的特定于编程域的代码生成器。 提供一个简单易用的API的愿望决定了插入代码生成阶段的结构和决定,其中创建代码生成器非常简单。 因此,在这里我们将探讨可能发生代码生成的不同开发生命周期阶段,然后我们描述为什么Java :: Geci使用它所使用的代码。
代码生成主要可能发生:
- (BC)编译前
- (DC)编译期间
- (DT)在测试阶段
- (DCL)在类加载期间
- (DRT)在运行时
在下文中,我们将讨论这些不同的情况。
还请参见:对Java进行现代化以在云原生世界中保持同步
(BC)编译前
常规阶段是在编译之前。 在这种情况下,代码生成器将读取某些配置或可能的源代码,并通常将Java代码生成到与手册源代码分开的特定目录中。
在这种情况下,生成的源代码不是进入版本控制系统的代码的一部分。 代码维护必须处理代码生成,并且几乎不能选择从流程中省略代码生成器并继续手动维护代码。
代码生成器无法轻松访问Java代码结构。 如果生成的代码必须以任何方式使用,扩展或补充已经存在的手动代码,则它必须分析Java源代码。 可以逐行或使用某些解析器来完成。 无论哪种方式,这都是一项任务,稍后将由Java编译器再次完成,并且还有很小的机会Java编译器和用于解析代码生成器代码的工具可能不是100%兼容的。
(DC)编译期间
Java使创建由编译器调用的所谓注释处理器成为可能。 它们可以在编译阶段生成代码,并且编译器将编译生成的类。 这样,代码生成便成为了编译阶段的一部分。
在此阶段运行的代码生成器无法访问已编译的代码,但是它们可以通过Java编译器为注释处理器提供的API访问已编译的结构。
可以生成新的类,但不能修改现有的源代码。
(DT)在测试阶段
首先,它似乎有点偏离。 为什么有人要在测试阶段执行代码生成? 但是,我在这里尝试“出售”的FOSS确实做到了这一点,我将详细介绍此阶段代码生成的可能性,优点和缺点。
(DCL)在上课期间
在类加载期间也可以修改代码。 执行此操作的程序称为Java代理。 它们不是真正的代码生成器。 它们在字节码级别上工作,并修改已编译的代码。
(DRT)在运行时
一些代码生成器在运行时工作。 其中许多应用程序直接生成Java字节码,并将代码加载到正在运行的应用程序中。 也可以生成Java源代码,编译代码并将结果字节加载到JVM中。
在测试阶段生成代码
这是Java :: Geci(Java GEnerate代码内联)生成代码的时间和地点。 为了帮助您理解在单元测试期间执行代码生成的怪异想法(当为时已晚:代码已经编译)让我告诉您另一个故事。 这个故事是虚构的,它从未发生过,但并没有使解释的能力相形见.。
我们有一个包含几个数据类的代码,每个数据类都有几个字段。 我们必须为每个这些类创建equals()和hashCode()方法。 最终,这意味着代码冗余。 当类更改时,添加或删除了一个字段,则方法也必须更改。 删除字段不是问题:编译器不会编译引用不存在的字段的equal()或hashCode()方法。 另一方面,编译器不介意这种方法不引用新的现有字段。
我们时不时地忘记更新这些方法,我们试图发明越来越多的复杂且更好的方法来抵消容易出错的人类编码。 最奇怪的想法是创建字段名称的MD5值,并将其作为注释插入到equals()和hashCode()方法中。 如果字段中有更改,则测试可以检查源代码中的值是否与根据字段名称计算出的值不同,然后发出错误消息: 单元测试失败 。 我们从未实施过。
甚至更怪异的想法,也并不是那么怪异,最终导致了Java :: Geci实际上是在测试过程中根据通过反射获得的可用字段创建预期的equals()和hashCode()方法测试,并将其与已经在代码中了。 如果它们不匹配,则必须重新生成它们。 但是,此时的代码已经重新生成。 唯一的问题是它在JVM的内存中,而不在包含源代码的文件中。 为什么只发出错误信号并告诉程序员重新生成代码? 测试为什么不回写更改? 毕竟,我们应该告诉计算机应该怎么做,而不是相反!
这就是导致Java :: Geci的顿悟。
还请参见: 32位或64位JVM不再重要吗?
Java :: Geci体系结构
Java :: Geci在编译,部署和执行生命周期的中间生成代码。 在构建阶段运行单元测试时,将启动Java :: Geci。 实际上,您必须编写一个或多个单元测试来配置和启动代码生成。
这意味着手册和先前生成的代码已经被编译,并且可以通过反射用于代码生成器。
在测试阶段执行代码生成还有另一个优势。 以后运行的任何代码生成都应仅生成与手动代码功能正交的代码。 这是什么意思? 从产生的代码不应以任何方式修改或干扰单元测试可能发现的现有手动创建的代码的意义上说,它必须是正交的。 这样做的原因是,在以后的任何阶段发生的代码生成已经在单元测试执行之后进行,因此不可能测试生成的代码是否以任何不希望的方式影响代码的行为。
在测试过程中生成代码可以考虑手册以及生成的代码对整个代码进行测试。 本身不应该对生成的代码本身进行测试(这是对代码生成器项目进行测试的任务),但是程序员编写的手动代码的行为可能取决于生成的代码以及测试的执行可能取决于生成的代码。
为确保生成的代码对所有测试都可以,在生成任何新代码的情况下,应再次执行编译和测试。 为了确保这一点,从测试中调用代码生成,并且在生成新代码的情况下测试失败。
为了更正此错误,通常会从三行单元测试中调用Java :: Geci中的代码生成,该结构具有以下结构:
Assertions.assertFalse(...generate(...),"code has changed, recompile!");
对…generate(…)的调用是配置框架和生成器的方法调用链,当框架执行时,框架将决定所生成的代码是否与现有代码不同。 如果代码更改,它将Java代码写回到源代码,但是如果生成的代码没有更改,则将代码保留完整。
如果更改了任何代码并将其写回到源代码,则对代码生成链中的最终调用的方法generate()将返回true。 这将使测试失败,但是如果我们使用已修改的源再次运行测试,则测试应该可以正常运行。
此结构对生成器有一些约束:
- 如果生成器在相同的源和类上执行,则生成的代码应完全相同。 通常这不是一个严格的要求,代码生成器通常不会生成随机源。 某些代码生成器可能希望在代码中插入时间戳作为注释。 默认情况下,将忽略代码格式和注释更改。 (可配置。)
- 生成的代码成为源代码的一部分,它们不是编译时工件。 所有将代码生成到现有类源中的代码生成器通常都是这种情况。 Java :: Geci可以生成单独的文件,但是它主要是为内联代码生成而设计的(因此得名)。
- 生成的代码必须保存到存储库中,手动源以及生成的代码必须处于不需要进一步生成代码的状态。 这样可以确保开发中的CI服务器可以使用原始工作流程:提取–编译–测试–将工件提交到存储库。 代码生成已经在开发人员机器上完成,并且CI上的代码生成器仅确保它确实完成了(否则测试失败)。
请注意,在开发人员机器上生成代码这一事实并不违反构建应独立于机器的规则。 如果存在任何机器依赖性,则代码生成将导致CI服务器上的代码不同,因此构建将中断。 某些样本生成器的某些早期版本确实发生了这种情况。 这是生成器本身的错误。
代码生成API
代码生成器应用程序应该很简单。 框架必须执行与大多数代码生成器相同的所有任务,并应提供支持,否则框架的职责是什么?
Java :: Geci为代码生成器做了很多事情:
- 它处理文件集的配置以查找源文件
- 扫描源目录并找到源代码文件
- 读取文件,如果文件是Java源代码,则有助于查找与源代码相对应的类
- 支持反射调用以帮助确定性代码生成
- 统一配置处理
- Java源代码生成方式不同
- 仅在更改时修改源文件并写回更改
- 提供功能齐全的示例代码生成器。 其中之一是成熟的Fluent API生成器,仅此一个就可以是一个整个项目。
- 支持Jamal模板和代码生成。
摘要
阅读本文,您可以了解为什么在专业Java开发中生成代码的原因,方式和时间。 我还简要介绍了Java :: Geci,这是一个用于创建特定于域的生成器的框架。 您实际上可以在Java :: Geci的GitHub主页上开始使用它。
翻译自: https://jaxenter.com/code-generation-164697.html
unity模型聚焦功能代码