Press "Enter" to skip to content

Fizz Buzz, Object-Oriented Edition: Exploring the Open/Closed Principle With Polymorphism and Metaprogramming

Fizz Buzz, the children’s game turned coding interview question, requires little more than basic programming literacy to solve. But it has just enough complexity that it can also be used to illustrate some important tenets of object-oriented design through refactoring.

The premise of the game is to count up from one, but replace the number with “Fizz” if it’s divisible by three, “Buzz” if it’s divisible by five, or “FizzBuzz” if it’s divisible by both. For example:

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16…

A Simple Implementation

A straightforward structured programming solution to print these answers to stdout line-by-line in (non-idiomatic) Ruby might look like this:

def divisible_by?(numerator, denominator)
  return numerator.remainder(denominator) == 0
end

def replacement_for(number)
  case
  when divisible_by?(number, 3) && divisible_by?(number, 5)
    return "FizzBuzz"
  when divisible_by?(number, 3)
    return "Fizz"
  when divisible_by?(number, 5)
    return "Buzz"
  else
    return number.to_s
  end
end

def game(limit)
  for number in 1..limit
    puts replacement_for(number)
  end
end

game(16)

Idiomatic Ruby

A more-idiomatic Ruby solution might instead look like this:

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { replacement_for it }

  private
  def replacement_for(number) = case
  when divisible_by?(number, 5) && divisible_by?(number, 3)
    "FizzBuzz"
  when divisible_by?(number, 3)
    "Fizz"
  when divisible_by?(number, 5)
    "Buzz"
  else
    number.to_s
  end
  def divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

puts Game.new.to_a

In case you don’t read Ruby, you’ll want to know that methods automatically return the last expression they evaluate, how Ranges and blocks work, and that all methods after private are private.

You also might be surprised by the endless method syntax. In short, it allows single-expression methods to be defined more succinctly. It adds friction for future maintainers that would otherwise make the method more complicated, and signals that it should be free of side effects.

Object-Oriented?

I’ve also given the code an Object-Oriented patina. When I wrapped the business logic up in a class, I made it look object-oriented, without taking advantage of one of OO’s sharp tools: polymorphism. More on that soon.

There’s really no need to go further than this. Be shameless in your pursuit of working code, and recognize when “good enough” is good enough. But I’m not one to pass up a learning opportunity. Fizz Buzz has just enough complexity to demonstrate a principle that is usually tough to understand through toy examples: the open/closed principle.

Open/Closed Principle

The open/closed principle was first named in Object-Oriented Software Construction by Bertrand Meyer, and later popularized by Robert C. Martin (“Uncle Bob”). Here are selected quotes from Uncle Bob on the principle.

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. […] It would seem that these two attributes are at odds with each other. […] it requires a dedication on the part of the designer to apply abstraction to those parts of the program that the designer feels are going to be subject to change.

Robert C. Martin, “The Open-Closed Principle”

There are several techniques for achieving the OCP on a large scale. All of these techniques are based upon abstraction. Indeed, abstraction is the key to the OCP. […] with a little forethought, we can add new features to existing code, without changing the existing code and by only adding new code.

Robert C. Martin, “Design Principles and Design Patterns”

Plugin systems are the ultimate consummation, the apotheosis, of the Open-Closed Principle. They are proof positive that open-closed systems are possible, useful, and immensely powerful.

Robert C. Martin, “The Open Closed Principle”

The open/closed principle, if applied to Fizz Buzz, would mean that new rules could be added only writing new public code, not by editing existing methods or classes. A common strategy for doing this at a small scale is to use a mutable list of rules, each of which can be interrogated to see if it should apply.

Polymorphism

Polymorphism comes from the Greek for “of many forms.” Biologists use it to describe how certain members of a species can be different from each other. Programmers stole polymorphism from biologists¹ to describe how certain classes can behave differently from each other while implementing the same interface. If objects of two different classes respond to the same messages with the same types, they can be used polymorphically.

Polymorphism is useful because you can write code that calls one or more methods on an object without knowing what class that object is. All you need to know is what messages to send it, and maybe what return types to expect. You can fulfill the open/closed principle with polymorphism by accepting any kind of object that has the right methods and choosing the right one at runtime.

1: In fact, object-oriented programming owes a lot to biologists, but that’s a subject for another article.

An Illustrative Example

class Replacement
  def self.for(n) = @registered.find { it.valid? n }.new(n).to_s
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
  # Subclasses registered later take precedence
  def self.register(subclass) = (@registered ||= []).prepend subclass
end

class TheNumber < Replacement
  superclass.register(self)
  def self.valid?(n) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  superclass.register(self)
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  superclass.register(self)
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  superclass.register(self)
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

