Menu
Coddy logo textTech

Patron Builder

Fait partie de la section Programmation orientée objet du Journey R de Coddy. Leçon 47 sur 57.

Un constructeur assemble un objet complexe étape par étape et le crée à la fin avec build(). Un constructeur basé sur une fermeture conserve les composants dans des variables privées  chaque étape renvoie le constructeur lui-même avec invisible(self), ce qui permet d'enchaîner les étapes, et une étape appelée seule n'affiche rien :

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 "))

Sortie :

large with olives and basil

build() est l’endroit où vérifier que l’objet est complet. Les éléments manquants provoquent l’affichage d’un message au lieu de produire un objet à moitié construit :

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)))

Sortie :

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

La même idée fonctionne avec des valeurs simples et le pipe |>. Chaque étape est une fonction qui prend la description obtenue jusque-là et renvoie une copie mise à jour, et la dernière étape construit le résultat :

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")

Sortie :

POST /users
Accept: json

Un builder remplace un constructeur comportant de nombreux arguments facultatifs. Chaque étape indique ce qu’elle définit, et les éléments qui ne sont pas mentionnés conservent leurs valeurs par défaut :

# difficile à lire : qu'est-ce que TRUE, qu'est-ce que 3 ?
make_report("sales", TRUE, FALSE, 3, "pdf")

# un builder dit ce que chaque valeur signifie
report_builder("sales")$with_totals()$top(3)$format("pdf")$build()
challenge icon

Défi

Facile

Complétez le query_builder(table) basé sur une fermeture dans QueryBuilder.R. Ses étapes renvoient le générateur afin de pouvoir être enchaînées :

  • select(cols) : les colonnes à afficher (par défaut *) ;
  • where(cond) : ajoute une condition ; plusieurs conditions sont reliées par AND ;
  • order_by(col) et limit(n) ;
  • build() : renvoie la requête, par exemple SELECT name, age FROM users WHERE age > 30 AND city = 'Rome' ORDER BY name LIMIT 5, en omettant les parties qui n'ont pas été définies.

Le code fourni lit la table, puis les lignes select name age, where age > 30, order name ou limit 5, et affiche la requête construite. Il construit ensuite une seconde requête en une seule chaîne.

Votre code doit être placé dans QueryBuilder.R. main.R contient le code d'entrée/sortie fourni et ne peut pas être modifié.

Essayez vous-même

source("QueryBuilder.R")

# Code d'entrée/sortie fourni : le garder tel quel
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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Compilateur R en ligne