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

How It Works

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.

When to Use It

  • 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()))
})

Example: Null ggplot2

geom_null <- function(...){
  ggplot2::ggplot() + ggplot2::geom_blank() + ggplot2::theme_void()
}
fig <- 
  tryCatch({
    stopifnot(runif(1) > 0.05) # simulate 5% chance for the subroutine to fail
    
    mtcars %>% 
      ggplot2::ggplot(ggplot::aes(x = mpg, y = hp)) + 
      ggplot2::geom_point()
  }, 
  error = function(e) return(geom_null()) # if subroutine has failed, return null
  )

plot(fig)
  • Useful in shiny dashboards when a visual is dependent on the user selection of what to plot. In this case, you could also add a “call to action” text as a ggplot object.
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")
}

Example: Null mtcars Car

NullCar <- function() mtcars[0,]
print(NullCar())
#>  [1] mpg  cyl  disp hp   drat wt   qsec vs   am   gear carb
#> <0 rows> (or 0-length row.names)

# The Null Car and the NULL value are not the same
identical(NullCar(), NULL)
#> [1] FALSE

# Binding mtcars with the Null Car returns mtcars 
identical(rbind(mtcars, NullCar()), mtcars)
#> [1] TRUE

Example: Null Value Object

Person <- function(given = NA_character_, family = NA_character_){
  tibble::tibble(given = given, family = family) %>% tidyr::drop_na(given)
}

# Instantiating a person with a `given` name, returns a non-null person object
print(Person("Madonna"))
#> # A tibble: 1 × 2
#>   given   family
#>   <chr>   <chr> 
#> 1 Madonna <NA>

# Instantiating a person without a `given` name, returns the null person object
print(Person())
#> # A tibble: 0 × 2
#> # … with 2 variables: given <chr>, family <chr>

Further Reading

Null Object on Wikipedia

Fowler, Martin. 2002. Patterns of enterprise application architecture. Addison-Wesley Longman Publishing Co., Inc.