《clean architecture》第二部分編程範式讀書筆記

2021-12-22 Go招聘

前言

第二部分的主題內容

Chap3.  編程範式總覽

Chap4. 結構化編程

Chap5. 面向對象編程

Chap6.函數式編程

總結

前言

上次土撥鼠水了一篇第一部分的讀書筆記,《clean architecture》第一部分筆記。今天再來水一下第二部分關於編程範式的筆記。

第二部分的主題內容

第二部分主要是關於編程範式的講述,分別從結構化編程、面向對象編程、函數式編程來說明介紹編程範式。

Chap3. PARADIGM OVERVIEW 編程範式總覽

STRUCTURED PROGRAMMING 結構化編程OBJECT-ORIENTED PROGRAMMING 面向對象編程FUNCTIONAL PROGRAMMING 函數式編程

Chap4. STRUCTURED PROGRAMMING 結構化編程

A HARMFUL PROCLAMATION goto 是有害的FUNCTIONAL DECOMPOSITION 功能性降解拆分NO FORMAL PROOFS 形式化證明沒有發生SCIENCE TO THE RESCUE 科學來救場

Chap5. OBJECT-ORIENTED PROGRAMMING 面向對象編程

THE POWER OF POLYMORPHISM 多態的強大性DEPENDENCY INVERSION 依賴反轉

Chap6. FUNCTIONAL PROGRAMMING 函數式編程

IMMUTABILITY AND ARCHITECTURE 不可變性與軟體架構SEGREGATION OF MUTABILITY 可變性的隔離Chap3.  編程範式總覽三個編程範式

三個編程範式,它們分別是結構化編程(structured programming)、 面向對象編程(object-oriented programming)以及函數式編程(functional programming)。

結構化編程是第一個普遍被採用的編程範式,由 Edsger Wybe Dijkstra 於 1968 年最先提出。結構化編程範式歸結為:結構化編程對程序控制權的直接轉移進行了限制和規範。

面向對象編程式的提出比結構化編程還早了兩年,是在 1966 年由 Ole Johan Dahl 和 Kriste Nygaard 在論文中總結歸納出來的。面向對象編程範式歸結為:面向對象編程對程序控制權的間接轉移進行了限制和規範。

函數式編程概念是基於與阿蘭·圖靈同時代的數學家 Alonzo Church 在 1936 年發明的入演算的直接衍生物。函數式程式語言中應該是沒有賦值語句的。大部分函數式程式語言只允許在非常嚴格的限制條件下,才可以更改某個變量的值。函數式編程範式歸結為:函數式編程對程序中的賦值進行了限制和規範。

思考和小結

每個編程範式的目的都是設置限制。這些範式主要是為了告訴我們不能做什麼,而不是可以做什麼。這三個編程範式分別限制了 goto 語句、函數指針和賦值語句的使用。

這三個編程範式可能是僅有的三個了,三個編程範式都是在 1958 年到 1968 年這 10 年間被提出來的,後續再也沒有新的編程範式出現過。

這些編程範式的歷史知識與軟體架構有關係嗎?當然有,而且關係相當密切。譬如說多態是我們跨越架構邊界的手段,函數式編程是我們規範和限制數據存放位置與訪問權限的手段,結構化編程則是各模塊的算法實現基礎。 這和軟體架構的三大關注重點不謀而合:功能性、組件獨立性以及數據管理。

Chap4. 結構化編程可推導性

Dijkstra 認為程式設計師可以像數學家一樣對自己的程序進行推理證明。換句話說,程式設計師可以用代碼將一些已證明可用的結構串聯起來。

Dijkstra 在研究過程中發現了一個問題。goto 語句的某些用法會導致某個模塊 無法被遞歸拆分成更小的、可證明的單元,這會導致無法採用分解法來將大型問題進一步拆分成更小的、可證明的部分。

不過兩年前Bohm 和 Jocopini 剛剛證明了人們可以用順序結構、分支結構、循環結構這三種結構構造出任何程序。這個發現非常重要:因為它證明了我們構建可推導模塊所需要的控制結構集與構建所有程序所需的控制結構集的最小集是等同的。這樣—來,結構化編程就誕生了。