This is a big leap from the original idiomatic Ruby code. If you’d like to follow each refactoring step-by-step, expand this:

Click to hide or show step-by-step refactoring

Oh, hi there! I’m glad you clicked into this detailed section. It took me a lot of extra work to write out. If I helped you at all, can you please leave a comment with the word “emerald” in it to let me know it was worth it? Thanks!

First, I wrote a test suite that I could run after every change to make sure I didn’t break anything. Refactoring can be fearless when you’re wearing your seatbelt.

require "minitest/autorun"
require_relative "game"

class GameTest < Minitest::Test
  def test_one
    subject = Game.new(limit: 1)
    assert_equal %w[1], subject.to_a
  end

  def test_default
    subject = Game.new
    expected = %w[1    2    Fizz     4
                  Buzz Fizz 7        8
                  Fizz Buzz 11       Fizz
                  13   14   FizzBuzz 16  ]
    assert_equal expected, subject.to_a
  end
end

The first refactoring was to extract a class. I looked at the words in problem domain I was solving to find “replacement.”

class Replacement
  def self.for(number) = case
  when divisible_by?(number, 5) && divisible_by?(number, 3)
    "FizzBuzz"
  when divisible_by?(number, 3)
    "Fizz"
  when divisible_by?(number, 5)
    "Buzz"
  else
    number.to_s
 end

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. My next refactoring was to extract subclasses for the base case and each special case. This turned for into a factory method. Don’t “break into a cold sweat when you [hear] this word… we love factories.”

class Replacement
  def self.for(number) = case
  when divisible_by?(number, 5) && divisible_by?(number, 3)
    FizzBuzz.new(number).to_s
  when divisible_by?(number, 3)
    Fizz.new(number).to_s
  when divisible_by?(number, 5)
    Buzz.new(number).to_s
  else
    TheNumber.new(number).to_s
  end
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

class TheNumber < Replacement
  def to_s = @n.to_s
end

class Fizz < Replacement
  def to_s = "Fizz"
end

class Buzz < Replacement
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. I learned an important lesson from 99 Bottles of OOP: refactor towards making each part of a conditional identical. I recognized that I could extract three methods so that each predicate was only a single method call.

class Replacement
  def self.for(number) = case
  when fizzbuzz?(number)
    FizzBuzz.new(number).to_s
  when fizz?(number)
    Fizz.new(number).to_s
  when buzz?(number)
    Buzz.new(number).to_s
  else
    TheNumber.new(number).to_s
  end
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end

  private
  def self.fizz?(number)
    divisible_by?(number, 3)
  end
  def self.buzz?(number)
    divisible_by?(number, 5)
  end
  def self.fizzbuzz?(number)
    divisible_by?(number, 5) && divisible_by?(number, 3)
  end
end

class TheNumber < Replacement
  def to_s = @n.to_s
end

class Fizz < Replacement
  def to_s = "Fizz"
end

class Buzz < Replacement
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. I recognized duplication in fizzbuzz?, and saw an opportunity to replace inline code with a method call. This does couple the definition of whether “FizzBuzz” is appropriate to the definitions of whether “Fizz” and “Buzz” are appropriate. That coupling seems inherent to the domain, so I was okay with it.

class Replacement
  def self.for(number) = case
  when fizzbuzz?(number)
    FizzBuzz.new(number).to_s
  when fizz?(number)
    Fizz.new(number).to_s
  when buzz?(number)
    Buzz.new(number).to_s
  else
    TheNumber.new(number).to_s
  end
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end

  private
  def self.fizz?(number)
    divisible_by?(number, 3)
  end
  def self.buzz?(number)
    divisible_by?(number, 5)
  end
  # Coupling to fizz? and buzz? here is core to the business logic, not accidental.
  def self.fizzbuzz?(number)
    fizz?(number) && buzz?(number)
  end
end

class TheNumber < Replacement
  def to_s = @n.to_s
end

class Fizz < Replacement
  def to_s = "Fizz"
end

class Buzz < Replacement
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. Because my extracted methods were specific to the special cases, I pushed those methods down into the subclasses and renamed the parameters.

class Replacement
  def self.for(n) = case
  when FizzBuzz.fizzbuzz?(n)
    FizzBuzz.new(n).to_s
  when Fizz.fizz?(n)
    Fizz.new(n).to_s
  when Buzz.buzz?(n)
    Buzz.new(n).to_s
  else
    TheNumber.new(n).to_s
  end
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

class TheNumber < Replacement
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.fizz?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.buzz?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  # Coupling to Fizz and Buzz here is core to the business logic, not accidental.
  def self.fizzbuzz?(n) = Fizz.fizz?(n) && Buzz.buzz?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. Now that each class is responsible for testing whether a number is valid for that case, I renamed the methods to make them match.

