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