閱讀本文并了解如何使用具有功能組合的聲明性代碼成為更好的程序員。
在許多情況下,具有功能組合的聲明性解決方案提供優(yōu)于傳統(tǒng)命令式代碼的代碼度。閱讀本文并了解如何使用具有功能組合的聲明性代碼成為更好的程序員。
在本文中,我們將仔細(xì)研究三個(gè)問題示例,并研究兩種不同的技術(shù)(命令式和聲明性)來解決這些問題。
本文中的所有源代碼都是開源的,可從https://github.com/minborg/imperative-vs-declarative
<https://github.com/minborg/imperative-vs-declarative>
獲取。最后,我們還將看到本文的學(xué)習(xí)如何應(yīng)用于數(shù)據(jù)庫應(yīng)用程序領(lǐng)域。我們將使用Speedment Stream
<https://speedment.com/stream/>作為ORM工具,因?yàn)樗峁┝伺c數(shù)據(jù)庫中的表,視圖和連接相對應(yīng)的標(biāo)準(zhǔn)Java
Streams,并支持聲明性構(gòu)造。
實(shí)際上有無數(shù)個(gè)候選示例可用于代碼度量評估。
1.問題示例
在本文中,我選擇了開發(fā)人員在日常工作可能遇到的三個(gè)常見問題:
1.1.SumArray
迭代數(shù)組并執(zhí)行計(jì)算
1.2.GroupingBy
并行聚合值
1.3.Rest
使用分頁實(shí)現(xiàn)REST接口
2.解決方案技術(shù)
正如本文開頭所描述的,我們將使用這兩種編碼技術(shù)解決問題:
2.1 命令式解決方案
一個(gè)命令式的解決方案,我們使用帶有for循環(huán)和顯式可變狀態(tài)的傳統(tǒng)代碼樣例。
2.2 聲明式解決方案
聲明式解決方案,其中我們組合各種函數(shù)以形成解決問題的高階復(fù)合函數(shù),通常使用java.util.stream.Stream或其變體。
3.代碼指標(biāo)
然而,我們的想法是使用SonarQube(此處為SonarQube Community Edition,Version
7.7)將靜態(tài)代碼分析應(yīng)用于不同的解決方案,以便我們可以為問題/解決方案組合推導(dǎo)出有用且標(biāo)準(zhǔn)化的代碼度量標(biāo)準(zhǔn)。然后將比較這些指標(biāo)。
在本文中,我們將使用以下代碼度量標(biāo)準(zhǔn):
3.1. LOC
“LOC”表示“代碼行”,是代碼中非空行的數(shù)量。
3.2. Statements
是代碼中的語句總數(shù)。每個(gè)代碼行上可能有零到多個(gè)語句。
3.3. 循環(huán)復(fù)雜性
表示代碼的復(fù)雜性,并且是通過源代碼程序的線性獨(dú)立路徑數(shù)量的定量度量。例如,單個(gè)“if”子句在代碼中顯示兩條單獨(dú)的路徑。在維基百科上閱讀更多內(nèi)容
<https://en.wikipedia.org/wiki/Cyclomatic_complexity>。
3.4。認(rèn)知復(fù)雜性
SonarCube聲稱:
“認(rèn)知復(fù)雜性改變了使用數(shù)學(xué)模型來評估軟件可維護(hù)性的實(shí)踐。它從Cyclomatic
Complexity設(shè)定的先例開始,但是使用人為判斷來評估結(jié)構(gòu)應(yīng)該如何計(jì)算,并決定應(yīng)該將什么添加到模型中作為一個(gè)整體結(jié)果,它產(chǎn)生了方法復(fù)雜性分?jǐn)?shù),這使得程序員對可維護(hù)性模型的評估比以前更公平?!?br>
在SonarCube自己的頁面上可以閱讀更多內(nèi)容
<https://www.sonarsource.com/resources/white-papers/cognitive-complexity.html>。
通常情況下,需要設(shè)想一個(gè)解決方案,其中這些指標(biāo)很小而不是很大。
對于記錄,應(yīng)該注意下面設(shè)計(jì)的任何解決方案只是解決任何給定問題的一種方法。如果您知道更好的解決方案,請隨時(shí)通過
https://github.com/minborg/imperative-vs-declarative拉取請求提交意見。
<https://github.com/minborg/imperative-vs-declarative拉取請求提交意見。>
4.迭代數(shù)組
我們從簡單開始。此問題示例的對象是計(jì)算int數(shù)組中元素的總和,并將結(jié)果返回為long。以下接口定義了問題:
public interface SumArray { long sum(int[] arr); }
4.1.命令式解決方案
以下解決方案使用命令式技術(shù)實(shí)現(xiàn)SumArray問題:
public class SumArrayImperative implements SumArray { @Override public long
sum(int[] arr) { long sum = 0; for (int i : arr) { sum += i; } return sum; } }
4.2聲明式解決方案
這是一個(gè)使用聲明性技術(shù)實(shí)現(xiàn)SumArray的解決方案:
public class SumArrayDeclarative implements SumArray { @Override public long
sum(int[] arr) { return IntStream.of(arr) .mapToLong(i -> i) .sum(); } }
請注意,IntStream :: sum只返回一個(gè)int,因此,我們必須加入中間操作mapToLong()。
4.3.分析
SonarQube提供以下分析:
SumArray的代碼度量標(biāo)準(zhǔn)如下表所示(通常更低):
技術(shù) LOC Statements 循環(huán)復(fù)雜性 認(rèn)知復(fù)雜性
Imperative 12 5 2 1
Functional 11 2 2 0
這是它在圖表中的值(通常更低):
5.并行聚合值
這個(gè)問題示例的對象是將Person對象分組到不同的桶中,其中每個(gè)桶構(gòu)成一個(gè)人的出生年份和一個(gè)人工作的國家的唯一組合。對于每個(gè)組,應(yīng)計(jì)算平均工資。聚合應(yīng)使用公共ForkJoin池并行計(jì)算。
這是(不可變的)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 }
我們還定義了另一個(gè)名為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 }
定義了這兩個(gè)類之后,我們現(xiàn)在可以通過接口定義此問題示例:
public interface GroupingBy { Map<YearCountry, Double>
average(Collection<Person> persons); }
5.1.命令式的解決方案
實(shí)現(xiàn)GroupingBy示例問題的命令式解決方案并非易事。這是問題的一個(gè)解決方案:
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; } } }
5.2. 聲明式解決方案
這是一個(gè)使用聲明性構(gòu)造實(shí)現(xiàn)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類的靜態(tài)導(dǎo)入(例如Collectors :: groupingBy)。這不會(huì)影響代碼指標(biāo)。
5.3.分析
SonarQube提供以下分析:
GroupingBy的代碼度量標(biāo)準(zhǔn)如下表所示(通常更低):
技術(shù) LOC Statements 循環(huán)復(fù)雜性 認(rèn)知復(fù)雜性
Imperative 52 27 11 4
Functional 17 1 1 0
這是它在圖表中的值(通常更低):
6.實(shí)現(xiàn)REST接口
在該示例性問題中,我們將為Person對象提供分頁服務(wù)。出現(xiàn)在頁面上的Persons必須滿足某些(任意)條件,并按特定順序排序。該頁面將作為不可修改的Person對象列表返回。
這是一個(gè)解決問題的接口:
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的單獨(dú)工具程序類中:
public final class RestUtil { private RestUtil() {} public static final int
PAGE_SIZE = 50; }
6.1.命令式實(shí)現(xiàn)方法
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))); } }
6.2.聲明式解決方法
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)); } }
6.3.分析
SonarQube提供以下分析:
Rest的代碼度量標(biāo)準(zhǔn)如下表所示(通常更低):
技術(shù) LOC Statements 循環(huán)復(fù)雜性 認(rèn)知復(fù)雜性
Imperative 27 10 4 4
Functional 21 1 1 0
這是它在圖表中的值(通常更低):
7.Java 11改進(jìn)
上面的例子是用Java 8編寫的。使用Java 11,我們可以使用LVTI(局部變量類型推斷)縮短聲明性代碼。這會(huì)使我們的代碼更短,但不會(huì)影響代碼指標(biāo)。
@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()); }
同樣,這不會(huì)影響代碼指標(biāo)。
8.摘要
三個(gè)示例性問題的平均代碼度量產(chǎn)生以下結(jié)果(通常更低):
鑒于本文中的輸入要求,當(dāng)我們從命令式構(gòu)造到聲明式構(gòu)造時(shí),所有代碼度量標(biāo)準(zhǔn)都有顯著改進(jìn)。
8.1.在數(shù)據(jù)庫應(yīng)用程序中使用聲明性構(gòu)造
為了在數(shù)據(jù)庫應(yīng)用程序中獲得聲明性構(gòu)造的好處,我們使用了Speedment Stream <https://speedment.com/stream/>。
Speedment Stream是一個(gè)基于流的Java ORM工具,可以將任何數(shù)據(jù)庫表/視圖/連接轉(zhuǎn)換為Java流,從而允許您在數(shù)據(jù)庫應(yīng)用程序中應(yīng)用聲明性技能。
您的數(shù)據(jù)庫應(yīng)用程序代碼將變得更好。事實(shí)上,針對數(shù)據(jù)庫的Speedment和Spring Boot的分頁REST解決方案可能表達(dá)如下:
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提供,并構(gòu)成數(shù)據(jù)庫表“Person”的句柄,可以通過Spring使用@AutoWired注解。
9.總結(jié)
選擇聲明性命令式解決方案可以大大降低一般代碼復(fù)雜性,并且可以提供許多好處,包括更快的編碼,更好的代碼質(zhì)量,更高的可讀性,更少的測試,更低的維護(hù)成本等等。
為了從數(shù)據(jù)庫應(yīng)用程序中的聲明性構(gòu)造中受益,Speedment Stream是一種可以直接從數(shù)據(jù)庫提供標(biāo)準(zhǔn)Java Streams的工具。
掌握聲明性構(gòu)造和功能組合是當(dāng)今任何當(dāng)代Java開發(fā)人員必須的。
8月福利準(zhǔn)時(shí)來襲,關(guān)注公眾號
?
后臺(tái)回復(fù):003即可領(lǐng)取7月翻譯集錦哦~
?
往期福利回復(fù):001,002即可領(lǐng)??!
熱門工具 換一換