Model a domain concept using natural lingo of the domain experts, such as “Passenger”, “Address”, and “Money”.
NullObject()
Caution: NullObject is designed for demonstration purposes. Instead of directly using the design pattern as it appears in the package, you’d have to adjust the source code to the problem you are trying to solve.
Null Object provides special behaviour for particular cases.
Note: The Null Object is not the same as the reserved word in R
NULL
(all caps).
When a function fails in R, some functions produce a run-time error
while others return NULL
(and potentially prompt a warning). What the
function evokes in case of a failure is subjected to its programmer
discretion. Usually, the programmer follows either a punitive or
forgiving policy regarding how run-time errors should be handled.
In other occasions, NULL
is often the result of unavailable data. This
could happened when querying a data source matches no entries, or when
the system is waiting for user input (mainly in Shiny).
If it is possible for a function to return NULL
rather than an error,
then it is important to surround it with null test code, e.g.
if(is.null(...)) do_the_right_thing()
. This way the software would do
the right thing if a null is present.
Often the right thing is the same in many contexts, so you end up writing similar code in lots of places—committing the sin of code duplication.
Instead of returning NULL
, or some odd value such as NaN
or
logical(0)
, return a Null Object that has the same interface as
what the caller expects. In R, this often means returning a data.frame
structure, i.e. column names and variables types, with no rows.
In situations when a subroutine is likely to fail, such as loss of
Internet or database connectivity. Instead of prompting a run-time
error, you could return the Null Object as part of a gracefully failing strategy. A
common strategy employs tryCatch
that returns the Null Object
in the case of an error:
# Simulate a database that is 5% likely to fail read_mtcars <- function() if(runif(1) < 0.05) stop() else return(mtcars) # mtcars null object constructor NullCar <- function() mtcars[0,] # How does the null car object look like? NullCar() #> [1] mpg cyl disp hp drat wt qsec vs am gear carb #> <0 rows> (or 0-length row.names) # Subroutine with gracefully failing strategy set.seed(1814) cars <- tryCatch( # Try reading the mtcars dataset read_mtcars(), # If there is an error, return the Null Car object error = function(e) return(NullCar()) ) # Notice: Whether the subroutine fails or succeeds, it returns a tibble with # the same structure. colnames(cars) #> [1] "mpg" "cyl" "disp" "hp" "drat" "wt" "qsec" "vs" "am" "gear" #> [11] "carb"
In Shiny dashboards
geom_null <- function(...){ ggplot2::ggplot() + ggplot2::geom_blank() + ggplot2::theme_void() } if(exists("user_input")){ ggplot2::ggplot(user_input, ggplot::aes(x = mpg, y = hp)) + ggplot2::geom_point() } else { geom_null() + geom_text(aes(0,0), label = "choose an entry from the list") }
In unit-tests
classes <- function(x) sapply(x, class) test_that("mtcars follows a certain table structure", { # Compare column names expect_identical(colnames(mtcars), colnames(NullCar())) # Compare variable types expect_identical(classes(mtcars), classes(NullCar())) })
Other base design patterns:
Singleton
,
ValueObject()
#> [1] "given" "family"#> [1] 0