[譯] 函數式 TypeScript

原文做者:@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 的知識。

相關文章
相關標籤/搜索