阅读本文并了解如何使用具备功能组合的声明性代码成为更好的程序员。html
在许多状况下,具备功能组合的声明性解决方案提供优于传统命令式代码的代码度。阅读本文并了解如何使用具备功能组合的声明性代码成为更好的程序员。java
在本文中,咱们将仔细研究三个问题示例,并研究两种不一样的技术(命令式和声明性)来解决这些问题。git
本文中的全部源代码都是开源的,可从
实际上有无数个候选示例可用于代码度量评估。github
在本文中,我选择了开发人员在平常工做可能遇到的三个常见问题:数据库
迭代数组并执行计算数组
并行聚合值ide
使用分页实现REST接口函数
正如本文开头所描述的,咱们将使用这两种编码技术解决问题:工具
一个命令式的解决方案,咱们使用带有for循环和显式可变状态的传统代码样例。
声明式解决方案,其中咱们组合各类函数以造成解决问题的高阶复合函数,一般使用java.util.stream.Stream
或其变体。
然而,咱们的想法是使用SonarQube(此处为SonarQube Community Edition,Version 7.7)将静态代码分析应用于不一样的解决方案,以便咱们能够为问题/解决方案组合推导出有用且标准化的代码度量标准。而后将比较这些指标。
在本文中,咱们将使用如下代码度量标准:
“LOC”表示“代码行”,是代码中非空行的数量。
是代码中的语句总数。每一个代码行上可能有零到多个语句。
表示代码的复杂性,而且是经过源代码程序的线性独立路径数量的定量度量。例如,单个“if”子句在代码中显示两条单独的路径。在维基百科上阅读更多内容。
SonarCube声称:
“认知复杂性改变了使用数学模型来评估软件可维护性的实践。它从Cyclomatic Complexity设定的先例开始,可是使用人为判断来评估结构应该如何计算,并决定应该将什么添加到模型中做为一个总体结果,它产生了方法复杂性分数,这使得程序员对可维护性模型的评估比之前更公平。“
在SonarCube本身的页面上能够阅读更多内容。
一般状况下,须要设想一个解决方案,其中这些指标很小而不是很大。
对于记录,应该注意下面设计的任何解决方案只是解决任何给定问题的一种方法。若是您知道更好的解决方案,请随时经过
咱们从简单开始。此问题示例的对象是计算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 |
Statements | 循环复杂性 | 认知复杂性 |
---|---|---|---|---|
Imperative | 12 |
5 |
2 |
1 |
Functional | 11 |
2 |
2 |
0 |
这是它在图表中的值(一般更低):
这个问题示例的对象是将Person对象分组到不一样的桶中,其中每一个桶构成一我的的出生年份和一我的工做的国家的惟一组合。对于每一个组,应计算平均工资。聚合应使用公共ForkJoin池并行计算。
这是(不可变的)Person类:
public final class Person {
private final String firstName;
private final String lastName;
private final int birthYear;
private final String country;
private final double salary;
public Person(String firstName,
String lastName,
int birthYear,
String country,
double salary) {
this.firstName = requireNonNull(firstName);
this.lastName = requireNonNull(lastName);
this.birthYear = birthYear;
this.country = requireNonNull(country);
this.salary = salary;
}
public String firstName() { return firstName; }
public String lastName() { return lastName; }
public int birthYear() { return birthYear; }
public String country() { return country; }
public double salary() { return salary; }
// equals, hashCode and toString not shown for brevity
}复制代码
咱们还定义了另外一个名为YearCountry的不可变类,把它做为分组键:
public final class YearCountry {
private final int birthYear;
private final String country;
public YearCountry(Person person) {
this.birthYear = person.birthYear();
this.country = person.country();
}
public int birthYear() { return birthYear; }
public String country() { return country; }
// equals, hashCode and toString not shown for brevity
}复制代码
定义了这两个类以后,咱们如今能够经过接口定义此问题示例:
public interface GroupingBy {
Map<YearCountry, Double> average(Collection<Person> persons);
}复制代码
实现GroupingBy示例问题的命令式解决方案并不是易事。这是问题的一个解决方案:
public class GroupingByImperative implements GroupingBy {
@Override
public Map<YearCountry, Double> average(Collection<Person> persons) {
final List<Person> personList = new ArrayList<>(persons);
final int threads = ForkJoinPool.commonPool().getParallelism();
final int step = personList.size() / threads;
// Divide the work into smaller work items
final List<List<Person>> subLists = new ArrayList<>();
for (int i = 0; i < threads - 1; i++) {
subLists.add(personList.subList(i * step, (i + 1) * step));
}
subLists.add(personList.subList((threads - 1) * step, personList.size()));
final ConcurrentMap<YearCountry, AverageAccumulator> accumulators = new ConcurrentHashMap<>();
// Submit the work items to the common ForkJoinPool
final List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < threads; i++) {
final List<Person> subList = subLists.get(i);
futures.add(CompletableFuture.runAsync(() -> average(subList, accumulators)));
}
// Wait for completion
for (int i = 0; i < threads; i++) {
futures.get(i).join();
}
// Construct the result
final Map<YearCountry, Double> result = new HashMap<>();
accumulators.forEach((k, v) -> result.put(k, v.average()));
return result;
}
private void average(List<Person> subList, ConcurrentMap<YearCountry, AverageAccumulator> accumulators) {
for (Person person : subList) {
final YearCountry bc = new YearCountry(person);
accumulators.computeIfAbsent(bc, unused -> new AverageAccumulator())
.add(person.salary());
}
}
private final class AverageAccumulator {
int count;
double sum;
synchronized void add(double term) {
count++;
sum += term;
}
double average() {
return sum / count;
}
}
}复制代码
这是一个使用声明性构造实现GroupingBy的解决方案:
public class GroupingByDeclarative implements GroupingBy {
@Override
public Map<YearCountry, Double> average(Collection<Person> persons) {
return persons.parallelStream()
.collect(
groupingBy(YearCountry::new, averagingDouble(Person::salary))
);
}
}复制代码
在上面的代码中,我使用了一些来自Collectors类的静态导入(例如Collectors :: groupingBy)。这不会影响代码指标。
SonarQube提供如下分析:
GroupingBy
的代码度量标准以下表所示(一般更低):
技术 |
LOC |
Statements | 循环复杂性 | 认知复杂性 |
---|---|---|---|---|
Imperative | 52 |
27 |
11 |
4 |
Functional | 17 |
1 |
1 |
0 |
这是它在图表中的值(一般更低):
在该示例性问题中,咱们将为Person对象提供分页服务。出如今页面上的Persons必须知足某些(任意)条件,并按特定顺序排序。该页面将做为不可修改的Person对象列表返回。
这是一个解决问题的接口:
public interface Rest {
/**
* Returns an unmodifiable list from the given parameters.
*
* @param persons as the raw input list
* @param predicate to select which elements to include
* @param order in which to present persons
* @param page to show. 0 is the first page
* @return an unmodifiable list from the given parameters
*/
List<Person> page(List<Person> persons,
Predicate<Person> predicate,
Comparator<Person> order,
int page);
}复制代码
页面的大小在名为RestUtil的单独工具程序类中:
public final class RestUtil {
private RestUtil() {}
public static final int PAGE_SIZE = 50;
}复制代码
public final class RestImperative implements Rest {
@Override
public List<Person> page(List<Person> persons,
Predicate<Person> predicate,
Comparator<Person> order,
int page) {
final List<Person> list = new ArrayList<>();
for (Person person:persons) {
if (predicate.test(person)) {
list.add(person);
}
}
list.sort(order);
final int from = RestUtil.PAGE_SIZE * page;
if (list.size() <= from) {
return Collections.emptyList();
}
return unmodifiableList(list.subList(from, Math.min(list.size(), from + RestUtil.PAGE_SIZE)));
}
}复制代码
public final class RestDeclarative implements Rest {
@Override
public List<Person> page(List<Person> persons,
Predicate<Person> predicate,
Comparator<Person> order,
int page) {
return persons.stream()
.filter(predicate)
.sorted(order)
.skip(RestUtil.PAGE_SIZE * (long) page)
.limit(RestUtil.PAGE_SIZE)
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
}
}复制代码
SonarQube提供如下分析:
Rest
的代码度量标准以下表所示(一般更低):
技术 |
LOC |
Statements | 循环复杂性 | 认知复杂性 |
---|---|---|---|---|
Imperative | 27 |
10 |
4 |
4 |
Functional | 21 |
1 |
1 |
0 |
这是它在图表中的值(一般更低):
上面的例子是用Java 8编写的。使用Java 11,咱们可使用LVTI(局部变量类型推断)缩短声明性代码。这会使咱们的代码更短,但不会影响代码指标。
@Override
public List<Person> page(List<Person> persons,
Predicate<Person> predicate,
Comparator<Person> order,
int page) {
final var list = new ArrayList<Person>();
...复制代码
与Java 8相比,Java 11包含一些新的收集器。例如,Collectors.toUnmodifiableList
(),它将使咱们的声明性Rest解决方案更短:
public final class RestDeclarative implements Rest {
@Override
public List<Person> page(List<Person> persons,
Predicate<Person> predicate,
Comparator<Person> order,
int page) {
return persons.stream()
.filter(predicate)
.sorted(order)
.skip(RestUtil.PAGE_SIZE * (long) page)
.limit(RestUtil.PAGE_SIZE)
.collect(toUnmodifiableList());
}复制代码
一样,这不会影响代码指标。
三个示例性问题的平均代码度量产生如下结果(一般更低):
鉴于本文中的输入要求,当咱们从命令式构造到声明式构造时,全部代码度量标准都有显着改进。
为了在数据库应用程序中得到声明性构造的好处,咱们使用了Speedment Stream。 Speedment Stream是一个基于流的Java ORM工具,能够将任何数据库表/视图/链接转换为Java流,从而容许您在数据库应用程序中应用声明性技能。
您的数据库应用程序代码将变得更好。事实上,针对数据库的Speedment和Spring Boot的分页REST解决方案可能表达以下:
public Stream<Person> page(Predicate<Person> predicate,
Comparator<Person> order,
int page) {
return persons.stream()
.filter(predicate)
.sorted(order)
.skip(RestUtil.PAGE_SIZE * (long) page)
.limit(RestUtil.PAGE_SIZE);
}复制代码
Manager
由Speedment提供,并构成数据库表“Person”的句柄,能够经过Spring使用@AutoWired注解。
选择声明性命令式解决方案能够大大下降通常代码复杂性,而且能够提供许多好处,包括更快的编码,更好的代码质量,更高的可读性,更少的测试,更低的维护成本等等。
为了从数据库应用程序中的声明性构造中受益,Speedment Stream是一种能够直接从数据库提供标准Java Streams的工具。
8月福利准时来袭,关注公众号后台回复:003便可领取7月翻译集锦哦~往期福利回复:001,002便可领取!