原文做者:@VictorSavkin
原文地址:https://vsavkin.com/functiona...
中文翻譯:文藺
譯文地址:http://www.wemlion.com/2016/f...
本文由@文藺 翻譯,轉載請保留此聲明。著做權屬於原做者,請勿用做商業用途。javascript
談到函數式編程時,咱們常提到機制、方法,而不是核心原則。函數式編程不是關於 Monad、Monoid 和 Zipper 這些概念的,雖然它們確實頗有用。從根本上來講,函數式編程就是關於如使用通用的可複用函數進行組合編程。本文是我在重構 TypeScript 代碼時使用函數式的一些思考的結果。java
首先,咱們須要用到如下幾項技術:typescript
儘量使用函數代替簡單值編程
數據轉換過程管道化數組
提取通用函數函數式編程
來,開始吧!函數
假設咱們有兩個類,Employee 和 Department。Employee 有 name 和 salary 屬性,Department 只是 Employee 的簡單集合。this
class Employee { constructor(public name: string, public salary: number) {} } class Department { constructor(public employees: Employee[]) {} works(employee: Employee): boolean { return this.employees.indexOf(employee) > -1; } }
咱們要重構的是 averageSalary 函數。spa
function averageSalary(employees: Employee[], minSalary: number, department?: Department): number { let total = 0; let count = 0; employees.forEach((e) => { if(minSalary <= e.salary && (department === undefined || department.works(e))){ total += e.salary; count += 1; } }); return total === 0 ? 0 : total / count; }
averageSalary 函數接收 employee 數組、最低薪資 minSalary 以及可選的 department 做爲參數。若是傳了 department 參數,函數會計算該部門中全部員工的平均薪資;若不傳,則對所有員工進行計算。翻譯
該函數的使用方式以下:
describe("average salary", () => { const empls = [ new Employee("Jim", 100), new Employee("John", 200), new Employee("Liz", 120), new Employee("Penny", 30) ]; const sales = new Department([empls[0], empls[1]]); it("calculates the average salary", () => { expect(averageSalary(empls, 50, sales)).toEqual(150); expect(averageSalary(empls, 50)).toEqual(140); }); });
需求雖簡單粗暴,可就算不提代碼難以拓展,其混亂是顯而易見的。若新增條件,函數簽名及接口就不得不發生變更,if 語句也會也愈來愈臃腫可怕。
咱們一塊兒用一些函數式編程的辦法重構這個函數吧。
使用函數代替簡單值看起來彷佛不太直觀,但這倒是整理概括代碼的強大辦法。在咱們的例子中,這樣作,意味着要將 minSalary 和 department 參數替換成兩個條件檢驗的函數。
type Predicate = (e: Employee) => boolean; function averageSalary(employees: Employee[], salaryCondition: Predicate, departmentCondition?: Predicate): number { let total = 0; let count = 0; employees.forEach((e) => { if(salaryCondition(e) && (departmentCondition === undefined || departmentCondition(e))){ total += e.salary; count += 1; } }); return total === 0 ? 0 : total / count; } // ... expect(averageSalary(empls, (e) => e.salary > 50, (e) => sales.works(e))).toEqual(150);
咱們所作的就是將 salary、department 兩個條件接口統一塊兒來。而此前這兩個條件是寫死的,如今它們被明肯定義了,而且遵循一致的接口。此次整合容許咱們將全部條件做爲數組傳遞。
function averageSalary(employees: Employee[], conditions: Predicate[]): number { let total = 0; let count = 0; employees.forEach((e) => { if(conditions.every(c => c(e))){ total += e.salary; count += 1; } }); return (count === 0) ? 0 : total / count; } //... expect(averageSalary(empls, [(e) => e.salary > 50, (e) => sales.works(e)])).toEqual(150);
條件數組只不過是組合的條件,能夠用一個簡單的組合器將它們放到一塊兒,這樣看起來更加明晰。
function and(predicates: Predicate[]): Predicate { return (e) => predicates.every(p => p(e)); } function averageSalary(employees: Employee[], conditions: Predicate[]): number { let total = 0; let count = 0; employees.forEach((e) => { if(and(conditions)(e)){ total += e.salary; count += 1; } }); return (count == 0) ? 0 : total / count; }
值得注意的是,「and」 組合器是通用的,能夠複用而且還可能拓展爲庫。
提起結果
如今,averageSalary 函數已健壯得多了。咱們能夠加入新條件,無需破壞函數接口或改變函數實現。
函數式編程的另一個頗有用的實踐是將全部數據轉換過程變成管道。在本例中,就是將 filter 過程提取到循環外面。
function averageSalary(employees: Employee[], conditions: Predicate[]): number { const filtered = employees.filter(and(conditions)); let total = 0 let count = 0 filtered.forEach((e) => { total += e.salary; count += 1; }); return (count == 0) ? 0 : total / count; }
這樣一來計數的 count 就沒什麼用了。
function averageSalary(employees: Employee[], conditions: Predicate[]): number{ const filtered = employees.filter(and(conditions)); let total = 0 filtered.forEach((e) => { total += e.salary; }); return (filtered.length == 0) ? 0 : total / filtered.length; }
接下來,如在疊加以前將 salary 摘取出來,求和過程就變成簡單的 reduce 了。
function averageSalary(employees: Employee[], conditions: Predicate[]): number { const filtered = employees.filter(and(conditions)); const salaries = filtered.map(e => e.salary); const total = salaries.reduce((a,b) => a + b, 0); return (salaries.length == 0) ? 0 : total / salaries.length; }
接着咱們發現,最後兩行代碼和當前域徹底沒什麼關係。其中不包含任何與員工、部門相關的信息。僅僅只是一個計算平均數的函數。因此也將其提取出來。
function average(nums: number[]): number { const total = nums.reduce((a,b) => a + b, 0); return (nums.length == 0) ? 0 : total / nums.length; } function averageSalary(employees: Employee[], conditions: Predicate[]): number { const filtered = employees.filter(and(conditions)); const salaries = filtered.map(e => e.salary); return average(salaries); }
又一次,提取出的函數是徹底通用的。
最後,將全部 salary 部分提出來以後,咱們獲得終極方案。
function employeeSalaries(employees: Employee[], conditions: Predicate[]): number[] { const filtered = employees.filter(and(conditions)); return filtered.map(e => e.salary); } function averageSalary(employees: Employee[], conditions: Predicate[]): number { return average(employeeSalaries(employees, conditions)); }
對比原始方案和終極方案,我敢說,毫無疑問,後者更棒。首先,它更通用(咱們能夠不破壞函數接口的狀況下添加新類型的判斷條件)。其次,咱們從可變狀態(mutable state)和 if 語句中解脫出來,這使代碼更容易閱讀、理解。
函數式風格的編程中,咱們會編寫許多小型函數,它們接收一個集合,返回新的集合。這些函數可以以不一樣方式組合、複用 —— 棒極了。不過,這種風格的一個缺點是代碼可能會變得過分抽象,致使難以讀懂,那些函數組合在一塊兒到底要幹嗎?
我喜歡使用樂高來類比:樂高積木可以以不一樣形式放在一塊兒 —— 它們是可組合的。但注意,並非全部積木都是一小塊。因此,在使用本文所述技巧進行代碼重構時,千萬別妄圖將一切都變成接收數組、返回數組的函數。誠然,這樣一些函數組合使用極度容易,可它們也會顯著下降咱們對程序的理解能力。
本文展現瞭如何使用函數式思惟重構 TypeScript 代碼。我所遵循的是如下幾點規則:
儘量使用函數代替簡單值
數據轉換過程管道化
提取通用函數
強烈推薦如下兩本書:
關注 @victorsavkin 得到更多關於 Angular 和 TypeScript 的知識。