在许多情况下,具有功能组成的声明式解决方案提供了优于传统命令式代码的优越代码度量。 阅读本文并了解如何使用具有功能组成的声明性代码成为更好的程序员。
在本文中,我们将仔细研究三个问题示例,并研究用于解决这些问题的两种不同技术(命令式和声明式)。
本文中的所有源代码都是开源的,可以在以下位置获得
https://github.com/minborg/imperative-vs-declarative 。 最后,我们还将看到如何将本文的知识应用于数据库应用程序领域。 我们将使用Speedment Stream作为ORM工具,因为它提供了与数据库中的表,视图和联接相对应的标准Java Streams,并支持声明性构造。
从字面上看,有无数个示例候选可用于代码度量评估。
问题范例
在本文中,我选择了开发人员在工作期间可能会遇到的三个常见问题:
SumArray
遍历数组并执行计算
分组
并行汇总值
休息
通过分页实现REST接口
解决技术
正如本文开头所暗示的,我们将使用以下两种编码技术来解决上述问题:
势在必行
一种命令式解决方案,其中我们使用带有for循环和显式可变状态的传统代码样式。
陈述式
声明性解决方案,其中我们组合了各种功能以形成可以解决问题的高阶复合功能,通常使用 java.util.stream.Stream
或其变体。
代码指标
然后,想法是使用适用于SonarQube(此处为SonarQube社区版,版本7.7)的不同解决方案的静态代码分析,以便我们可以为问题/解决方案组合得出有用的标准化代码度量。 然后将这些指标进行比较。
在本文中,我们将使用以下代码指标:
LOC
“ LOC”表示“代码行”,是代码中非空行的数量。
陈述
是代码中语句的总数。 每条代码行上可能有零到多条语句。
圈复杂度
指示代码的复杂性,是对通过程序源代码的线性独立路径数量的定量度量。 例如,单个“ if”子句在代码中提供了两条单独的路径。 阅读更多
了这里的*。
认知复杂性
SonarCube声称:“认知复杂性不同于使用数学模型评估软件可维护性的实践。 它从Cyclomatic Complexity设定的先例开始,但是使用人工判断来评估应如何计算结构并决定应向模型整体添加哪些内容。 结果,它得出的方法复杂性得分比以前的模型更能使程序员对可维护性进行相对公平的评估。” 在SonarCube自己的页面上了解更多信息 。
通常,最好构想一个解决方案,其中这些指标较小,而不是较大。
作为记录,应该注意的是,以下设计的任何解决方案只是解决任何给定问题的一种方法。 让我知道您是否有更好的解决方案,并随时通过https://github.com/minborg/imperative-vs-declarative提交拉取请求。
遍历数组
我们从一个简单的开始。 该问题示例的对象是计算int数组中元素的总和,并将结果返回为 long
。 以下接口定义了问题:
public interface SumArray {
long sum( int [] arr); }
当务之急
以下解决方案使用命令式技术实现SumArray
问题:
public class SumArrayImperative implements SumArray {
@Override
public long sum( int [] arr) {
long sum = 0 ;
for ( int i : arr) {
sum += i;
}
return sum;
} }
声明式解决方案
这是使用声明性技术实现SumArray
的解决方案:
public class SumArrayDeclarative implements SumArray {
@Override
public long sum( int [] arr) {
return IntStream.of(arr)
.mapToLong(i -> i)
.sum();
} }
注意, IntStream::sum
只返回一个int,因此我们必须应用中间操作mapToLong()
。
分析
SonarQube提供以下分析:
下表显示了SumArray
的代码指标(通常越低越好):
技术 | LOC | 陈述 | 圈复杂度 | 认知复杂性 |
---|---|---|---|---|
势在必行 | 12 | 5 | 2 | 1个 |
功能性 | 11 | 2 | 2 | 0 |
这是它在图形中的外观(通常越低越好):
并行汇总值
该问题示例的对象是将“ Person
对象分组到不同的存储桶中,其中每个存储桶构成一个人的出生年份和一个工作的国家/地区的唯一组合。对于每个组,应计算平均工资。 聚合应使用公共ForkJoin池并行计算。
(不变的) Person
类是这样的:
势在必行
我们还定义了另一个称为YearCountry
不变类,该类将用作分组键:
势在必行
定义了这两个类之后,我们现在可以通过以下接口定义此问题示例:
势在必行
当务之急
对GroupingBy
示例问题实施命令式解决方案并非难事。 这是解决问题的一种解决方案:
势在必行
声明式解决方案
这是一个使用声明性构造实现GroupingBy
的解决方案:
势在必行
在上面的代码中,我使用了一些静态导入 Collectors
类(例如Collectors::groupingBy
)。 这不会影响代码指标。
分析
SonarQube提供以下分析:
下表显示了GroupingBy
的代码指标(越低越好):
技术 | LOC | 陈述 | 圈复杂度 | 认知复杂性 |
---|---|---|---|---|
势在必行 | 52 | 27 | 11 | 4 |
功能性 | 17 | 1个 | 1个 | 0 |
相应的图形如下所示(通常越低越好):
实施REST接口
在这个示例性问题中,我们将为Person对象提供分页服务。 出现在页面上的人员必须满足某些(任意)条件,并且必须按照给定的顺序进行排序。 该页面应作为不可修改的“个人”对象列表返回。
这是捕获问题的接口:
势在必行
页面的大小在一个单独的实用程序类RestUtil
:
势在必行
当务之急
这是Rest接口的命令性实现:
势在必行
声明式解决方案
下列类以声明的方式实现Rest接口:
势在必行
分析
SonarQube提供以下分析:
下表显示了Rest的代码指标(通常越低越好):
技术 | LOC | 陈述 | 圈复杂度 | 认知复杂性 |
---|---|---|---|---|
势在必行 | 27 | 10 | 4 | 4 |
功能性 | 21 | 1个 | 1个 | 0 |
在这里,相同的数字显示在图表中(再次降低通常会更好):
Java 11的改进
上面的示例是用Java 8编写的。使用Java 11,我们可以使用LVTI(局部变量类型推断)来缩短声明性代码。 这会使我们的代码短一些,但不会影响代码指标。
势在必行
与Java 8相比,Java 11包含一些新的收集器。 例如, Collectors.toUnmodifiableList()
可以使我们的声明式Rest解决方案更短:
势在必行
同样,这不会影响代码指标。
摘要
对我们的三个示例性问题的代码度量取平均会得出以下结果(通常越低越好):
考虑到本文的输入要求,当我们从命令式构造到声明式构造时,所有代码度量都有显着改进。
在数据库应用程序中使用声明性构造
为了从数据库应用程序中获得声明式构造的好处,我们使用了Speedment Stream 。 Speedment Stream是基于Stream的Java ORM工具,可以将任何数据库表/视图/联接转换为Java流,从而使您可以在数据库应用程序中运用声明性技能。
您的数据库应用程序代码将变得更好。 实际上,针对数据库的具有Speedment和Spring Boot的分页REST解决方案可能表示如下:
势在必行
Speedment提供Manager<Person> persons
,并构成数据库表“ Person”的句柄,并且可以通过Spring @AutoWired
进行管理。
结论
选择声明式而不是命令式解决方案可以大大降低通用代码的复杂性,并可以提供许多好处,包括更快的编码,更好的代码质量,提高的可读性,更少的测试,减少的维护成本等等。
为了从数据库应用程序中的声明性构造中受益,Speedment Stream是一种可以直接从数据库提供标准Java Streams的工具。
如今,对于任何当代的Java开发人员来说,必须掌握声明性构造和功能组成。
资源资源
文章源代码: https : //github.com/minborg/imperative-vs-declarative
SonarQube: https ://www.sonarqube.org/ Speedment Stream: https ://speedment.com/stream/ Speedment初始化程序: https ://www.speedment.com/initializer/
翻译自: https://www.javacodegeeks.com/2019/08/declarative-coding-makes-better-programmer.html