Puppet Design Patterns: The Factory Pattern
Yesterday, at PuppetCamp Boston, I gave a talk on Puppet Design Patterns. Last week, I wrote a blog post about the Strategy Pattern. In this post I’ll talk about the Factory Pattern.
What’s in a name?
You may be familiar with the Factory Pattern by a different name: the
create_resources
pattern. I’ve decided to call this the Factory Pattern
because the create_resources
function is no longer the only method available
to implement this pattern. With the new language features introduced in Puppet
4, we can now use iteration natively in the DSL instead of relying on the
create_resources
function.
I chose the name “Factory” because this pattern closely aligns with the GoF Factory Method and Abstract Factory patterns. Others have used the Factory Pattern terminology to refer generically to an object that instantiates other objects based on some input or message. While Puppet doesn’t have objects (in the traditional sense), it does have resources. A Puppet class can act as a resource factory, creating other resources based on input from its interface. This is becoming an increasingly common pattern.
The Factory Pattern in Action
The Factory Pattern allows you to have a single point of entry for your module.
Your entry point can use the information passed to it to determine what
resources to create. Let’s take a look at an example using the
create_resources
function.
class filefactory (
$files = {}
) {
validate_hash($files)
unless empty($files) {
create_resources('file', $files)
}
}
The example above is contrived. The filefactory
class does nothing but create
other resources. In reality, your module would likely have other
responsibilities in addition to creating resources. Here, we accept a single
parameter named $files
and we expect the data passed to $files
to be a hash.
We check to make sure the $files
variable is not empty, and then we pass that
hash to the create_resources
function where we tell it to create the resource
type file
. Let’s take a look at what this would look like in Puppet 4.
class filefactory (
Hash $files
) {
$files.each |$name, $resource| {
file { $name:
* => $resource
}
}
}
In the Puppet 4 example, we can use iteration inside the DSL instead of
delegating that to the create_resources
function. In this example we still
accept a $files
parameter, but we don’t provide a default here. We also rely
on the type system to ensure that the data passed to $files
is a hash instead
of using the validate_hash
function. We iterate over the $files
hash and
unpack the key as $name
and the value as $resource
. We then create a file,
passing $name
as the resource title, and we use the splat operator to pass the
$resource
hash. The splat operator passes hash keys to the resource as
parameter names, and the hash values as the parameter values.
In the above examples, the filefactory
class is the resource factory. It
creates new resources based on the data passed to the factory’s interface. Let’s
take a look at a real-world example of this.
The Factory Pattern in ghoneycutt/nrpe
The Factory Pattern allows you to create resources inside your entry point. We
see this in a number of modules, but let’s take a look at Garrett Honeycutt’s
nrpe
module here. This
module consists of one class and one defined type. The class installs and
manages the nrpe
service while the defined type allows the user to configure
nrpe
plugins and checks. Garrett uses the Factory Pattern to allow his users
to simply include nrpe
and let the nrpe
class create the nrpe::plugin
resources based on Hiera data. Let’s take a look at the code (I’ve truncated the
code to focus on the parts we care about).
class nrpe (
...
$plugins = undef,
$hiera_merge_plugins = false,
...
) {
...
if is_string($hiera_merge_plugins) {
$hiera_merge_plugins_bool = str2bool($hiera_merge_plugins)
} else {
$hiera_merge_plugins_bool = $hiera_merge_plugins
}
...
if $plugins != undef {
if $hiera_merge_plugins_bool {
$plugins_real = hiera_hash(nrpe::plugins)
} else {
$plugins_real = $plugins
}
validate_hash($plugins_real)
create_resources('nrpe::plugin',$plugins_real)
}
}
Here, as you can see, Garrett’s nrpe
class accepts the $plugins
and
$hiera_merge_plugins
parameters. If the $plugins
parameter is not undef
,
it passes that data to the create_resources
function depending on whether the
user has enabled hash merging via the $hiera_merge_plugins
parameter. This
use of the Factory Pattern provides a safe and simple interface to this module.
A Note on create_resources
Many people in the community consider use of the create_resources
function as
an antipattern. This is mainly due to the fact that it was created as a
workaround for the lack of iteration support in older versions of Puppet. In the
past, the create_resources
function was often misused. I personally think that
create_resources
is perfectly fine when used judiciously. Now that we have
powerful iteration support in Puppet 4, hopefully the Factory Pattern can begin
to be seen at as a more ‘legitimate’ pattern without the baggage of
create_resources
weighing it down.
A Note on the Name
I don’t claim to be an authority on names. I think this pattern closely resembles what others refer to as a Factory. Hopefully this terminology will catch on, but I don’t represent the community as a whole. This post is meant to start the discussion :)