在Java世界中介绍Scala的案例

紫色触手是我:我想要蓝色的水。 是的,我从不开心。

New Job,Big Company,Java团队,距离Scala都不远。

我的目标是:将一些不太大的Java项目移至Scala,并开始惯于在Scala中创建新项目。

为什么? 尽管我喜欢Java,因为Java具有更多功能,但我只是喜欢Scala。 现有的项目都很好用Java语言,我对Scala以及我们可以从中获得的好处非常了解。

我认为Scala使我们在不使用“神秘”库的情况下更易于阅读,与他人共享和重构。 Scala导致更健壮的代码,更高的生产率,并且开发人员产生的错误更少。 这全都归功于强大的Scala类型系统(除其他外)和编码方式。

经过实验的开发人员倾向于学习和改变。 只有管​​理人员对此感到冷漠:“ 如果我们开始使用Scala,则需要首先使用公司层次结构对其进行验证。 找到Scala开发人员比Java开发人员困难。

我需要说服力。 我需要解释一下为什么我们要使用Scala来提高生产力,为什么对开发人员来说使用它好,为什么我们需要推广它:Java的缺点是什么,Scala的优点是什么。

几年前,我来自.NET,必须同时进入现代Java和Scala领域-在那之前,我从未没有过Java 1.5和ant 。 学习曲线相当陡峭,但如今谁又不寻找挑战呢? Scala是一个挑战,使我们对FP和类型系统的广阔世界敞开胸怀。 感谢Scala,我对编程有了很多了解


在本文中,我将谈谈我从自己的经历中学到的东西。 您可能有不同的观点,请随时分享。

我们将研究在项目中看到的Java缺陷和不正确的做法,并考虑Scala如何帮助我们避免这些缺陷。

腹泻

众所周知,Java非常冗长。 有些人喜欢它,其他人只是忍受它。 lambda极大地帮助我们减少了详细程度,但这还不够。

var将有所帮助,数据类将有所帮助,模式匹配将有所帮助,但并非每个项目都可以像Java 9+那样升级其JVM。 我们大多数人将停留在Java 8一段时间。

相反, 偶然的冗长性完全无法帮助您理解代码 。 Java传达了比应有的方式更多的信息。 这不仅是类型的问题,而且是API设计的问题。 您必须阅读越多的代码,您就越需要在代码中进行推理。

我宁愿在编写或审查代码时不要花太多时间,只是最低限度,只是业务流程,而不是编程语言本身。 Scala使代码更具说明性,而Java则势在必行。 您需要先“运行程序”以了解发生了什么。

甚至Maven(无所不在的Java构建工具)也相当冗长且“难以理解”。 在Scala中,我们有sbt又名“简单构建工具”(我们不要自欺欺人,这远非简单)。 不过,当您的朋友帮助一个项目并将“ 263行不可读的Maven xml行转换为34行sbt定义 ”时:我更喜欢阅读34行(不混淆)以进行理解。

https://twitter.com/guizmaii/status/1032929860430323715

不太开心的路

由于Java中已检查的异常约束,因此将许多异常重新抛出RuntimeException。 发生这种情况是因为:

  • 接口不打算抛出任何东西
  • 接口不打算抛出实现可以实现的异常(这很有意义,为什么接口知道这一点?)
  • “我们不在乎”来声明它们,而更喜欢让下游没有try-catch。 如果您从未这样做过,请举手?

让我们抛开所有捕获的Exception处理程序的难闻气味,避免编写多个catch

在查看多个功能如何协同工作时, try-catch可能会使逻辑模糊。 这是使功能短路的“侧输出”,您无需考虑这些。 始终应以一种而不是多种方式读取代码。

我喜欢类比的是面向铁路的编程,可以显式地处理类型中的错误(就像在Scala中一样),从而可以安全地编写函数。

大多数功能都有一条幸福的道路和一条不幸福的道路(错误)。 但是它在返回类型中是显式的,例如使用Either [Error,Int] ,允许组合和错误遍历。

https://www.slideshare.net/ScottWlaschin/railway-oriented-programming

注释会破坏图层

在我从事的项目中,模型与Jackson和ORM的注释相互交织。 Guice到处都是。 希望我没有在里面找到任何Spring(它很大程度上依赖于注释)。

注释可以具有良好的一面:

  • 无需大量输入即可在运行时带来“神奇”的行为(例如Guice或Spring)。
  • 在类或成员变量(例如Jackson)的顶部添加元数据。
  • 轻松创建HTTP静态Web服务(Spring,JAX-RS)。

它们都是很好的用例,但是却是一把两刃剑。

一些注释迫使我们打破层次边界并合并不应捆绑在一起的代码。 通常, 注释会强制紧密耦合 。 这完全是违反规则的

我可能太严格了(有人说软件精通吗?),但是项目应该分为几层:模型,持久性,服务,API…应该可以重用。

