Menu
CoddyTech

装饰器模式

本课属于 Coddy R 学习旅程的“面向对象编程”单元。第 46 课,共 57 课。

装饰器会包装某个对象,并在保持接口不变的同时添加行为。对于函数,装饰器接收一个函数,并返回一个新函数,在调用前后执行额外操作:

with_logging <- function(f, name) {
  function(x) {
    cat("calling", name, "with", x, "\n")
    f(x)
  }
}
double <- function(x) x * 2
logged <- with_logging(double, "double")
print(logged(21))

输出:

calling double with 21 
[1] 42

装饰器可以记住结果。with_cache() 会将每个答案保存在一个环境中,只有遇到新的参数时才会调用被包装的函数:

with_cache <- function(f) {
  cache <- new.env()
  function(x) {
    key <- as.character(x)
    if (!exists(key, envir = cache, inherits = FALSE)) assign(key, f(x), envir = cache)
    get(key, envir = cache)
  }
}
work <- 0
slow_square <- function(x) {
  work <<- work + 1
  x^2
}
fast <- with_cache(slow_square)
for (x in c(3, 4, 3, 3, 4)) fast(x)
cat("results computed:", work)

输出:

results computed: 2

对象也可以被装饰。每个装饰器都是一个对象,它会在一个字段中保存被包装的对象,并响应相同的泛型函数,添加自己的部分,并让内部对象处理其余部分:

cost <- function(d) UseMethod("cost")
describe <- function(d) UseMethod("describe")
cost.coffee <- function(d) 2
describe.coffee <- function(d) "coffee"
with_milk <- function(inner) structure(list(inner = inner), class = "milk")
cost.milk <- function(d) cost(d$inner) + 0.5
describe.milk <- function(d) paste(describe(d$inner), "+ milk")
cup <- with_milk(with_milk(structure(list(), class = "coffee")))
cat(describe(cup), "=", cost(cup))

输出:

coffee + milk + milk = 3

装饰器可以层层嵌套,它们的顺序很重要:最外层的装饰器在进入时最先运行,在退出时最后运行:

exclaim <- function(f) function(s) paste0(f(s), "!")
twice <- function(f) function(s) paste(f(s), f(s))
greet <- function(name) paste("hi", name)
a <- twice(exclaim(greet))
b <- exclaim(twice(greet))
cat(a("ada"), "/", b("ada"))

输出:

hi ada! hi ada! / hi ada hi ada!
challenge icon

挑战

简单

饮品会添加额外配料。Drink.R 中包含泛型 cost() 和 describe(),以及基础饮品 new_coffee(),它的价格是 2,自我描述为 coffee。在 Extras.R 中编写三个装饰器,每个装饰器都是一个对象,包含它所包装的饮品,保存在字段 inner 中:

  • add_milk(d):类为 milk,价格增加 0.50,描述增加 + milk;
  • add_sugar(d):类为 sugar,价格增加 0.20,描述增加 + sugar;
  • add_syrup(d, flavour):类为 syrup,包含字段 flavour,价格增加 0.80,描述增加 + vanilla syrup。

编写它们的 cost() 和 describe() 方法时,先调用内部饮品的方法。提供的代码会读取诸如 milk,syrup vanilla,sugar 的行(空行表示纯咖啡),并输出 coffee + milk + vanilla syrup + sugar: 3.50。

你的代码写在 Drink.R 和 Extras.R 中。main.R 包含提供的输入/输出代码,不能编辑。

亲手试试

source("Drink.R")
source("Extras.R")

# 提供的输入输出代码:保持原样
input <- suppressWarnings(readLines(file("stdin")))
for (line in input) {
  d <- new_coffee()
  if (nchar(line) > 0) for (extra in strsplit(line, ",")[[1]]) {
    p <- strsplit(extra, " ")[[1]]
    d <- switch(p[1], milk = add_milk(d), sugar = add_sugar(d), syrup = add_syrup(d, p[2]))
  }
  cat(sprintf("%s: %.2f", describe(d), cost(d)), "\n", sep = "")
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: R 在线编译器