閱讀本文並瞭解如何使用具備功能組合的聲明性代碼成爲更好的程序員。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便可領取!