面向程序(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
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/547405.html
標籤:面向對象
上一篇:怎么使用硬體同步原語替代鎖?

