软件复用组件

成功的组件模型都依赖于非常简单的基础,这些基础使得组件能够创建、生成更加复杂的结构。

面向对象编程从未建立过这种基础性、通用的标准,其中组件的基础单位是对象,但对象还不够底层。每个开发者都会为客户创造一个新的“标准”,没有任何团队能为客户应该有哪些字段达成一致,因为它们需要满足客户在不同场景下的不同需求,因而需要不同的数据和运算方式。

在函数式编程语言的集合类型中,像List、VectorMap等,它们都共享一些共同的操作,这些操作大多定义在Seq这个抽象特征中。除了foreach操作外,所有操作都是纯净的高阶函数,没有副作用,且都接受函数作为参数,具体工作由该函数完成。这种高阶函数与离散数据中的组合器(combinator)概念非常接近。

我们可以将这些组合器串联起来,从而用很少的代码完成复杂的功能。对于特定问题,我们可以将数据和需要实现的行为分离。这与通常的面向对象编程的方法正好相反,面向对象总是将数据和行为绑定在一起。在自定义的类中创建所需逻辑的临时实现,是面向对象的典型做法

工资单计算器的例子


In [4]:
case class Employee (
    name: String,
    title: String,
    annualSalary: Double,
    taxRate: Double,
    insurancePremiumsPerWeek: Double)


defined class Employee

In [5]:
val employees = List(
    Employee("Buck Trends", "CEO", 200000, 0.25, 100.0),
    Employee("Cindy Banks", "CFO", 170000, 0.22, 120.0),
    Employee("Joe Coder", "Developer", 130000, 0.20, 120.0)
)


employees: List[Employee] = List(
  Employee("Buck Trends", "CEO", 200000.0, 0.25, 100.0),
  Employee("Cindy Banks", "CFO", 170000.0, 0.22, 120.0),
  Employee(
    "Joe Coder",
    "Developer",
    130000.0,
    0.2,
    120.0
  )
)

定义Employee类放置雇员的字段类型,实际应用中,这些数据可能从数据库中加载。Employee类型很简单,只需要定义少量行为,在经典的面向对象设计中,我们可能会给Employee添加很多行为,用于工资计算或实现其他域的逻辑。在这里所选择的设计提供了最佳的分离,同时这种设计非常简洁。如果Employee的结构发生变化,需要修改代码时,维护的成本非常小。


In [6]:
// 计算每周工资单
val netPay = employees map { e =>
    val net = (1.0-e.taxRate) * (e.annualSalary / 52.0) -
        e.insurancePremiumsPerWeek 
    (e, net)
}


netPay: List[(Employee, Double)] = List(
  (
    Employee(
      "Buck Trends",
      "CEO",
      200000.0,
      0.25,
      100.0
    ),
    2784.6153846153848
  ),
  (
    Employee(
      "Cindy Banks",
      "CFO",
...

In [8]:
// 打印工资单
println("** Paychecks:")
netPay foreach {
    case (e, net) => println(f"  ${e.name+':'}%-16s ${net}%10.2f")
}


** Paychecks:
  Buck Trends:        2784.62
  Cindy Banks:        2430.00
  Joe Coder:          1880.00


In [9]:
// 生成报表
val report = (netPay foldLeft (0.0, 0.0, 0.0)) {
    case ((totalSalary, totalNet, totalInsurance), (e, net)) =>
        (totalSalary + e.annualSalary / 52.0,
            totalNet + net,
            totalInsurance + e.insurancePremiumsPerWeek)
}


report: (Double, Double, Double) = (9615.384615384615, 7094.615384615385, 340.0)

In [10]:
println("\n** Report:")
println(f"  Total Salary:    ${report._1}%10.2f")
println(f"  Total Net:       ${report._2}%10.2f")
println(f"  Total Insurance: ${report._3}%10.2f")


** Report:
  Total Salary:       9615.38
  Total Net:          7094.62
  Total Insurance:     340.00