如何成為更好的程式設計師?
- 2019 年 10 月 3 日
- 筆記
閱讀本文並了解如何使用具有功能組合的聲明性程式碼成為更好的程式設計師。
在許多情況下,具有功能組合的聲明性解決方案提供優於傳統命令式程式碼的程式碼度。閱讀本文並了解如何使用具有功能組合的聲明性程式碼成為更好的程式設計師。
在本文中,我們將仔細研究三個問題示例,並研究兩種不同的技術(命令式和聲明性)來解決這些問題。
本文中的所有源程式碼都是開源的,可從https://github.com/minborg/imperative-vs-declarative獲取。最後,我們還將看到本文的學習如何應用於資料庫應用程式領域。我們將使用Speedment Stream作為ORM工具,因為它提供了與資料庫中的表,視圖和連接相對應的標準Java Streams,並支援聲明性構造。
實際上有無數個候選示例可用於程式碼度量評估。
1.問題示例
在本文中,我選擇了開發人員在日常工作可能遇到的三個常見問題:
1.1.SumArray
迭代數組並執行計算
1.2.GroupingBy
並行聚合值
1.3.Rest
使用分頁實現REST介面
2.解決方案技術
正如本文開頭所描述的,我們將使用這兩種編碼技術解決問題:
2.1 命令式解決方案
一個命令式的解決方案,我們使用帶有for循環和顯式可變狀態的傳統程式碼樣例。
2.2 聲明式解決方案
聲明式解決方案,其中我們組合各種函數以形成解決問題的高階複合函數,通常使用java.util.stream.Stream
或其變體。
3.程式碼指標
然而,我們的想法是使用SonarQube(此處為SonarQube Community Edition,Version 7.7)將靜態程式碼分析應用於不同的解決方案,以便我們可以為問題/解決方案組合推導出有用且標準化的程式碼度量標準。然後將比較這些指標。
在本文中,我們將使用以下程式碼度量標準:
3.1. LOC
「LOC」表示「程式碼行」,是程式碼中非空行的數量。
3.2. Statements
是程式碼中的語句總數。每個程式碼行上可能有零到多個語句。
3.3. 循環複雜性
表示程式碼的複雜性,並且是通過源程式碼程式的線性獨立路徑數量的定量度量。例如,單個「if」子句在程式碼中顯示兩條單獨的路徑。在維基百科上閱讀更多內容。
3.4。認知複雜性
SonarCube聲稱:
「認知複雜性改變了使用數學模型來評估軟體可維護性的實踐。它從Cyclomatic Complexity設定的先例開始,但是使用人為判斷來評估結構應該如何計算,並決定應該將什麼添加到模型中作為一個整體結果,它產生了方法複雜性分數,這使得程式設計師對可維護性模型的評估比以前更公平。「
在SonarCube自己的頁面上可以閱讀更多內容。
通常情況下,需要設想一個解決方案,其中這些指標很小而不是很大。
對於記錄,應該注意下面設計的任何解決方案只是解決任何給定問題的一種方法。如果您知道更好的解決方案,請隨時通過https://github.com/minborg/imperative-vs-declarative拉取請求提交意見。
4.迭代數組
我們從簡單開始。此問題示例的對象是計算int數組中元素的總和,並將結果返回為long。以下介面定義了問題:
public interface SumArray { long sum(int[] arr); }
4.1.命令式解決方案
以下解決方案使用命令式技術實現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聲明式解決方案
這是一個使用聲明性技術實現SumArray的解決方案:
public class SumArrayDeclarative implements SumArray { @Override public long sum(int[] arr) { return IntStream.of(arr) .mapToLong(i -> i) .sum(); } }
請注意,IntStream :: sum只返回一個int,因此,我們必須加入中間操作mapToLong()。
4.3.分析
SonarQube提供以下分析:
SumArray的程式碼度量標準如下表所示(通常更低):
技術 | LOC | Statements | 循環複雜性 | 認知複雜性 |
---|---|---|---|---|
Imperative | 12 | 5 | 2 | 1 |
Functional | 11 | 2 | 2 | 0 |
這是它在圖表中的值(通常更低):
5.並行聚合值
這個問題示例的對象是將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); }
5.1.命令式的解決方案
實現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; } } }
5.2. 聲明式解決方案
這是一個使用聲明性構造實現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)。這不會影響程式碼指標。
5.3.分析
SonarQube提供以下分析:
GroupingBy
的程式碼度量標準如下表所示(通常更低):
技術 | LOC | Statements | 循環複雜性 | 認知複雜性 |
---|---|---|---|---|
Imperative | 52 | 27 | 11 | 4 |
Functional | 17 | 1 | 1 | 0 |
這是它在圖表中的值(通常更低):
6.實現REST介面
在該示例性問題中,我們將為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; }
6.1.命令式實現方法
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
的程式碼度量標準如下表所示(通常更低):
技術 | LOC | Statements | 循環複雜性 | 認知複雜性 |
---|---|---|---|---|
Imperative | 27 | 10 | 4 | 4 |
Functional | 21 | 1 | 1 | 0 |
這是它在圖表中的值(通常更低):
7.Java 11改進
上面的例子是用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()); }
同樣,這不會影響程式碼指標。
8.摘要
三個示例性問題的平均程式碼度量產生以下結果(通常更低):
鑒於本文中的輸入要求,當我們從命令式構造到聲明式構造時,所有程式碼度量標準都有顯著改進。
8.1.在資料庫應用程式中使用聲明性構造
為了在資料庫應用程式中獲得聲明性構造的好處,我們使用了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<Person> persons
由Speedment提供,並構成資料庫表「Person」的句柄,可以通過Spring使用@AutoWired註解。
9.總結
選擇聲明性命令式解決方案可以大大降低一般程式碼複雜性,並且可以提供許多好處,包括更快的編碼,更好的程式碼品質,更高的可讀性,更少的測試,更低的維護成本等等。
為了從資料庫應用程式中的聲明性構造中受益,Speedment Stream是一種可以直接從資料庫提供標準Java Streams的工具。
掌握聲明性構造和功能組合是當今任何當代Java開發人員必須的。
8月福利準時來襲,關注公眾號
後台回復:003即可領取7月翻譯集錦哦~
往期福利回復:001,002即可領取!