class Replacement
  def self.for(n) = case
  when FizzBuzz.valid?(n)
    FizzBuzz.new(n).to_s
  when Fizz.valid?(n)
    Fizz.new(n).to_s
  when Buzz.valid?(n)
    Buzz.new(n).to_s
  else
    TheNumber.new(n).to_s
  end
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

class TheNumber < Replacement
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  # Coupling to Fizz and Buzz here is core to the business logic, not accidental.
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. I was only one step away from polymorphism, where each case implements the same interface (new(Integer), to_s -> String and valid? -> Boolean). Adding valid? to TheNumber does this, and completes the replace conditional with polymorphism refactoring.

class Replacement
  def self.for(n) = case
  when FizzBuzz.valid?(n)
    FizzBuzz.new(n).to_s
  when Fizz.valid?(n)
    Fizz.new(n).to_s
  when Buzz.valid?(n)
    Buzz.new(n).to_s
  else
    TheNumber.new(n).to_s
  end
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

class TheNumber < Replacement
  def self.valid?(number) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  # Coupling to Fizz and Buzz here is core to the business logic, not accidental.
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. If I ever want to be able to handle more classes than these four, I can’t continue using a case statement. I’ll need to do something that can be changed at runtime without changing the source code of Replacement. An array is easy to use for this purpose. I stored this array in an instance variable in anticipation of using it for a registry later. This is arguably still a conditional, but it provides dynamism that if/else or case statements don’t.

class Replacement
  def self.for(n) = (@registered = [
    # Subclasses listed earlier take precedence
    FizzBuzz, Fizz, Buzz, TheNumber
  ]).find { it.valid? number }.new(n).to_s
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
end

class TheNumber < Replacement
  def self.valid?(n) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. The class is open for extension, but not closed for modification. After future maintainers extend the Replacement class with new subclasses, they still have to modify Replacement to add our class to its registry. Instead, Replacement can allow new subclasses to plug in to the registry themselves by adding a register method.

class Replacement
  def self.for(n) = (@registered = [
    FizzBuzz, Fizz, Buzz, TheNumber
  ]).find { it.valid? number }.new(n).to_s
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
  # Subclasses registered later take precedence
  def self.register(subclass) = (@registered ||= []).prepend subclass
end

class TheNumber < Replacement
  def self.valid?(n) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

The tests passed, so I continued. I removed the built-in subclasses list from the Replacement class, called register from each subclass, and ended up with what you saw earlier in Illustrative Example. For your convenience, I have reproduced it here:

class Replacement
  def self.for(n) = @registered.find { it.valid? n }.new(n).to_s
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end
  # Subclasses registered later take precedence
  def self.register(subclass) = (@registered ||= []).prepend subclass
end

class TheNumber < Replacement
  superclass.register(self)
  def self.valid?(n) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  superclass.register(self)
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  superclass.register(self)
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  superclass.register(self)
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

There are two important things I’d like you to notice when you compare the illustrative example against the original idiomatic Ruby example. First, there are no if or case statements anywhere in the program. Second, and more importantly, you can create new special-case replacements without modifying any classes. All you need to do is create a new subclass of Replacement, define its preconditions and behavior, and register it with the superclass. This is the heart of the open/closed principle: a plugin system. You can create shallow class hierarchies that allow you to extend the behavior of your application without modifying core classes. In this example, you could respond to a new requirement that all prime numbers print an emoji by simply adding a new class.

class Prime < Replacement
  superclass.register(self)
  def FizzBuzz.valid?(n) = n.prime?
  def to_s = "🎉"
end

Is this better than the original idiomatic Ruby example? In the game of Fizz Buzz, it is not—the requirements will never change. But consider it as a metaphor for code you might write for work. Business rules change as companies adapt to market conditions. Those changes require you to modify your software to keep up with new customer demands. Business rules are more complex than Fizz Buzz, and the ability to write a new class for the new behavior without changing existing code is a huge benefit. This design trades away simplicity to get extensibility.

I can stretch this toy example further to teach a bit of metaprogramming. There are two code smells that bother me. First, I am annoyed by the repetition of superclass.register(self).

Metaprogramming for DRY

What counts as metaprogramming in any programming language is a judgment call. In this case, I can take advantage of Ruby’s inherited hook to allow the Replacement superclass to register a subclass when it springs into existence.

class Replacement
  def self.for(n) = @registered.find { it.valid? n }.new(n).to_s
  def initialize(n) = @n = n

  protected
  def self.divisible_by?(numerator, denominator)
    numerator.remainder(denominator).zero?
  end

  private
  # Subclasses defined later take precedence
  def self.register(subclass) = (@registered ||= []).prepend subclass
  def self.inherited(subclass) = register subclass
