Title: | Design Patterns in R |
---|---|
Description: | Build robust and maintainable software with object-oriented design patterns in R. Design patterns abstract and present in neat, well-defined components and interfaces the experience of many software designers and architects over many years of solving similar problems. These are solutions that have withstood the test of time with respect to re-usability, flexibility, and maintainability. 'R6P' provides abstract base classes with examples for a few known design patterns. The patterns were selected by their applicability to analytic projects in R. Using these patterns in R projects have proven effective in dealing with the complexity that data-driven applications possess. |
Authors: | Harel Lustiger [aut, cre] , Tidylab [cph, fnd] |
Maintainer: | Harel Lustiger <[email protected]> |
License: | MIT + file LICENSE |
Version: | 0.4.0 |
Built: | 2024-12-22 12:43:14 UTC |
Source: | CRAN |
Model a domain concept using natural lingo of the domain experts, such as “Passenger”, “Address”, and “Money”.
NullObject()
NullObject()
Other base design patterns:
Singleton
,
ValueObject()
# See more examples at <https://tidylab.github.io/R6P/articles> colnames(NullObject()) nrow(NullObject())
# See more examples at <https://tidylab.github.io/R6P/articles> colnames(NullObject()) nrow(NullObject())
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
R6P::Singleton
-> Repository
new()
Instantiate an object
AbstractRepository$new()
add()
Add an element to the Repository.
AbstractRepository$add(key, value)
key
(character
) Name of the element.
value
(?
) Value of the element. Note: The values in the
Repository
are not necessarily of the same type. That depends on the
implementation of AbstractRepository
.
del()
Delete an element from the Repository.
AbstractRepository$del(key)
key
(character
) Name of the element.
get()
Retrieve an element from the Repository.
AbstractRepository$get(key)
key
(character
) Name of the element.
# See more examples at <https://tidylab.github.io/R6P/articles> # The following implementation is a Repository of car models with their # specifications. # First, we define the class constructor, initialize, to establish a # transient data storage. # In this case we use a dictionary from the collections package # <https://randy3k.github.io/collections/reference/dict.html> # Second, we define the add, del and get functions that operate on the dictionary. # As an optional step, we define the NULL object. In this case, rather then # the reserved word NULL, the NULL object is a data.frame with 0 rows and # predefined column. TransientRepository <- R6::R6Class( classname = "Repository", inherit = R6P::AbstractRepository, public = list( initialize = function() { private$cars <- collections::dict() }, add = function(key, value) { private$cars$set(key, value) invisible(self) }, del = function(key) { private$cars$remove(key) invisible(self) }, get = function(key) { return(private$cars$get(key, default = private$NULL_car)) } ), private = list( NULL_car = cbind(uid = NA_character_, datasets::mtcars)[0, ], cars = NULL ) ) # Adding customised operations is also possible via the R6 set function. # The following example, adds a query that returns all the objects in the database TransientRepository$set("public", "get_all_cars", overwrite = TRUE, function() { result <- private$cars$values() |> dplyr::bind_rows() if (nrow(result) == 0) { return(private$NULL_car) } else { return(result) } }) # In this example, we use the mtcars dataset with a uid column that uniquely # identifies the different cars in the Repository: mtcars <- datasets::mtcars |> tibble::rownames_to_column("uid") head(mtcars, 2) # Here is how the caller uses the Repository: ## Instantiate a repository object repository <- TransientRepository$new() ## Add two different cars specification to the repository repository$add(key = "Mazda RX4", value = dplyr::filter(mtcars, uid == "Mazda RX4")) repository$add(key = "Mazda RX4 Wag", value = dplyr::filter(mtcars, uid == "Mazda RX4 Wag")) ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4") ## Get all the specifications in the repository repository$get_all_cars() ## Delete "Mazda RX4" specification repository$del(key = "Mazda RX4") ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4")
# See more examples at <https://tidylab.github.io/R6P/articles> # The following implementation is a Repository of car models with their # specifications. # First, we define the class constructor, initialize, to establish a # transient data storage. # In this case we use a dictionary from the collections package # <https://randy3k.github.io/collections/reference/dict.html> # Second, we define the add, del and get functions that operate on the dictionary. # As an optional step, we define the NULL object. In this case, rather then # the reserved word NULL, the NULL object is a data.frame with 0 rows and # predefined column. TransientRepository <- R6::R6Class( classname = "Repository", inherit = R6P::AbstractRepository, public = list( initialize = function() { private$cars <- collections::dict() }, add = function(key, value) { private$cars$set(key, value) invisible(self) }, del = function(key) { private$cars$remove(key) invisible(self) }, get = function(key) { return(private$cars$get(key, default = private$NULL_car)) } ), private = list( NULL_car = cbind(uid = NA_character_, datasets::mtcars)[0, ], cars = NULL ) ) # Adding customised operations is also possible via the R6 set function. # The following example, adds a query that returns all the objects in the database TransientRepository$set("public", "get_all_cars", overwrite = TRUE, function() { result <- private$cars$values() |> dplyr::bind_rows() if (nrow(result) == 0) { return(private$NULL_car) } else { return(result) } }) # In this example, we use the mtcars dataset with a uid column that uniquely # identifies the different cars in the Repository: mtcars <- datasets::mtcars |> tibble::rownames_to_column("uid") head(mtcars, 2) # Here is how the caller uses the Repository: ## Instantiate a repository object repository <- TransientRepository$new() ## Add two different cars specification to the repository repository$add(key = "Mazda RX4", value = dplyr::filter(mtcars, uid == "Mazda RX4")) repository$add(key = "Mazda RX4 Wag", value = dplyr::filter(mtcars, uid == "Mazda RX4 Wag")) ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4") ## Get all the specifications in the repository repository$get_all_cars() ## Delete "Mazda RX4" specification repository$del(key = "Mazda RX4") ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4")
Enforces a single instance of a class and provides a global access point.
This is an abstract base class. Instantiating Singleton
directly triggers an error.
Classes inheriting from Singleton
share a single instance.
new()
Create or retrieve an object
Singleton$new()
Other base design patterns:
NullObject()
,
ValueObject()
# See more examples at <https://tidylab.github.io/R6P/articles> address <- function(x) sub("<environment: (.*)>", "\\1", capture.output(x)) # In this example we implement a `Counter` that inherits the qualities of Singleton Counter <- R6::R6Class("Counter", inherit = R6P::Singleton, public = list( count = 0, add_1 = function() { self$count <- self$count + 1 invisible(self) } )) # Whenever we call the constructor on `Counter`, we always get the exact same instance: counter_A <- Counter$new() counter_B <- Counter$new() identical(counter_A, counter_B, ignore.environment = FALSE) # The two objects are equal and located at the same address; thus, they are the same object. # When we make a change in any of the class instances, the rest are changed as well. # How many times has the counter been increased? counter_A$count # Increase the counter by 1 counter_A$add_1() # How many times have the counters been increased? counter_A$count counter_B$count
# See more examples at <https://tidylab.github.io/R6P/articles> address <- function(x) sub("<environment: (.*)>", "\\1", capture.output(x)) # In this example we implement a `Counter` that inherits the qualities of Singleton Counter <- R6::R6Class("Counter", inherit = R6P::Singleton, public = list( count = 0, add_1 = function() { self$count <- self$count + 1 invisible(self) } )) # Whenever we call the constructor on `Counter`, we always get the exact same instance: counter_A <- Counter$new() counter_B <- Counter$new() identical(counter_A, counter_B, ignore.environment = FALSE) # The two objects are equal and located at the same address; thus, they are the same object. # When we make a change in any of the class instances, the rest are changed as well. # How many times has the counter been increased? counter_A$count # Increase the counter by 1 counter_A$add_1() # How many times have the counters been increased? counter_A$count counter_B$count
Model a domain concept using natural lingo of domain experts, such as “Passenger,” “Address,” or “Money.”
ValueObject(given = NA_character_, family = NA_character_)
ValueObject(given = NA_character_, family = NA_character_)
given |
( |
family |
( |
Other base design patterns:
NullObject()
,
Singleton
# See more examples at <https://tidylab.github.io/R6P/articles> # In this example we are appointing elected officials to random ministries, just # like in real-life. Person <- ValueObject Person() # Create a test for objects of type Person # * Extract the column names of Person by using its Null Object (returned by Person()) # * Check that the input argument has all the columns that a Person has is.Person <- function(x) all(colnames(x) %in% colnames(Person())) # A 'Minister' is a 'Person' with a ministry title. The Minister constructor # requires two inputs: # 1. (`Person`) Members of parliament # 2. (`character`) Ministry titles Minister <- function(member = Person(), title = NA_character_) { stopifnot(is.Person(member), is.character(title)) stopifnot(nrow(member) == length(title) | all(is.na(title))) member |> dplyr::mutate(title = title) } # Given one or more parliament members # When appoint_random_ministries is called # Then the parliament members are appointed to an office. appoint_random_ministries <- function(member = Person()) { positions <- c( "Arts, Culture and Heritage", "Finance", "Corrections", "Racing", "Sport and Recreation", "Housing", "Energy and Resources", "Education", "Public Service", "Disability Issues", "Environment", "Justice", "Immigration", "Defence", "Internal Affairs", "Transport" ) Minister(member = member, title = sample(positions, size = nrow(member))) } # Listing New Zealand elected officials in 2020, we instantiate a Person object, # appoint them to random offices, and return a Minister value object. set.seed(2020) parliament_members <- Person( given = c("Jacinda", "Grant", "Kelvin", "Megan", "Chris", "Carmel"), family = c("Ardern", "Robertson", "Davis", "Woods", "Hipkins", "Sepuloni") ) parliament_members appoint_random_ministries(member = parliament_members)
# See more examples at <https://tidylab.github.io/R6P/articles> # In this example we are appointing elected officials to random ministries, just # like in real-life. Person <- ValueObject Person() # Create a test for objects of type Person # * Extract the column names of Person by using its Null Object (returned by Person()) # * Check that the input argument has all the columns that a Person has is.Person <- function(x) all(colnames(x) %in% colnames(Person())) # A 'Minister' is a 'Person' with a ministry title. The Minister constructor # requires two inputs: # 1. (`Person`) Members of parliament # 2. (`character`) Ministry titles Minister <- function(member = Person(), title = NA_character_) { stopifnot(is.Person(member), is.character(title)) stopifnot(nrow(member) == length(title) | all(is.na(title))) member |> dplyr::mutate(title = title) } # Given one or more parliament members # When appoint_random_ministries is called # Then the parliament members are appointed to an office. appoint_random_ministries <- function(member = Person()) { positions <- c( "Arts, Culture and Heritage", "Finance", "Corrections", "Racing", "Sport and Recreation", "Housing", "Energy and Resources", "Education", "Public Service", "Disability Issues", "Environment", "Justice", "Immigration", "Defence", "Internal Affairs", "Transport" ) Minister(member = member, title = sample(positions, size = nrow(member))) } # Listing New Zealand elected officials in 2020, we instantiate a Person object, # appoint them to random offices, and return a Minister value object. set.seed(2020) parliament_members <- Person( given = c("Jacinda", "Grant", "Kelvin", "Megan", "Chris", "Carmel"), family = c("Ardern", "Robertson", "Davis", "Woods", "Hipkins", "Sepuloni") ) parliament_members appoint_random_ministries(member = parliament_members)