Menu
CoddyTech

工厂模式

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

工厂是一个创建对象的函数,这样调用方就不需要知道类或它的构造函数。它接收一个描述,例如类型名称,并返回相应的对象:

area <- function(s) UseMethod("area")
area.circle <- function(s) pi * s$r^2
area.square <- function(s) s$side^2
make_shape <- function(type, size) {
  switch(type,
    circle = structure(list(r = size), class = "circle"),
    square = structure(list(side = size), class = "square"))
}
for (t in c("circle", "square")) cat(t, round(area(make_shape(t, 2)), 2), "\n")

输出:

circle 12.57 
square 4 

对于不认识的类型,switch() 会返回 NULL。工厂函数应该停止执行,并显示一条指出该类型的消息,这样就能在出错的地方发现问题:

make_shape <- function(type, size) {
  switch(type,
    circle = structure(list(r = size), class = "circle"),
    square = structure(list(side = size), class = "square"),
    stop("unknown shape: ", type, call. = FALSE))
}
msg <- tryCatch(make_shape("hexagon", 1), error = function(e) conditionMessage(e))
cat(msg)

输出:

unknown shape: hexagon

注册表会将构造函数保存在一个有名称的列表中,因此工厂本身永远不会改变。这里的注册表是一个环境,register_shape() 会在程序运行时添加一种新类型:

registry <- new.env()
register_shape <- function(name, maker) assign(name, maker, envir = registry)
make_shape <- function(name, ...) {
  if (!exists(name, envir = registry, inherits = FALSE)) stop("unknown shape: ", name, call. = FALSE)
  get(name, envir = registry)(...)
}
register_shape("circle", function(r) structure(list(r = r), class = "circle"))
register_shape("rect", function(w, h) structure(list(w = w, h = h), class = "rect"))
print(class(make_shape("rect", 2, 3)))
print(sort(ls(registry)))

输出:

[1] "rect"
[1] "circle" "rect"  

使用这些对象的代码只依赖于通用接口,例如 area()。注册一个带有 area() 方法的新类后,无需更改代码就能使用它:

registry <- new.env()
register_shape <- function(name, maker) assign(name, maker, envir = registry)
make_shape <- function(name, ...) get(name, envir = registry)(...)
area <- function(s) UseMethod("area")
total_area <- function(shapes) sum(vapply(shapes, area, numeric(1)))
register_shape("square", function(a) structure(list(a = a), class = "square"))
area.square <- function(s) s$a^2
register_shape("tri", function(b, h) structure(list(b = b, h = h), class = "tri"))
area.tri <- function(s) s$b * s$h / 2
print(total_area(list(make_shape("square", 2), make_shape("tri", 4, 3))))

输出:

[1] 10
challenge icon

挑战

简单

记录可以导出为多种格式。Exporters.R 中包含通用函数 export(exporter, record) 和 csv_exporter 的方法,该方法会将各个值用逗号连接起来。请在该文件中完成另外两个方法:

  • json_exporter:{"name": "Ada", "age": "36"};
  • text_exporter:name=Ada; age=36。

然后在 Factory.R 中编写工厂函数 make_exporter(format):如果格式是 csv、json 或 text,就返回一个空列表,其类分别为 csv_exporter、json_exporter 或 text_exporter;其他格式则会停止并报错 unknown format: xml(使用 call. = FALSE)。提供的代码会读取字段名称和值,然后读取格式名称,并输出每种格式的导出结果;如果出错,则输出 error: 和错误消息。

请将代码写入 Exporters.R 和 Factory.R。main.R 中包含提供的输入/输出代码,不能编辑。

亲手试试

source("Exporters.R")
source("Factory.R")

# 提供的输入/输出代码:保持原样
input <- suppressWarnings(readLines(file("stdin")))
fields <- strsplit(input[1], ",")[[1]]
values <- strsplit(input[2], ",")[[1]]
record <- setNames(values, fields)
for (fmt in input[-(1:2)]) {
  out <- tryCatch(export(make_exporter(fmt), record), error = function(e) paste("error:", conditionMessage(e)))
  cat(out, "\n", sep = "")
}
quiz icon自我测试

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

面向对象编程中的所有课

自己练习: R 在线编译器