goto 是有害的

隨著程式語言的演進,goto 語句的重要性越來越小,最終甚至消失了。如今大部分的現代程式語言中都已經沒有了 goto 語句。哦,對了,LISP 裡從來就沒有過!

功能分解

既然結構化編程範式可將模塊遞歸降解拆分為可推導的單元,這樣一來,我們就可以將一個大型問題拆分為一系列高級函數的組合,而這些高級函數各自又可以繼續被拆分為一系列低級函數。

測試

Dijkstra 曾經說過「測試只能展示 Bug 的存在,並不能證明不存在 Bug」, 換句話說,一段程序可以由一個測試來證明其錯誤性,但是卻不能被證明是正確的。

小結

結構化編程範式中最有價值的地方就是,它賦予了我們創造可證偽程序單元的能力。這就是為什麼現代程式語言一般不支持無限制的 goto 語句。更重要的是,這也是為什麼在架構設計領域,功能性分解仍然是最佳實踐之一。

Chap5. 面向對象編程

什麼是面向對象?一種常見的回答是「數據與函數的組合」 。另一種是面向對象編程是一種對真實世界進行建模 的方式。回答此問題的同時另外還會搬出這三個詞語:封裝(encapsulation)、繼承(inheritance)、多態(polymorphism)。其隱含意思就是說面向對象編程是這三項的有機組合,或者任何一種支持面向對象的程式語言必須支持這三個特性。

封裝

通過釆用封裝特性,我們可以把一組相關聯的數據和函數圈起來,使圈外面的代碼只能看見部分函數,數據則完全不可見。這裡舉了一個c語言版本封裝的例子。使用 point.h 的程序是沒有 Point 結構體成員的訪問權限的。它們只能調用 makePoint() 函數和 distance() 函數,但對它們來說,Point 這個數據結構體的內部細節,以及函數的具體實現方式都是不可見的。

point.h

struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);

point.c

#include "point.h"
#include <stdlib.h>
#include <math.h>
struct Point {
  double x,y;
};
struct Point* makepoint(double x, double y) {
  struct Point* p = malloc(sizeof(struct Point));
  p->x = x;
  p->y = y;
  return p;
}
double distance(struct Point* p1, struct Point* p2) {
  double dx = p1->x - p2->x;
  double dy = p1->y - p2->y;
  return sqrt(dx*dx+dy*dy);
}

C++通過在程式語言層面引入 public、private、protected 這些關鍵詞,部分維護了封裝性。但所有這些都是為了解決編譯器自身的技術實現問題而引入的 hack——編譯器由於技術實現原因必須在頭文件中看到成員變量的定義。

而 Java 和 C# 則徹底拋棄了頭文件與實現文件分離的編程方式,這其實進一步削弱了封裝性。因為在這些語言中,我們是無法區分一個類的聲明和定義的。

所以我們很難說強封裝是面向對象編程的必要條件。而事實上,有很多面向對象程式語言|對封裝性並沒有強制性的要求。

繼承

繼承的主要作用是讓我們可以在某個作用域內對外部定義的某一組變量與函數進行覆蓋

namedPoint.h

struct NamedPoint;
struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);

namedPoint.c

#include "namedPoint.h"
#include <stdlib.h>
struct NamedPoint {
  double x,y;
  char* name;
};
struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
  p->x = x;
  p->y = y;
  p->name = name;
  return p;
}
void setName(struct NamedPoint* np, char* name) {
  np->name = name;
}
char* getName(struct NamedPoint* np) {
  return np->name;
}

main.c

#include "point.h"
#include "namedPoint.h"
#include <stdio.h>
int main(int ac, char** av) {
  struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
  struct NamedPoint* upperRight = makeNamedPoint  (1.0, 1.0, "upperRight");
  printf("distance=%f\n",
    distance(
             (struct Point*) origin,
             (struct Point*) upperRight));
}