end

class TheNumber < Replacement
  def self.valid?(n) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.valid?(n) = divisible_by?(n, 3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.valid?(n) = divisible_by?(n, 5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

That saves a few lines of code. I stopped repeating myself. More importantly, it saves future maintainers from needing to remember to register the class. There is no longer any room for a programmer to implement a subclass without registering it, which would be an annoyingly subtle bug.

One more code smell bothers me here.

Metaprogramming for the Single Responsibility Principle

Have you noticed that Replacement, a class about replacing one number with another, is responsible for knowing numeric concepts like divisibility? The move method refactoring gets that responsibility out of Replacement and puts it where it belongs. But where does it belong?

If the numeric values were an object instead of a primitive, I think it would belong there. I would be able to tell them to figure out whether they were divisible by another number, rather than asking them for a remainder and then asking if the result of that is zero. Luckily, in Ruby integers are objects of the Integer class, which inherits from Numeric. Even better, I can add new capabilities to Numeric at runtime by refining the class. I’ll leave the debate over whether I should refine Numeric or monkey-patch it to the comments section. :-)

Fully Object-Oriented Fizz Buzz

module NumericRefinements
  refine Numeric do
    def divisible_by?(other) = remainder(other).zero?
  end
end

class Replacement
  def self.for(n) = @registered.find { it.valid? n }.new(n).to_s
  def initialize(n) = @n = n

  private
  # Subclasses defined later take precedence
  def self.register(subclass) = (@registered ||= []).prepend subclass
  def self.inherited(subclass) = register subclass
end

using NumericRefinements

class TheNumber < Replacement
  def self.valid?(n) = true
  def to_s = @n.to_s
end

class Fizz < Replacement
  def self.valid?(n) = n.divisible_by?(3)
  def to_s = "Fizz"
end

class Buzz < Replacement
  def self.valid?(n) = n.divisible_by?(5)
  def to_s = "Buzz"
end

class FizzBuzz < Replacement
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a
Click to hide or show a version without inheritance

Nothing about this strategy requires inheritance. I used it here to avoid repeating initialize and register, and to hint at polymorphism a language that doesn’t have syntactic support for interfaces. Here is a version without any inheritance. In this case, Replacement is just a factory class.

module NumericRefinements
  refine Numeric do
    def divisible_by?(other) = remainder(other).zero?
  end
end

class Replacement
  def self.for(n) = @registered.find { it.valid? n }.new(n).to_s

  # Subclasses defined later take precedence
  def self.register(subclass) = (@registered ||= []).prepend subclass
end

using NumericRefinements

class TheNumber
  Replacement.register self
  def self.valid?(n) = true
  def initialize(n) = @n = n
  def to_s = @n.to_s
end

class Fizz
  Replacement.register self
  def self.valid?(n) = n.divisible_by?(3)
  def initialize(n) = @n = n
  def to_s = "Fizz"
end

class Buzz
  Replacement.register self
  def self.valid?(n) = n.divisible_by?(5)
  def initialize(n) = @n = n
  def to_s = "Buzz"
end

class FizzBuzz
  Replacement.register self
  def self.valid?(n) = Fizz.valid?(n) && Buzz.valid?(n)
  def initialize(n) = @n = n
  def to_s = "FizzBuzz"
end

class Game
  def initialize(limit: 16) = @limit = limit
  def to_a = (1..@limit).collect { Replacement.for it }
end

puts Game.new.to_a

I understand that this example is over-engineered for a game whose rules never change. But I don’t get paid to write software that never changes—do you? I hope I helped you to understand how to use object-oriented design to make your code resilient to change:

  • You can use polymorphism to replace if/else or case statements.
  • You can use factory methods to choose the right class at runtime.
  • Together, these allow you to extend a class without modifying it.
  • Metaprogramming can save lines of code and mental overhead.
  • Step-by-step refactoring can reveal a design you didn’t see in the first place. If you’re curious how, scroll back up to show the step-by-step refactoring section.

If you’d like to learn more about object-oriented design and how you can create software that is resilient to change (or if you’re waiting for the follow-up to my last blog post about C# and .NET), subscribe to my email list.

One Comment

  1. Text to Coloring Text to Coloring

    This is a great example of how a simple problem like FizzBuzz can be used to highlight core object-oriented principles. I particularly like how you introduced the Open/Closed Principle by encapsulating the logic inside the `Game` class—making it easily extendable without modifying existing code.

Leave a Reply

Your email address will not be published. Required fields are marked *