Menu
Coddy logo textTech

Builder-Muster

Teil des Abschnitts Objektorientierte Programmierung der R-Journey von Coddy. Lektion 47 von 57.

Ein Builder setzt ein kompliziertes Objekt Schritt für Schritt zusammen und erstellt es am Ende mit build(). Ein auf Closures basierender Builder hält die Bestandteile in privaten Variablen; jeder Schritt gibt den Builder selbst mit invisible(self) zurück, sodass die Schritte verkettet werden und ein einzeln aufgerufener Schritt nichts ausgibt:

pizza_builder <- function() {
  size <- "medium"
  toppings <- character(0)
  self <- list(
    size = function(s) { size <<- s; invisible(self) },
    add = function(t) { toppings <<- c(toppings, t); invisible(self) },
    build = function() structure(list(size = size, toppings = toppings), class = "pizza")
  )
  self
}
p <- pizza_builder()$size("large")$add("olives")$add("basil")$build()
cat(p$size, "with", paste(p$toppings, collapse = " and "))

Ausgabe:

large with olives and basil

build() ist der Ort, an dem geprüft wird, ob das Objekt vollständig ist. Fehlende Teile führen zu einer Meldung, statt ein halb erstelltes Objekt zu erzeugen:

email_builder <- function() {
  to <- NULL
  subject <- ""
  self <- list(
    to = function(x) { to <<- x; invisible(self) },
    subject = function(x) { subject <<- x; invisible(self) },
    build = function() {
      if (is.null(to)) stop("an email needs a recipient", call. = FALSE)
      paste0("To: ", to, " | Subject: ", subject)
    }
  )
  self
}
print(email_builder()$to("ada@x.org")$subject("Hi")$build())
print(tryCatch(email_builder()$subject("Hi")$build(), error = function(e) conditionMessage(e)))

Ausgabe:

[1] "To: ada@x.org | Subject: Hi"
[1] "an email needs a recipient"

Dieselbe Idee funktioniert mit einfachen Werten und der Pipe |>. Jeder Schritt ist eine Funktion, die die bisherige Beschreibung entgegennimmt und eine aktualisierte Kopie zurückgibt, und der letzte Schritt erstellt das Ergebnis:

request <- function(url) list(url = url, method = "GET", headers = character(0))
with_method <- function(req, m) { req$method <- m; req }
with_header <- function(req, name, value) { req$headers[name] <- value; req }
build_request <- function(req) {
  c(paste(req$method, req$url), paste0(names(req$headers), ": ", req$headers))
}
r <- request("/users") |> with_method("POST") |> with_header("Accept", "json") |> build_request()
cat(r, sep = "\n")

Ausgabe:

POST /users
Accept: json

Ein Builder ersetzt einen Konstruktor mit vielen optionalen Argumenten. Jeder Schritt benennt, was er festlegt, und nicht erwähnte Teile behalten ihre Standardwerte:

# schwer zu lesen: was ist TRUE, was ist 3?
make_report("sales", TRUE, FALSE, 3, "pdf")

# ein Builder sagt, was jeder Wert bedeutet
report_builder("sales")$with_totals()$top(3)$format("pdf")$build()
challenge icon

Aufgabe

Einfach

Vervollständige den auf Closures basierenden query_builder(table) in QueryBuilder.R. Seine Schritte geben den Builder zurück, sodass sie verkettet werden können:

  • select(cols): die anzuzeigenden Spalten (Standardwert *);
  • where(cond): fügt eine Bedingung hinzu; mehrere werden mit AND verbunden;
  • order_by(col) und limit(n);
  • build(): gibt die Abfrage zurück, beispielsweise SELECT name, age FROM users WHERE age > 30 AND city = 'Rome' ORDER BY name LIMIT 5, wobei die nicht gesetzten Teile weggelassen werden.

Der bereitgestellte Code liest die Tabelle und anschließend die Zeilen select name age, where age > 30, order name oder limit 5 und gibt die erstellte Abfrage aus. Danach erstellt er eine zweite Abfrage in einer einzigen Verkettung.

Dein Code gehört in QueryBuilder.R. main.R enthält den bereitgestellten Eingabe-/Ausgabecode und darf nicht bearbeitet werden.

Probier es selbst

source("QueryBuilder.R")

# Gelieferter Ein-/Ausgabecode: belasse ihn wie er ist
input <- suppressWarnings(readLines(file("stdin")))
q <- query_builder(input[1])
for (line in input[-1]) {
  p <- strsplit(line, " ")[[1]]
  rest <- paste(p[-1], collapse = " ")
  if (p[1] == "select") q <- q$select(p[-1])
  if (p[1] == "where") q <- q$where(rest)
  if (p[1] == "order") q <- q$order_by(p[2])
  if (p[1] == "limit") q <- q$limit(as.numeric(p[2]))
}
cat(q$build(), "\n", sep = "")
cat(query_builder("logs")$select("msg")$limit(1)$build(), "\n", sep = "")
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Online-R-Compiler