可以看出NamedPoint 之所以可以被偽裝成 Point 來使用,是因為 NamedPoint 是 Point 結構體的一個超集,同兩者共同成員的順序也是一樣的。這種編程方式雖然看上去有些投機取巧,但是在面向對象理論被提出之前,這已經很常見了。其實,C++內部就是這樣實現單繼承的。在 main.c 中,程式設計師必須強制將 NamedPoint 的參數類型轉換為 Point,而在真正的面向對象程式語言中,這種類型的向上轉換通常應該是隱性的。因此,我們可以說,早在面向對象程式語言被發明之前,對繼承性的支持就已經存在很久了。

綜上所述,我們可以認為,雖然面向對象編程在繼承性方面並沒有開創出新,但是的確在數據結構的偽裝性上提供了相當程度的便利性。分析得出 面向對象編程在封裝性上得 0 分,在繼承性上勉強可以得 0.5 分(滿分為 1)。

多態

在面向編程對象語言被發明之前,我們所使用的程式語言能支持多態嗎? 答案是肯定的。那是怎麼做的呢?函數指針。調用的方法會指向函數指針指向的函數。UNIX 作業系統強制要求每個 IO 設備都要提供 open、close、read、write 和 seek 這 5 個標準函數。所以IO設備需要支持這些接口函數。為什麼 UNIX 作業系統會將 IO 設備設計成插件形式呢?因為自 20 世紀 50 年代末期以來,我們學到了一個重要經驗:程序應該與設備無關。 這個經驗從何而來呢?因為一度所有程序都是設備相關的,但是後來我們發現自己其實真正需要的是在不同的設備上實現同樣的功能。歸根結底,多態其實不過就是函數指針的一種應用。自從 20 世紀 40 年代末期馮·諾依曼架構誕生那天起,程式設計師們就一直在使用函數指針模擬多態了。也就是說,面向對象編程在多態方面沒有提出任何新概念。

依賴反轉

想像一下在安全和便利的多態支持出現之前,軟體是什麼樣子的。下面有一個典型的調用樹的例子如圖5.1,main 函數調用了一些高層函數,這些高層函數又調用了一些中層函數,這些中層函數又繼續調用了一些底層函數。在這裡,系統行為決定了控制流,而控制流則決定了原始碼依賴關係。

一旦我們使用了多態,情況就不一樣了。如圖5.2。模塊 HL1 調用了 ML1 模塊中的 F() 函數,這裡的調用是通過原始碼級別的接口來實現的。

現在,我們可以再回頭來看圖 5.1 中的調用樹,就會發現其中的眾多原始碼依賴關係都可以通過引入接口的方式來進行反轉。通過這種方法,軟體架構師可以完全控制採用了面向對象這種編程方式的系統中所有的原始碼依賴關係,而不再受到系統控制流的限制。不管哪個模塊調用或者被調用,軟體架構師都可以隨意更改原始碼依賴關係。

圖5.3 依賴反轉可以使資料庫模塊和用戶界面模塊都依賴於業務邏輯模塊。我們讓用戶界面和資料庫都成為業務邏輯的插件。也就是說,業務邏輯模塊的原始碼不需要引入用戶界面和資料庫這兩個模塊。

這樣一來,業務邏輯、用戶界面以及資料庫就可以被編譯成三個獨立的組件或者部署單元(例如 jar 文件、DLL 文件、Gem 文件等)了,這些組件或者部署單元的依賴關係與原始碼的依賴關係是一致的,業務邏輯組件也不會依賴於用戶界面和資料庫這兩個組件。

業務邏輯組件就可以獨立於用戶界面和資料庫來進行部署了,我們對用戶界面或者資料庫的修改將不會對業務邏輯產生任何影響,這些組件都可以被分別獨立地部署。

如果系統中的所有組件都可以獨立部署,那它們就可以由不同的團隊並行開發,這就是所謂的獨立開發能力。

小結

面向對象編程到底是什麼?業界在這個問題上存在著很多不同的說法和意見。然而對一個軟體架構師來說,其含義應該是非常明確的:面向對象編程就是以對象為手段來對原始碼中的依賴關係進行控制的能力, 這種能力讓軟體架構師可以構建出某種插件式架構,讓高層策略性組件與底層實現性組件相分離,底層組件可以編譯成插件,實現獨立於高層組件的開發和部署。

Chap6.函數式編程