阅读有关Clean Architecture的更多信息https://android.jlelse.eu/thoughts-on-clean-architecture-b8449d9d02df

称它为您想要的:

  • 清洁建筑
  • 洋葱架构
  • 六角形的建筑。

关键是应该在层次上进一步推迟框架的选择。 框架不能成为核心业务 (领域)的一部分,我们应该在代码中找到适当的关注点分离。

如果我想使用某个存储库接口,那么我不想导入Spring,因为您在上面添加了Spring注释。 由于某些原因,我想提供自己的实现。

分层使您很容易理解我们正在阅读的代码的范围和影响以及它所关注的模块。

这是序列化/反序列化的另一个示例:如果我正在阅读有关Car模型的一些代码以了解我们如何考虑它,那么我真的不在乎它如何序列化。 这与核心域完全正交,那么为什么要对代码添加耦合?

此外, 汽车可以序列化为不同的形式(JSON,Avro,Protobuf…)。 您是否要在其之上添加来自不同框架的越来越多的注释? 它们形成不同的模型,不应捆绑在一起,甚至不应位于不同的程序包独立的模块中 。 如果我想重用您的模型但没有注释附带的依赖关系怎么办? 我被卡住了。

注释使您很难知道是依靠它还是使用它 。 如何知道它不是无效代码? 您不能只使用IDE来“查找引用”。 这是另一个世界,另一种语言,例如颠倒世界。 这会阻止代码库探索并导致不确定性

在Java中,不考虑层和依赖关系而在各处使用批注太容易了。

在Scala中,我们的框架通常不使用注释,而是使用适当的代码(通过代码生成,宏和隐式)。 因此,很自然地将此代码打包在另一个模块中,而不会影响核心代码。 并非所有注释都不好,我们稍后会看到。


运行时检查而不是编译时检查

依靠运行时就像在玩火

许多Java功能和框架都依赖于运行时反射,自省和类路径扫描。

正如我所说,在我想转换为Scala的项目中,代码包含了Guice注释。 这很“不错”,但是很难理解依赖关系图。 因为它是在运行时构建的并且使用反射,所以我们甚至都不会尝试,并且让运行时在启动时崩溃还是不崩溃……惊喜! 真是浪费我们的时间。 谁从未遇到过这个问题? 同样,这导致不确定性。

使用Spring,您可以使用许多注释,其中大多数注释会在运行时产生影响,并使用魔术字符串作为参数 。 对@HystrixCommand(fallbackMethod =“ newList”)的特殊致敬,如果电路断开则将触发。 您会注意到您已经重命名了fallback函数,但是在生产环境中,当它崩溃时,它没有重命名此魔术字符串(如果我错了,请更正我)。

