Puppet Design Patterns: The Strategy Pattern
I’m currently in the process of putting together a talk about Design Patterns for Puppet, so I figured I’d blog a bit about it along the way. I’m pretty passionate about patterns for a number of reasons. I think they really help you understand the dynamics of the language you’re working in. They also help you understand design decisions involved in implementing a lasting solution to a problem.
Design patterns are frequently used solutions to common problems. They tend to emerge naturally, and are usually observed rather than invented. The seminal work on design patterns was Design Patterns: Elements of Reusable Object-Oriented Software, commonly referred to as the Gang of Four (GoF) book. If you’re interested in understanding more about design patterns in general, I highly recommend you pick up a copy of the GoF book as well as Design Patterns in Ruby by Russ Olsen.
Not all of the GoF patterns can be directly applied to Puppet, and most of the patterns that do apply need a little bit of massaging to get there. This is mostly due to the fact that Puppet is not an object oriented programming language. That being said, I think there are definitely some lessons to be learned from the GoF when it comes to Puppet. There’s also great value in simply identifying a pattern and giving it a name.
The Strategy Pattern
The Strategy pattern is used when you have a part of an algorithm that must vary under certain conditions. The Strategy pattern uses composition to achieve that variation. The GoF describes this as “pull[ing] the algorithm out into a separate object.”
There are two types of classes in the Strategy pattern: the strategy and the context. The strategy classes are parts of the code that need to vary; they are broken out into separate classes and (ideally) implement a common interface. The context class uses the strategy classes to achieve some complex behavior while abstracting the implementation from the user.
We often see the Strategy pattern used when trying to make our modules work across various platforms. Since Debian and RedHat based Linux distributions use different package managers, we must often vary our repository management logic to accommodate for the differences in configuration semantics and primitives available. Let’s take a look at the puppetlabs/rabbitmq
module for a real-world example of the Strategy pattern.
Strategy in Action: puppetlabs/rabbitmq
In the rabbitmq
class there’s a manage_repos
parameter to enable or disable the management of the package repository. The rabbitmq
module supports yum
and apt
based distributions and breaks the logic to configure those package managers in two separate classes: rabbitmq::repo::rhel
and rabbitmq::repo::apt
. These are the strategy classes. Let’s take a look at how those classes are used in the rabbitmq
class.
if $manage_repos != false {
case $::osfamily {
'RedHat', 'SUSE': {
include '::rabbitmq::repo::rhel'
$package_require = undef
}
'Debian': {
class { '::rabbitmq::repo::apt' :
key_source => $package_gpg_key,
key_content => $key_content,
}
$package_require = Class['apt::update']
}
default: {
$package_require = undef
}
}
} else {
$package_require = undef
}
As you can see, this code looks at the osfamily
fact to determine which rabbitmq::repo
class to include. For RedHat
and SUSE
based distrbutions, the rabbitmq
class includes the rabbitmq::repo::rhel
class. For Debian
based distributions, it includes the rabbitmq::repo::apt
class. The rabbitmq
class is the user of the strategy classes; the GoF called this the context class.
The Strategy pattern here allows us to abstract away the innards of repository management based on the osfamily
running on the client. By breaking out the repository management into separate classes, we’ve made the code more modular and easier to read. We’ve achieved a better separation of concerns by delegating the repository management to a separate class. We’ve also made it easier and less risky to add new platforms to this module.