函數式編程所依賴的原理,在很多方面其實是早於編程本身出現的。因為函數式編程這種範式強烈依賴於 Alonzo Church 在 20 世紀 30 年代發明的 λ 演算。

函數式程式語言中的變量(Variable)是不可變(Vary)的。

不可變性與軟體架構

為什麼不可變性是軟體架構設計需要考慮的重點呢?為什麼軟體架構帥要操心變量的可變性呢?答案顯而易見:所有的競爭問題、死鎖問題、並發更新問題都是由可變變量導致的。 如果變量永遠不會被更改,那就不可能產生競爭或者並發更新問題。如果鎖狀態是不可變的,那就永遠不會產生死鎖問題。

作為一個軟體架構師,當然應該要對並發問題保持高度關注。我們需要確保自己設計的系統在多線程、多處理器環境中能穩定工作。

可變性的隔離

一種常見方式是將應用程式,或者是應用程式的內部服務進行切分,劃分為可變的和不可變的兩種組件。不可變組件用純函數的方式來執行任務,期間不更改任何狀態。這些不可變的組件將通過與一個或多個非函數式組件通信的方式來修改變量狀態(參見圖 6.1)。

由於狀態的修改會導致一系列並發問題的產生,所以我們通常會採用某種事務型內存來保護可變變量,避免同步更新和競爭狀態的發生。事務型內存基本上與資料庫保護磁碟數據的方式 1 類似,通常釆用的是事務或者重試機制。

這裡的要點是:一個架構設計良好的應用程式應該將狀態修改的部分和不需要修改狀態的部分隔離成單獨的組件,然後用合適的機制來保護可變量

軟體架構師應該著力於將大部分處理邏輯都歸於不可變組件中,可變狀態組件的邏輯應該越少越好。

事件溯源

這裡舉了個簡單的例子,假設某個銀行應用程式需要維護客戶帳戶餘額信息,當它放行存取款事務時,就要同時負責修改餘額記錄。

如果我們不保存具體帳戶餘額,僅僅保存事務日誌,那麼當有人想查詢帳戶餘額時。我們就將全部交易記錄取出,並且每次都得從最開始到當下進行累計。當然,這樣的設計就不需要維護任何可變變量了。

事件溯源,在這種體系下,我們只存儲事務記錄,不存儲具體狀態。當需要具體狀態時,我們只要從頭開始計算所有的事務即可。

這種數據存儲模式中不存在刪除和更新的情況,我們的應用程式不是 CRUD,而是 CR。因為更新和刪除這兩種操作都不存在了,自然也就不存在並發問題。如果我們有足夠大的存儲量和處理能力,應用程式就可以用完全不可變的、純函數式的方式來編程。

小結

每個範式都約束了某種編寫代碼的方式,沒有一個編程範式是在增加新能力。

我們必須面對這種不友好的現實:軟體構建並不是一個迅速前進的技術。今天構建軟體的規則和 1946 年阿蘭·圖靈寫下電子計算機的第一行代碼時是一樣的。儘管工具變化了,硬體變化了,但是軟體編程的核心沒有變。

總而言之,軟體,或者說電腦程式無一例外是由順序結構、分支結構、循環結構和間接轉移這幾種行為組合而成的,無可增加,也缺一不可。

總結

名言警句:

三個編程範式,它們分別是結構化編程(structured programming)、 面向對象編程(object-oriented programming)以及函數式編程(functional programming)。結構化編程範式:對程序控制權的直接轉移進行了限制和規範。面向對象編程範式:對程序控制權的間接轉移進行了限制和規範。面向對象編程在封裝性上得 0 分,在繼承性上勉強可以得 0.5 分(滿分為 1)。多態是我們跨越架構邊界的手段,函數式編程是我們規範和限制數據存放位置與訪問權限的手段,結構化編程則是各模塊的算法實現基礎。如果系統中的所有組件都可以獨立部署,那它們就可以由不同的團隊並行開發,這就是所謂的獨立開發能力。面向對象編程就是以對象為手段來對原始碼中的依賴關係進行控制的能力。所有的競爭問題、死鎖問題、並發更新問題都是由可變變量導致的。一個架構設計良好的應用程式應該將狀態修改的部分和不需要修改狀態的部分隔離成單獨的組件,然後用合適的機制來保護可變量。每個範式都約束了某種編寫代碼的方式,沒有一個編程範式是在增加新能力。軟體,或者說電腦程式無一例外是由順序結構、分支結構、循環結構和間接轉移這幾種行為組合而成的,無可增加,也缺一不可。