同样,这种代码在Java中是惯用的:

  如果(obj instanceof Integer){ 
int intValue =(((整数)obj).intValue();
// ...
} else if(obj instanceof String){
...

因为尚无智能模式匹配(但是,它会很棒),所以有时我们会看到此代码。 但是编译器不能在编译时保证其完整性 。 如果开发人员为obj传递了另一种类型而忘记在此处添加条件怎么办? 在Scala中,模式匹配功能强大,无处不在,并在编译时检查。

Scala程序员几乎不使用注释。 它们主要用于生成类似@BeanProperty(支持Java兼容性(!))或scalameta的样板。 它们也可以像@tailrec一样在编译时检查函数的行为,以确保函数是尾递归的。 Scala面向编译时 ,这就是程序更健壮的原因。

请注意, 所有Java批注仅在编译时处理,并且目标是生成样板,例如google / auto,mapstruct,Immutables或lombok,这也不错。 这是使用注释的好方法。


语义湖

通常,我发现代码在传统Java中不够语义化。 Collections API没有提供很多功能。 我仍然看到自制的for循环: 什么都没有传达我们为什么循环的想法,我们想要实现什么?

Java Stream API(更语义,更流畅)没有得到足够的使用,因为它没有提供足够的功能并且具有一些特殊性。

在Scala中,Collections API更完整,包含许多常用功能( foldreduceexistdifffilterheadtailslidesumzip …),没有奇怪的Collector东西。 在Java中,由于缺少功能,您通常不得不依赖StackOverflow上的代码片段,或者使用独特的第三方库作为Collections API(例如Guava,jOOλ,vavr)。

使用正确命名的方法,您无需阅读给定功能的代码即可知道结果:与传统的for循环相比,它传达了更多的含义,在经典的for循环中,您需要深入研究代码以了解其(副作用)。


缺乏抽象

ClassNotFoundException

在Java中null仍然是一回事 。 我们总是假设在Java中事物可以为null。 我讨厌太多的空测试。 有一些注释可以避免空测试,例如@Nullable或@NotNull:注释应该是类型本身的一部分,而不是外部世界的一部分。

谁从未使用过番石榴的前提条件? (示例取自Apache Druid):

  Preconditions.checkNotNull(task,“ task”);  Preconditions.checkNotNull(status,“ status”);  Preconditions.checkArgument( 
task.getId()。equals(status.getId()),
“任务/状态ID不匹配[%s /%s]”,
task.getId(),status.getId());

该代码充满了引发RuntimeExceptions的 前提条件 。 类中的每个函数都需要一次又一次地检查它们使用的变量。 您永远无法确切知道实例的状态,因此可以添加防御代码 。 这是最糟糕的代码,因为现在没人敢删除它。 这说明模型错误或类型系统不够强大。

这些空测试或注释在Scala中不存在,因此不会污染代码 。 我们使用Scala类型系统和抽象来处理它们,就像我们映射的 Option一样。 在Java中,由于Optional的特殊性和较差的API,它并不是普遍存在的。


F字

所有这些使我们进入了函数式编程。

很少将Java与FP结合使用(vavr正在尝试)。 它通常是易变的,并且使用不纯净的代码很难进行推理(即:它不是参照透明的,请参阅我的上一篇文章以了解其重要性:为什么参照透明很重要)。

另外,Java中没有很多Fluent接口。 StreamCompletableFuture是流利的,但不是通用的。 在Scala中,它无处不在,因为语言和库经常包含FP。

如果我们考虑一个程序,它的目的就是像这样处理数据: 输入→转换→输出。 FP正是与此有关:

输出=程序(输入).map(f1).map(f2)

这是一种功能样式:您可以传递和编写功能,易于阅读和理解。 要理解整体,请分而治之:您只需要分别了解每个功能即可理解整体。

这些函数(此处为f1 f2 )不依赖于外部上下文( this )。 他们使用我们给他们的变量并返回结果。

函数应遵守3条规则:

  • 总计 :没有部分函数可以返回与函数返回类型不同的结果(如异常)。
  • 确定性 :给定固定输入,输出应相同(无随机化,不依赖外部外部参数)。
  • 副作用 :在功能范围之外没有任何突变(例如println会改变stdout)。 那些突变必须在函数的返回类型中声明,以便调用者知道此行为(通常为IO )。

我不会在这里解释这个奇妙的世界,您可以在我的博客上了解更多。 https://www.sderosiaux.com/articles/2018/08/15/types-never-commit-too-early-part1/

将此与OOP进行对比:您有一个很长的类层次结构,其中每个父级都包含一些状态。 整个聚合形成实例的状态。 令人难以置信的疯狂:您有一个包含大量变量的类可以使用的方法,但是通常它们只需要一个或两个即可。 这是您开始添加特殊条件的地方,因为某些条件可以为null,或者整个实例参数集可能不连贯。

在FP中,无需将数据和功能组合到对象中。 面向对象方法与封装,受保护的私有数据及其成员有关,以保护自身免受混乱的可变状态的困扰。 一旦您不再具有可变状态,所有这些理由就会消失。

这就是FP功能强大的原因:功能范围仅缩小到所需范围。 FP中没有什么是有状态的。 状态从功能流向功能

它使测试FP程序变得容易 。 您不需要启动依赖注入框架或模拟ORM来测试某些东西。 您只需使用一些参数调用任何公共函数,测试输出,就可以完成。


不会灭绝

有人说Scala有变得无关紧要的危险。 他们使用GitHub和Tiobe统计数据支持他们的信念,该统计数据指出Scala处于回归状态。 而且,他们暗示对Scala感兴趣的人越来越少(没有来源),并且已经很难找到Scala开发人员:因此这是一个危险的诱饵。

从我自己的角度来看,我仍然看到大量活跃于烦恼,撰写和阅读博客的人,Scala开源项目仍在以良好的速度增长。

Scala来自“大数据”世界。 主要的软件都是用Scala编写的,例如Spark和Kafka。 在数据是一流公民的世界中,它们无处不在。 大型公司正在使用Scala:LinkedIn,Twitter,Netflix,Criteo。 这不是随机的:他们知道这使它们更加强大和高效。

开发人员有权

越抽象,您得到的语法就越强大且“简短”

我们仍在发现更好的抽象(由于在cats,monix,scalaz,zio中的工作,以及影响较小的库)和更好的做事方式。 这意味着我们甚至还没有进入“稳定”阶段,而仍在迭代“如何做”。

Scala 3传入。 这将明显改善Scala功能集,同时简化语言。 我们将获得新的工作模式,新的工作方式。

当我发现一家法国公司现在在做Scala时,我总是很高兴。 多多益善。 令人遗憾的是,这是一个值得注意的事件,没有Java那样广泛,我只是希望时机到来。

我认为Scala提高了代码质量的标准,并以强大的原则(FP)吸引了开发人员,并帮助他们开阔了胸怀,以寻求更好的代码抽象和更好的组织(关注点分离,类别理论)。


JVM呢

在Scala中,我们只需要JRE 8就可以使用 Java缺少的出色功能 :类型推断,高阶类型,模式匹配。 Scala被编译为JVM字节码,我们不需要任何迁移到JRE 9+的路径。

就像TypeScript和JavaScript:TypeScript在JavaScript之上提供了许多不存在的功能和类型。 这不是问题,因为我们不直接使用JavaScript及其所有怪癖。 TypeScript编译器确保该程序首先是有效的TypeScript,然后将其编译为Javascript。 这归功于静态类型,极大地减少了仅通过使用JavaScript即可拥有的bug数量。

请注意,TypeScript可以翻译为JS以外的其他语言(例如WebAssembly)。 它只是一种语言。 Scala与JVM字节码之上的抽象相同。

Scala可以编译为JVM字节码以外的语言:

  • 编译为JavaScript的ScalaJS。 有ReactJS绑定,VueJS绑定等等。 这非常强大,因为ScalaJS依赖于Scala类型系统。 您可以使用其所有功能,并且许多框架都兼容(即:可以编译为JS)。
  • Scala Native,可编译为本机代码。 它仍处于实验阶段,但仍在缓慢增长。

另外,由于Scala在JVM上运行,因此与Java应用程序相比, 我们使用现有工具来监视和调试 Scala应用程序:jconsole,visualvm,Java Mission Control等。Java开发人员可以继续使用其工具来调试Scala程序,不要紧。 仅内部使用的数据结构将与Scala框架不同且适当。


“ Scala比Java慢”

当Java开发人员声明时,我总是持怀疑态度。

根据您的代码可能是正确的。 但是通常,您只是不需要最高的性能。 我们可以在内部运行具有复杂逻辑的Scala服务器,并且仍然可以轻松处理数千个QPS(内部经验)。

是的,使用Scala时通常会产生开销,因为我们更喜欢不变性:与传统的可变Java程序相比,生成更多的垃圾对象和进行GC清理的次数更多。 但是GC的执行速度非常快(这只涉及年轻的对象)。

您必须在Scala中一个更健壮的正确键入的程序(给GC施加更多压力)与一个较难维护的Java程序之间进行权衡,Java程序可能更快并且消耗更少的RAM。 你喜欢哪个?

我什至会争辩说,在Scala中,重构代码以提高Java中的性能变得更加容易(更快且没有错误),这全都归功于我们通常在Scala中进行编码的方式。 代码通常更抽象和可重用。 仅更改某些类型类的实现就可以提高性能,而不会损害整个程序(例如,在微基准测试中,我将ZIO提升了3倍)。

关于性能,在Scala中,我们还使用了jmh,这是事实上的Java标准,用于编写微基准测试。 它通常用于改进关键代码,并确保性能不会随着时间和开发人员而降低 。 与sbt-jmh耦合以提供sbt命令,这是一个很棒的工具。


我们是否应该比Scala更进一步?

除Scala之外,还有Haskell或Eta(JVM上的Haskell实现)。 大多数人认为Haskell是已确认的Scala开发人员的自然“进化”。 我不确定情况是否如此,但是显然可以很好地了解Scala。

Scala生态系统从Haskell(scalaz,猫)那里吸收了很多想法来处理类别理论: 一堆抽象的东西组成 。 搜寻答案,我们经常偶然发现相同的概念,但使用Haskell代码。 它比Scala更为简洁,并且具有更强大的类型系统(更好的推断,种类多态(我们将在Dotty中使用!))。

但这看起来像一个利基市场(比Scala小得多)。 我没有听说过我所在地区的任何Haskell公司,只有巴黎的几家。 但这只是我。

我认为去Scala是Java开发人员可以做的最好的步骤。 它们不会丢失,因为现有的Java生态系统可用,但还有更多:Scala生态系统也可用(并且是首选)。

从“更好的Java”到Scala FP惯用的工作方式,在Scala中进行编码是时间和经验的问题。 与其他经过试验的开发人员共享无疑有助于理解这种“方式”。

如果您不确定直接过渡到Scala,也许使用vavr是一个不错的第一步,不要打扰过多的开发人员。 但是,为什么可以走一步,而又可以走一步呢? 在这两种情况下,都有一条学习曲线,因此最好只学习一条曲线,直接专注于Scala。

使用Scala将使开发人员自然而然地受到API的束缚,并被引导至功能编程范例: 这将提高代码质量并扩大他们的思维定式 。 即使以后再使用Java或任何一种语言,它们也不会以相同的方式进行编码: 它们已经进化了

那么,您认为我要出售它吗?


谢谢阅读!

非常感谢@philderome所做的更正和改进!