为什么声明式编码会使您成为更好的程序员?

本文对比分析了声明式编程与命令式编程在解决实际问题中的优劣,通过具体案例展示了不同编程范式对代码复杂性的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

在许多情况下,具有功能组成的声明性解决方案提供了优于传统必需代码的卓越代码度量。阅读本文并了解如何使用声明性代码和函数合成来成为更好的程序员。

在本文中,我们将更深入地了解三个问题示例,并研究两种不同的技术(“强制”和“声明”)来解决这些问题中的每一个。

本文中的所有源代码都是开放源码的,可以在 https://github.com/minborg/imperative-vs-declarative中获得。最后,我们还将看到本文学习的内容如何应用于数据库应用领域。我们将使用SpeedmentStream作为ORM工具,因为它提供了与数据库中的表、视图和连接相对应的标准Java流,并支持声明式构造。实际上,有无限多的候选示例可以用于代码度量评估。

实际上,有无限多的候选示例可以用于代码度量评估。

问题实例

在本文中,我选择了开发人员在工作期间可能面临的三个常见问题。

SumArray

对数组进行迭代并执行计算。

GroupingBy

并行聚合值

Rest

用分页实现REST接口

Solution Techniques

正如本文开头所暗示的,我们将使用以下两种编码技术来解决上述问题:

Imperative

我们使用具有FOR循环和显式可变状态的传统代码样式的命令式解决方案

Declarative

一种声明式解决方案,其中我们组合各种函数以形成解决问题的高阶复合函数,通常使用java.util.stream.Stream或其变体。

Code Metrics

然后,使用SonarQube(这里是SonarQube CommunityEdition,Version7.7)对不同的解决方案使用静态代码分析,以便我们可以为问题/解决方案组合导出有用的标准化代码度量标准。然后对这些指标进行比较。在本文中,我们将使用以下代码度量:

LOC

“LOC”的意思是“代码行”,是代码中的非空行数。

Statements

代码中语句的总数。每个代码行上可能有零到多个语句。

Cyclomatic Complexity

注释代码的复杂性,是对程序源代码中线性独立路径数量的定量度量。例如,一个“if”子句在代码中显示两个单独的路径。在维基百科上阅读更多内容。

Cognitive Complexity

SonarCube声称:“认知复杂性打破了使用数学模型来评估软件可维护性的做法。它从圈内复杂性设定的先例开始,但通过人工判断来评估应该如何计算结构,并决定整个模型应该添加哪些内容。因此,它得出的方法复杂性分数让程序员认为,对可维护性的相对评估比以往的模型更公平。”
在SonarCube自己的页面上阅读更多内容。通常情况下,我们需要设想一种解决方案,在这种解决方案中,这些度量是小的,而不是大的。为了记录在案,应该指出,下面提出的任何解决办法都只是解决任何特定问题的一种方法。如果您知道更好的解决方案,请告诉我,您可以通过https://github.com/minborg/imperative-vs-declarative.

Iterating Over an Array

我们从一个简单的开始。这个问题示例的对象是计算int数组中元素的和,并将结果作为一个长返回。以下接口定义了问题:

public interface SumArray {
    long sum(int[] arr);
}

Imperative Solution

以下解决方案使用命令式技术实现了SumArray 问题:

public class SumArrayImperative implements SumArray {
    @Override
    public long sum(int[] arr) {
        long sum = 0;
        for (int i : arr) {
            sum += i;
        }
        return sum;
    }
}

Declarative Solution

下面是使用声明性技术实现SumArray的解决方案:

public class SumArrayDeclarative implements SumArray {
    @Override
    public long sum(int[] arr) {
        return IntStream.of(arr)
            .mapToLong(i -> i)
            .sum();
    }
}

注意,IntStream:sum只返回一个int,因此,我们必须应用中间操作mapToLong()。

Analysis

SonarQube提供了以下分析:
SumArray的代码指标显示在下表中(较低的值通常更好):

Technique(技术)LOC(代码行)Statements(语句的总数)Cyclomatic Complexity (复杂性)Cognitive Complexity(认知复杂性)
Imperative(必要的)12521
Functional(功能的)11220

Aggregating Values in Parallel

这个问题示例的对象是将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);
}

Imperative Solution

对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;
        }
    }
}

Declarative Solution

下面是使用声明式构造实现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))
            );
    }
}

在上面的代码中,我使用了来自收集器类的一些静态导入(e.g. Collectors::groupingBy)。这不影响代码度量。

Analysis

SonarQube提供了以下分析:
GroupingBy的代码指标显示在下表中(较低的值通常更好):

Technique(技术)LOC(代码行)Statements(语句的总数)Cyclomatic Complexity (复杂性)Cognitive Complexity(认知复杂性)
Imperative(必要的)5227114
Functional(功能的)17110

Implementing a REST Interface

在此示例性问题中,我们将为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;
}

Imperative Solution

这里是REST接口的一个命令式实现:

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)));
    }
}

Declarative Solution

下面的类以声明的方式实现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(collectingAndThen(toList(), Collections::unmodifiableList));
    }
}

Analysis

SonarQube提供了以下分析:
REST的代码指标显示在下表中(较低的值通常更好):

Technique(技术)LOC(代码行)Statements(语句的总数)Cyclomatic Complexity (复杂性)Cognitive Complexity(认知复杂性)
Imperative(必要的)271044
Functional(功能的)21110

Java 11 Improvements

上面的例子是用Java 8编写的。使用Java 11,我们可以使用LVTI(LocalVariableTypeInference)来缩短声明性代码。这将使我们的代码更短一些,但不会影响代码度量。

@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());
}

同样,这不会影响代码度量。

在数据库应用程序中使用声明式构造

为了在数据库应用程序中获得声明式构造的好处,我们使用了SpeedmentStream。SpeedmentStream是一种基于流的JavaORM工具,它可以将任何数据库表/视图/连接转换为Java流,从而允许您在数据库应用程序中应用声明性技能。

您的数据库应用程序代码会变得更好。实际上,对数据库使用Speedment和SpringBoot的分页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<Person> persons由Speedment提供,构成数据库表“Person”的句柄,可以通过Spring@AutoWired

Conclusions

与命令式解决方案相比,选择声明式解决方案可以大大降低一般代码的复杂性,并能提供许多好处,包括更快的编码、更好的代码质量、更好的可读性、更少的测试、更低的为了从数据库应用程序中的声明性构造中获益,SpeedmentStream是一个可以直接从数据库提供标准Java流的工具。掌握声明式构造和功能组合对于当今的任何Java开发人员来说都是必须的。

Resources

文章源代码:https://github.com/minborg/imperative-vs-declarative
Speedment Stream:https://speedment.com/stream/
Speedment Initializer:https://www.speedment.com/initializer/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值