關於整潔架構之道的第二部分關於三種編程範式的記錄土撥鼠今天就介紹到這裡了。第三部分從設計原則(SOLID)開始,敬請期待。如果有不同見解歡迎留言討論。

相關焦點

  • 【Clean Architecture-學習筆記-1】編程範式
    demonlee demonlee 377 Sep 29 22:54 main.c-rw-rw-r-- 1 demonlee demonlee 137 Sep 29 23:20 Makefile-rwxrwxr-x 1 demonlee demonlee 17776 Oct 1 21:42 namedPoint-rw-rw-r-- 1 demonlee demonlee 433
  • 聊聊架構:Easy Clean architecture on Android
    本文的代碼示例可以從 Github 中獲得,倉庫地址是:https://github.com/SmartDengg/android-easy-cleanarchitectureWhy we need an architecture?
  • 整潔架構(Clean Architecture)的Go微服務: 程序結構
    code blog[18][7]go at google: language design in the service of software engineering[19][8]go microservice with clean architecture: application
  • 一種新的編程範式
    今天給大家介紹一下一種新的編程範式,這會對大家有所幫助。Vyper的創作為新的編程範式打開了大門。例如,Vyper正在刪除類繼承以及其他功能,因此可以說Vyper偏離了傳統的面向對象編程(OOP)範例,這很好。歷史上,OOP提供了一種表示現實世界對象的機制。例如,OOP允許實例化可以從person類繼承的employee對象。
  • 多範式程式語言-以Swift為例
    從最開始接觸編程就一直和各種客戶端打交道,從最初的嵌入式設備到後來的 J2ME,Blackberry,到現在主要工作在 iOS 平臺。版權說明:本文已得到作者郭麟授權轉載。Swift 的編程範式編程範式是程序語言背後的思想。代表了程序語言的設計者認為程序應該如何被構建和執行的看法。常見的編程範式有:過程式,面向對象,函數式,泛型編程等。
  • 左耳聽風系列之編程範式
    本章導航地圖:1.什麼是編程範式?編程範式:Programming Paradigm。即模範的意思,範式即方式、方法,是一種典型的編程風格。說白了就是指從事軟體工程的一種方法論。4.1面向對象編程範式的定義面向對象編程是一種具有對象概念的程序編程範型,同時也是一種程序開發的抽象方針,它可能包含數據、屬性、代碼與方法。它將對象作為程序的基本單元,將程序和數據封裝其中,以提高軟體的可重用性、靈活性和可擴展性,對象裡的程序可以訪問及修改對象相關聯的數據。在面向對象編程裡,電腦程式會被設計成彼此相關的對象。
  • 程式語言學習心得 (1)-- 掌握編程範式優於牢記語法
    由於涉及編程學習的不同方面,內容較多,將分為多篇內容向大家逐一推送。掌握編程範式優於牢記語法各種程式語言裡的獨特語法簡直是五花八門,下面就隨便選取了其中幾種語言,看看你們知道他們都是什麼語言嗎?1.這會大大提高你的編程水平,使你成為一個更好的程式設計師,儘管在實際工作中極少用到Lisp.」 — 《黑客與畫家》程序語言的編程思想主要受到編程範式的影響,如果你了解這點你就會發現很多新語言其實是新瓶裝老酒。
  • Swift 不是多範式函數式程式語言
    它是一種非常傳統的面向對象語言,著重於泛型編程。我的推測是,人們使用一個特性列表來確定一種語言的範式。但我們使用範式這個詞是有原因的。範式——以某一特定科學科目的理論和方法論為基礎的一種世界觀。「一種世界觀。」
  • 手帳——讀書筆記做起來(第二彈)
    今天主要講如何給自己的讀書筆記做索引,方便日後查閱檢索。學習類讀書筆記,如果有檢索需求,無非是下面兩種情況:1、讀了很多本書,形成了很多筆記,想要查閱的時候不記得要查的那本書對應的筆記寫在哪個本子的哪一頁了。
  • 看到別人厚厚的讀書筆記就羨慕,如何做好真正的讀書筆記?
    讀書筆記是貫穿整個讀書過程的大多數人認為做讀書筆記就是打開書閱讀的時候,才是開始做筆記的時候;其實這種認知是存在問題的,屬於不會做讀書筆記,也不知道真正的讀書筆記為何物。真正的讀書筆記是要貫穿讀書的全過程的,即讀書的5個步驟:選書→購書→讀書→記錄→活用。每一個步驟都需要去記錄做筆記。
  • 如何寫讀書筆記?——不會寫讀書筆記等於不會讀書
    寫讀書筆記是非常重要的,簡單來說,不會寫讀書筆記就是不會讀書。就像是一個人到了寶山,遍地是寶,但是卻空手而歸。即使是帶著寫讀書筆記的想法去閱讀,最後的效果也是比泛泛而讀的效果好太多。這樣你閱讀的時候,是一種主動的狀態,而不是被動的狀態。
  • 高中生讀書筆記怎麼寫 讀書筆記的格式
    高中生讀書筆記怎麼寫 讀書筆記的格式讀書筆記是指讀書時為了把自己的讀書心得記錄下來或為了把文中的精彩部分整理出來而做的筆記。下文有途網小編給大家整理了讀書筆記的寫法,供參考!讀書筆記的格式是什麼樣的(一)提綱式讀書筆記。以記住書的主要內容為目的。
  • 讀書筆記大全:教育教學讀書筆記
    讀書筆記大全:教育教學讀書筆記 2013-08-22 11:53 來源:讀書筆記網 作者:
  • 筆記我的讀書筆記整理術
    在寶貴的閱讀時間如何讓筆記做到極致,就需要有的放矢,尋找適合自己的筆記方法。接下來,我會談談我的讀書筆記方法。讀完一本書肯定多多少少會有自己的想法,這書評的第二部分內容就可以大致描寫一下自己的想法。在描寫自己想法的同時,這裡你的語言風格可以按照作者寫這本書的語言風格來寫,可以鍛鍊自己的文筆。
  • 讀書筆記大全:海邊的卡夫卡讀書筆記
    讀書筆記大全:海邊的卡夫卡讀書筆記 2013-08-21 16:17 來源:讀書筆記網 作者:
  • 如何做讀書筆記?
    是讀書方式不對嗎。那有什麼好的辦法能夠讓我們讀過書之後,記住書中的內容呢? 日本作家奧野宣之的做法是寫讀書筆記,他在《如何有效閱讀一本書》裡,詳細的介紹了他是如何做讀書筆記的。我覺得他的方法很容易操作,值得借鑑。
  • 如何做讀書筆記?超實用的做筆記方法
    這本書像是對《如何閱讀一本書》的輔助和延伸,學會閱讀後,還要學會如何做筆記。筆記幫助你理解書的內容人們經常說「你為別人講解書中的內容時,才會真正理解它」,把記讀書筆記作為目標去讀書,得到的效果也是一樣的。
  • 筆記讀書法,超實用的讀書方法
    第一步:記錄寫讀書筆記的日期,書名,作者名,出版社名稱第二步:摘抄做過標記的內容,可以用「○」表示,要保持內容的原汁原味,不要省略。第三步:在摘抄的內容下面一行寫上自己的評論或者感悟,可以用「☆」表示。
  • 低年級學生這樣做讀書筆記
    如果你告訴他讀和寫之間的距離,如同會吃和會做之間的距離一樣,他就會追著問:那做讀書筆記有用嗎?有用啊。但如果只是想憑著簡單的摘摘抄抄就有用,這如同拿著一本菜譜就以為能做出一桌好菜一樣不靠譜。從讀到寫之間需要的那個橋梁,叫思考。所以,如果確定想做讀書筆記,得在這個項目中充分體現思考的成果。
  • 讀書筆記大全:英語讀書筆記
    讀書筆記大全:英語讀書筆記 2013-08-22 11:43 來源:讀書筆記網 作者: