這篇文章主要關註分散式鎖,包括加鎖和解鎖的過程,鎖的用法,加鎖帶來的代價,對性能的影響以及如何避免死鎖。 ...
面向過程(PO)
面向過程是隨著VB一起來到我的世界,那個時候會的非常有限,感覺能把程式寫出來自己就非常棒了,VB是做那種可視化界面,在工具欄拖個框框放到面板上,然後就在各個事件上寫完整的邏輯,什麼封裝,抽象,繼承一概不懂,就有一種一個方法把實現過程需要的邏輯都羅列了,面向過程分析的是步驟。這樣說過於抽象,舉個例子,洗衣機洗衣服。
1、打開洗衣機
2、放入衣服
3、放入洗衣液
4、關上洗衣機
拆分流程,完成這件事情,都做了哪些流程,不關心誰做的。這樣做行不行,首先肯定沒問題,但是有什麼問題呢?如果在洗衣服的流程中加個柔順劑,那麼這個洗衣服的流程都存在被改動的風險,即可維護性低,不易擴展,不容易復用。
簡單來說面向過程,自頂向下,逐步細化!面向過程,就是按照我們分析好了的步驟,按部就班的依次執行就行了!所以當我們用面向過程的思想去編程或解決問題時,首先一定要把詳細的實現過程弄清楚。一旦過程設計清楚,代碼的實現簡直輕而易舉。
面向過程是一種最為實際的一種思考方式,就算是面向對象的方法也是含有面向過程的思想,可以說面向過程是一種基礎的方法,他考慮的是實際的實現,面向過程是從上往下步步求精。所以面向過程最重要的是模塊化的思想方法,面向對象的方法主要是把事務給對象化,對象包括屬性和行為,當程式規模不是很大時,面向過程的方法還會體現出一種優勢,程式的流程會特別清楚,按著模塊與函數的方法可以很好的組織。
面向對象(OOP)
面向對象則是隨著.Net和Java一起來到我的世界,這個時候已經知道面向過程存在一些問題,也學習過設計模式了,知道程式設計七大原則。
1、單一職責、2、開閉原則、3、里氏替換、4、依賴倒置、5、介面隔離、6、迪米特法則、7、合成復用
也知道面向對象的三大特征,封裝,繼承,多態。
也知道何為對象?現實世界中,任何一個操作或者是業務邏輯的實現都需要一個實體來完成,也就是說,實體就是動作的支配者,沒有實體,就肯定沒有動作發生,其實對應到程式世界,實體即對象,對象由屬性和方法組成,例如人屬性則指身高,體重之類特征性內容,而方法則指能做什麼。面向對象把問題看作由對象的屬性與對象所進行的行為組成。基於對象的概念,以類作為對象的模板,把類和繼承作為構造機制,以對象為中心,來思考並解決問題。
有了這些理論該怎麼解決面向過程中存在問題呢?接著上邊的案例,洗衣機洗衣服,主要涉及兩個對象,洗衣機,有兩個方法打開洗衣機,關上洗衣機。而人則有三個方法,放衣服,放洗衣液。使用面向對象編程方式
1、洗衣機.打開洗衣機
2、人.放衣服
3、人.放洗衣液
4、洗衣機.關上洗衣機
從編程上區別,就是對象成為了方法的執行者,每個流程的執行都需要一個對象,也就是代碼中的類。這樣的好處就是,剛纔在面向過程中想加入柔順劑的過程非常簡單,在人這個對象中添加個方法即可,就是經常說高耦合低內聚,也變的更加容易維護,拓展,復用也變的容易。
所謂的面向對象,就是在編程的時候儘可能的去模擬真實的現實世界,按照現實世界中的邏輯去處理一個問題,分析問題中參與其中的有哪些實體,這些實體應該有什麼屬性和方法,我們如何通過調用這些實體的屬性和方法去解決問題。
OOP 舉例
// 這是初始版本 public class IncomeTaxCalculator{ protected double _threshold = 3500; public double calculate(IncomeRecord record){ double tax = record.salary <= _threshold ? 0 : (record.salary - _threshold) * 0.2; return tax; } } // 往往 Value Object 一旦發佈基本上就很難改變,因為外部已經有很多引用 class IncomeRecord{ String id; // 身份證號 String name; // 姓名 double salary; // 工資 } // 當需求改變時 OOP 的處理方法 public class IncomeTaxCalculatorV2018 extends IncomeTaxCalculator{ // 2018年9月1號後起徵點調整到了 5000,重寫 calculate method 加上這個邏輯 public double calculate(IncomeRecord record){ if(today() > date(2018, 9, 1)){ double _threshold = 5000; } return super.calculate(record); } } IncomeTaxCalculator calculator = new IncomeTaxCalculator(); calculator.calculate(new IncomeRecord(1234, 'tiger', 10000)); // 需求改變後,只需要使用新的 class 即可: IncomeTaxCalculator calculator2018 = new IncomeTaxCalculatorV2018(); calculator2018.calculate(new IncomeRecord(1234, 'tiger', 10000));
從以上例子可以看出來原來的 class 完全不需要任何改動,有任何的新需求只需要新增一個 subclass 繼承原來的 IncomeTaxCalculator 即可。
不可否認,OOP 對可維護性有非常好的支持,把可維護性帶到了一個新的高度。但也有一些弊端。
-
subclass IncomeTaxCalculatorV2018.calculate() 包含了 today(),即 side effect,如果不這麼做,那就需要改變 IncomeRecord,即 input
-
parent class 內部變數 _threshold 發生了改變
-
繼承是面向對象的四大特性之一,用來表示類之間的is-a關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。具體參看《理論七:為何說要多用組合少用繼承?如何決定該用組合還是繼承? 》
如何判斷該用組合還是繼承?
儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。繼承改寫成組合意味著要做更細粒度的類的拆分。這也就意味著,我們要定義更多的類和介面。類和介面的增多也就或多或少地增加代碼的複雜程度和維護成本。所以,在實際的項目開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。
如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不複雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,我們就儘量使用組合來替代繼承。
除此之外,還有一些設計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關係,而模板模式(template pattern)使用了繼承關係。
對於JavaScript的基礎,其是基於原型鏈繼承
更加複雜一些。
來自游戲公司GameSys的Yan Cui發表了博文:《This is why you need Composition over Inheritance》使用了一個很好的案例來說明在實踐中如何使用組合。
EventSourcing/CQRS的倡導者Greg Young還指出,問題域的分解是我們當前軟體工業的最大問題。
問題域的分解不只是局限於代碼組織,微服務也是一個這方面的典型案例,從巨石monolithic鐵板一塊哦系統遷移到微服務是另外一種問題域的解耦。
因此,我們需要使用利刀分解前面描述的類層次樹形結構,使用更小的、可組合的替換它們,包括使用這種特點編程範式-函數式編程,這類語言-GO、F
函數式編程(FP)
這個函數源於數學里的函數,因為它的起源是數學家Alonzo Church發明的Lambda演算(Lambda calculus,也寫作 λ-calculus)。所以,Lambda這個詞在函數式編程中經常出現,可簡單理解成匿名函數。
和麵向對象相比,它要規避狀態和副作用,即同樣輸入一定會給出同樣輸出。
雖然函數式編程語言早就出現,但函數式編程概念卻是John Backus在其1977 年圖靈獎獲獎的演講上提出。
隨著函數式編程這幾年蓬勃的發展,越來越多的“老”程式設計語言已經在新的版本中加入了對函數式編程的支持。所以,如果你用的是新版本,可以不必像我寫得那麼複雜。
In computer science,functional programmingis aprogramming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
看了以上的定義,我對 FP 函數式編程的理解主要有兩點:
-
不改變 input
-
沒有 side effect
和麵向對象編程(object-oriented programming,簡稱 OOP)最大的區別就在於,OOP 裡子類會繼承、改變父類的狀態,並且很多時候 method 不是 pure function,會有很多 side effect 產生。
函數式編程
函數式編程,大量使用函數,減少代碼重覆,提升開發效率;接近自然語言,易於理解;因為不依賴外界狀態,只要給定輸入參數,結果必定相同,方便代碼管理;因為不存在修改變數,天生更易於併發,也能理解,GO語言預設是傳值的。
1、函數式編程的顯著特征-不可變|無副作用|引用透明
在函數式編程中,一個變數一旦被賦值,是不可改變的。沒有可變的變數,意味著沒有狀態。而中間狀態是導致軟體難以管理的一個重要原因,尤其在併發狀態下,稍有不慎,中間狀態的存在很容易導致問題。沒有中間狀態,也就能避免這類問題。無中間狀態,更抽象地說是沒有副作用。說的是一個函數只管接受一些入參,進行計算後吐出結果,除此以外不會對軟體造成任何其他影響,把這個叫做沒有副作用。因為沒有中間狀態,因此一個函數的輸出只取決於輸入,只要輸入是一致的,那麼輸出必然是一致的。這個又叫做引用透明。
2、函數式編程的目標 - 模塊化
結構化編程和非結構化編程的區別,從錶面上看比較大的一個區別是結構化編程沒了“goto”語句。但更深層次是結構化編程使得模塊化成為可能。
像goto語句這樣的能力存在,雖然會帶來一定的便利,但是它會打破模塊之間的界限,讓模塊化變得不容易。
模塊化有諸多好處,首先模塊內部是更小的單一的邏輯,更容易編程;其次模塊化有利於復用;最後模塊化使得每個模塊也更加易於測試。
模塊化是軟體成功的關鍵所在,模塊化的本質是對問題進行分解,針對細粒度的子問題編程解決,然後把一個個小的解決方案整合起來,解決完整的問題。這裡就需要一個機制,可以將一個個小模塊整合起來。函數式編程有利於小模塊的整合,有利於模塊化編程。
3、將函數整合起來 - 高階函數(Higher-order Functions)
高階函數的定義。滿足以下其中一個條件即可稱為高階函數:
-
接受一個或者多個函數作為其入參(takes one or more functions as arguments)
-
返回值是一個函數 (returns a function as its result)
假如我們需要計算出學校中所有女生的成績,和所有女老師的年齡。傳統的編程方式我們是這樣做的:
//用函數式編程的方式求解,可以這樣做: //求所有女生的成績 List<Integer> grades = students.stream().filter(s -> s.sex.equals("femail")).map(s -> {return s.grade}).collect(Collectors.toList()); //求所有女老師的年齡 List<Integer> ages = teachers.stream().filter(t -> t.sex.equals("femail")).map(t -> {return t.age}).collect(Collectors.toList());
例子中使用的是比較著名的高階函數,map, filter,此外常聽到的還有reduce。這些高階函數將迴圈給抽象了。map,filter裡面可以傳入不同的函數,操作不同的數據類型。但高階函數本身並不局限於map,reduce,filter,滿足上述定義的都可以成為高階函數。高階函數像骨架一樣支起程式的整體結構,具體的實現則由作為參數傳入的具體函數來實現。因此,我們看到高階函數提供了一種能力,可以將普通函數(功能模塊)整合起來,使得任一普通函數都能被靈活的替換和復用。
組合與管道
組合函數,目的是將多個函數組合成一個函數
舉個簡單的例子:
function afn(a){ return a*2; } function bfn(b){ return b*3; } const compose = (a,b)=>c=>a(b(c)); let myfn = compose(afn,bfn); console.log( myfn(2));
可以看到compose實現一個簡單的功能:形成了一個新的函數,而這個函數就是一條從 bfn -> afn 的流水線
下麵再來看看如何實現一個多函數組合:
const compose = (...fns)=>val=>fns.reverse().reduce((acc,fn)=>fn(acc),val);
compose執行是從右到左的。而管道函數,執行順序是從左到右執行的
const pipe = (...fns)=>val=>fns.reduce((acc,fn)=>fn(acc),val);
組合函數與管道函數的意義在於:可以把很多小函數組合起來完成更複雜的邏輯
柯里化
柯里化是把一個多參數函數轉化成一個嵌套的一元函數的過程
一個二元函數如下:
let fn = (x,y)=>x+y;
轉化成柯里化函數如下:
const curry = function(fn){ return function curriedFn(...args){ if(args.length<fn.length){ return function(){ return curriedFn(...args.concat([...arguments])); } } return fn(...args); } } const fn = (x,y,z,a)=>x+y+z+a; const myfn = curry(fn); console.log(myfn(1)(2)(3)(1));
關於柯里化函數的意義如下:
• 讓純函數更純,每次接受一個參數,鬆散解耦
• 惰性執行
4、惰性計算
除了高階函數和仿函數(或閉包)的概念,還引入了惰性計算的概念。
在惰性計算中,表達式不是在綁定到變數時立即計算,而是在求值程式需要產生表達式的值時進行計算。延遲的計算使您可以編寫可能潛在地生成無窮輸出的函數。因為不會計算多於程式的其餘部分所需要的值,所以不需要擔心由無窮計算所導致的 out-of-memory 錯誤。一個惰性計算的例子是生成無窮 Fibonacci 列表的函數,但是對第n個Fibonacci 數的計算相當於只是從可能的無窮列表中提取一項。
5、函數是一等公民(first-class citizen
函數式編程第一個需要瞭解的概念就是函數。在函數式編程中,函數是一等公民(first-class citizen):
-
可按需創建
-
可存儲在數據結構中
-
可以當作實參傳給另一個函數
-
可當作另一個函數的返回值
對象,是OOP語言的一等公民,它就滿足上述所有條件。所以,即使語言沒有這種一等公民的函數,也完全能模擬(之前就用Java對象模擬出一個函數Predicate)。
在函數式編程中函數是"第一等公民",所謂"第一等公民"(first class),指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變數,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。
舉例來說,下麵代碼中的print變數就是一個函數,可以作為另一個函數的參數。
var print = function(i){ console.log(i);}; [1,2,3].forEach(print);
看待函數式編程,如果只看到一些具體的特性,像map,reduce,緩求值等等,就會覺得不過如此,甚至覺得不過是把一些常用的邏輯整理了一下而已,那就錯過了函數式編程的精彩。我們需要從函數式編程的思想基石--基於函數構建軟體,以及函數式編程對於模塊化的益處,我們就能看到函數式編程思想的魅力。
FP 舉例
// 初始方法 function calculator(record){ const threshold = 3500; return record.salary <= threshold ? 0 : (record.salary - _threshold) * 0.2; } // 應對需求,新增的計算方法 function calculatorV2018(record){ const threshold = 5000; return record.salary <= threshold ? 0 : (record.salary - _threshold) * 0.2; } // 高階函數 higher-order function,包裝之前的函數 function getCalculator(oldFn, newFn, today){ if(today() > date(2018, 9, 1)){ return newFn; }else{ return oldFn; } } calculator(new IncomeRecord(1234, 'tiger', 10000)); // 需求改變後,用高階函數包裝之前的函數 const taxCalculatorV2018 = getCalculator(calculator, calculatorV2018, new Date(2018, 9, 1)); taxCalculatorV2018(new IncomeRecord(1234, 'tiger', 10000));
儘管在OOP中可以創建純函數,但它並不是這種範式的主要焦點,因為它的主要單元是對象,而對象的設計又是為了與對象的狀態進行交互。
純函數是非常簡單和可重用的代碼塊,在實現一個程式時可以非常實用。因此,函數是函數式編程的主要單元是非常合理的。
-
良好的可讀性和理解力,因為它們是原子性的。
-
純函數是跨分散式計算集群和CPU並行處理的良好解決方案。
-
由於純函數是獨立的,所以在代碼中重構和重組它們更容易。另外,獨立於外部也使它們更具有可移植性,更容易在其他應用程式中重覆使用。
-
純函數可以很容易地被測試,考慮到所需要的只是測試輸入和確認(預期)結果。
純函數的缺點是,它將操作置於數據之上。如果一個純函數只產生與輸入相同的輸出,那麼它就不能返回其他不同的(也許是有意義的)值。由於這個原因,函數式編程具有極強的操作性、實用性,而且正如其名稱所示,是功能性的。
面向對象的編程在很大程度上依賴於類和對象的概念,而類和對象又包含函數和數據。正如所解釋的,類是一個既定的藍圖(或原型),對象就是從這個藍圖中建立起來的。因此,類代表了某一對象類型所共有的一組方法(或屬性)。反過來,一個對象是OOP的基本單位,代表現實生活中的實體。一個對象必須有。
-
一個身份一個唯一的名字;擁有一個唯一的ID可以使對象與其他對象進行交互。
-
一個狀態一個對象的狀態反映了一個對象的屬性或特性。
-
行為一個對象的方法,以及對象將如何響應並與其他對象互動。
例如,讓我們想象一下,我們有 "運動員1 "這個對象,在這個對象中,我們通過屬性擁有關於這個對象的所有數據。因此,狀態可以是運動、身高、體重、獎盃、國家等等。這些屬性存儲了數據,而一個對象的數據可以通過歸屬於一個對象的函數來操作。在這種情況下,這個對象的方法可以是攻擊、防禦、跳躍、跑步、衝刺等。此外,開發者可以通過在對象的代碼模塊中聲明變數來創建屬性。
總之,在OOP語言中,數據被存儲在屬性中,而背後的邏輯在於函數和各自的方法中。關於面向對象的編程,方法是屬於一個類或對象的功能;方法是由一個特定的類甚至對象**"擁有"**。相比之下,函數是 "自由 "的,意味著它們可以在代碼的任何其他範圍內,不屬於類或對象。
因此,一個方法總是一個函數,但一個函數不總是一個方法。當對象包含緊密合作的屬性和方法時,這些對象屬於同一個類。
在OOP語言中,編寫代碼是為了定義類,並由此定義各自的對象。純粹的面向對象語言遵循四個核心原則:封裝、抽象、繼承和多態性。
可變的與不可變的
面向對象編程可以支持可變數據。相反,函數式編程則使用不可變的數據。在這兩種編程範式中
-
不可變的對象指的是一個一旦創建就不能修改其狀態的對象。
-
可變的對象則正好相反;一個對象的狀態甚至在創建後也可以被修改。
在純函數式編程語言(例如Haskell)中,不可能創建可變的對象。因此,對象通常是不可變的。在OOP語言中,答案並不那麼直接,因為它更多地取決於每種OOP語言的規範。為了提高運行時的效率以及可讀性,字元串和具體對象可以被表達為不可變的對象。另外,在處理多線程應用程式時,不可變的對象會非常有幫助,因為它避免了數據被其他線程改變的風險。
可變對象也有其優勢
它們允許開發者直接在對象中進行修改,而不需要分配對象,從而節省了時間,加快了項目的進度。然而,這要由開發者和開發團隊根據項目的目標來決定它是否真的有回報。例如,變異也會為bug打開更多的大門,但有時它的速度是非常合適的,甚至是必要的。
因此,OOP可以支持可變性,但其語言也可能允許不可變性。Java、C++、C#、Python、Ruby和Perl可以被認為是面向對象的編程語言,但它們並不完全支持可變性或不可變性。例如,在Java中,字元串是不可變的對象。儘管如此,Java也有字元串的可變版本。同樣地,在C++中,開發者可以將新的類實例聲明為不可變的或可變的。另一個很好的例子是Python,它的內置類型是不可變的(例如,數字、布爾、frozensets、字元串和圖元);然而,自定義類通常是可變的。
同樣重要的是要記住,許多提到的語言不是100%的函數式編程或面向對象。例如,Python是最流行的語言之一,它確實是一種多範式的語言。因此,它可以根據開發者的偏好,採用更多的函數式或OOP方法。
三者的對比
面向過程
-
優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發、 Linux/Unix等一般採用面向過程開發,性能是最重要的因素。
-
不足:不易維護、不易復用、不易擴展
面向對象
-
優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統 更加靈活、更加易於維護
-
缺點:因為需要創建大量的類,性能不高,不適合對性能要求很苛刻的地方。
函數式編程
-
優點:變數不可變,引用透明,天生適合併發。表達方式更加符合人類日常生活中的語法,代碼可讀性更強。實現同樣的功能函數式編程所需要的代碼比面向對象編程要少很多,代碼更加簡潔明晰。函數式編程廣泛運用於科學研究中,因為在科研中對於代碼的工程化要求比較低,寫起來更加簡單,所以使用函數式編程開發的速度比用面向對象要高很多,如果是對開發速度要求較高但是對運行資源要求較低同時對速度要求較低的場景下使用函數式會更加高效。
-
缺點:由於所有的數據都是不可變的,所以所有的變數在程式運行期間都是一直存在的,非常占用運行資源。同時由於函數式的先天性設計導致性能一直不夠。雖然現代的函數式編程語言使用了很多技巧比如惰性計算等來優化運行速度,但是始終無法與面向對象的程式相比,當然面向對象程式的速度也不夠快。函數式編程雖然已經誕生了很多年,但是至今為止在工程上想要大規模使用函數式編程仍然有很多待解決的問題,尤其是對於規模比較大的工程而言。如果對函數式編程的理解不夠深刻就會導致跟面相對象一樣晦澀難懂的局面。
FP 和 OOP 都是前輩們探索出來為更好的維護和協同工作而人為發明的 concept,沒有誰好誰壞之分。遇到不同的使用場景,選擇最合適的即可。
函數式編程與OOP:關鍵的區別
函數式編程 | OOP |
---|---|
一個函數是主要單位 | 對象是主要單位 |
純粹的函數沒有副作用 | 方法可能有副作用 |
遵循更多的聲明式編程模型 | 主要遵循命令式的編程方式 |
在純函數式編程語言中,不可能創建可變的對象。因此,對象通常是不可變的。 | 在OOP語言中,答案並不那麼直接,因為它更多地取決於每種OOP語言的規範。因此,OOP可以同時支持可變和不可變的對象。 |
函數式編程寫的是純函數。純函數只產生與輸入相同的輸出。因此,函數式編程具有極強的操作性、實用性,而且正如其名稱所示,是功能性的。 | OOP不像函數式編程那樣具有操作性。事實上,OOP將數據存儲在對象中,數據的優先順序高於操作。 |
如何選擇,其是都是又項目架構所決定。
參考文章:
我對函數式編程、面向對象和麵向過程三者的理解 https://blog.csdn.net/jiadajing267/article/details/121216442
面向對象編程 V.S 函數式編程 https://bbs.huaweicloud.com/blogs/303315
每日一題:說說你對函數式編程的理解?優缺點? https://developer.aliyun.com/article/1073601
The do's and don'ts of OOP https://www.imaginarycloud.com/blog/the-dos-and-donts-of-oop/
函數式編程與OOP的內容及主要區別 https://juejin.cn/post/7112646218031267847
轉載本站文章《再談編程範式(3):理解面向過程/面向對象/函數式編程的精髓》,
請註明出處:https://www.zhoulujun.cn/html/theory/engineering/model/8932.html