dom lizarraga

    dominiclizarraga@hotmail.com

    POOD Session 7: Manufacture Role Players & Alter Base Classes (Monkeypatching)

    26 minutes to read

    Session 7: Manufacture Role Players

    Date: September 20, 2025

    This blog post consists in three parts:

    Key Concepts

    Manufacture Role Players

    Watch 1: Contrasting the Concrete Factory with Shameless Green

    In this lesson, we covered how the new Factory differs from the old conditional structure we had. Sandi encouraged us to think about why the first conditional had 0, 1, 2, and "else" branches, while in the new Factory model, we only have 0, 1, and "else" branches.

    We don’t have a "2" branch because in the Shameless Green exercise, becuase some words needed to be transformed from plural to singular. Now, with the new Factory model, it takes care of this as a BottleNumber class.

    The key point of factories is selecting the required path and creating the corresponding instance, no behavior is involved. This contrasts with the first version, where after choosing the path, and behavior was supplied. This is something that factories should avoid doing.

    Watch 2: Fathoming Factories

    Polymorphism is the word we use to refer to that quality where many different objects can respond to the same message.

    Message senders don’t need to know the name of the class. They just need to know the name of the message, and so, the class effectively disappears.

    So Shock should be ignorant of the names of all these classes. It shouldn’t know the implementation details inside these classes. And it shouldn’t know how to pick which class is the right one for any certain circumstance.

    It’s the responsibility of a factory to manufacture the correct instance of a role playing object. The factory knows all those things that the Shock class doesn’t know. The factory knows all the names of the types, and it knows how to pick the right type for any specific circumstance.

    You can think of factories as the place in your application where conditionals go to die.

    Isolating conditionals in factories loosens the coupling between the objects in your application, and it lowers the cost of change.

    If you isolate the conditional in the factory and the names of the types in the factory, it means that it doesn’t matter to the Shock class, to the object wants to interact with a player of the cost role, that class doesn’t care if you add or remove new Shock types.

    The changes that need to be made to add or remove an object all happen in the factory, and the factory contains a very simple bit of code.

    Here is a visual example of how we passed from a convoluted Bycicle/Shock objects to a factory and roles.

    Working on a bicycle simulation. Each bicycle could have optional shocks, and those shocks involved a complex “cost” calculation. As more shock types were added, the best approach was to extract them into their own classes. Each shock class implemented its own cost method, allowing all shocks to respond consistently to the same message.

    This design made it easy to add new shock types without breaking existing code. However, Sandi explained that an even better approach would be to introduce a factory, which is responsible only for knowing which shock to instantiate. The factory doesn’t need to know about the internal implementation of each shock, it just returns the correct one when requested.

    Sandi mentioned 3 types of factories:

    Dimension Options
    Structure Open or Closed
    Choosing Logic Location Factory owns the logic or Logic is dispersed in the objects being chosen
    Type Knowledge Factory knows all type names or Objects register themselves with the factory

    Watch 3: Opening the Factory

    In this lesson, we looked at how our current factory isn’t really open. The fact that we had to manually add BottleNumber6 shows that every time we need a new type, we’d have to edit the factory again. That’s fine if nothing ever changes, but it’s not very flexible. So the goal here is to make it open, to let new BottleNumber classes get created automatically just by following a naming convention.

    To do that, we use a bit of Ruby metaprogramming. The trick is const_get, a method that takes a string like "BottleNumber6" and turns it into the actual class. If the class exists, great — we get it back. If it doesn’t, Ruby throws a NameError, which we can rescue and fall back to a default BottleNumber class. This makes the factory much more dynamic, since it no longer needs a big case statement to decide what to build.

    There was also a quick side trip into how garbage collection works in Ruby, which was actually super helpful. Basically, objects get cleaned up when nothing references them anymore, but class definitions don’t, they stick around forever because Ruby keeps them in a sort of “big hash in the sky.” That’s why you need to be careful with class variables: anything stored there will live for the entire life of the program. So yeah, a bit of metaprogramming magic, a bit of memory awareness, and now our factory is officially open for business.

    class BottleNumber
      def self.for(number)
        begin
          const_get("BottleNumber#{number}")
        rescue NameError
          BottleNumber
        end.new(number)
    
        # case number
        # when 0
        #   BottleNumber0
        # when 1
        #   BottleNumber1
        # else
        #   BottleNumber
        # end.new(number)
      end
    end
    

    It’s not obvious what this code does, and that might be an issue for people. It’s a very valid.

    These metaprogramed classes, a big downside is you can’t find them anymore and someone can wipe them out since are not referenced anymore.

    You know, for every question in OO, the answer is always, "It depends", and here it depends. We’re just trying to save money. We wanna write the most cost effective code. Sometimes adding a little complexity will reduce overall cost.

    You have to be careful because we wanna be overly complex a lot of times, but if complexity will save you money, then it’s cheaper and you should do it.

    Watch 4: Supporting Arbitrary Class Names

    In this lesson, we explored the third way to make the factory open. This time, Sandi proposed using a Hash to support arbitrary class names, meaning the factory isn’t limited to BottleNumber anymore. The implementation is pretty straightforward, but even with this hash approach, it still feels a lot like the first version with the case statement. If you want to add a new class, you still have to edit the hash and add a new key-value pair. So technically, it’s not fully open yet, but it does separate data from logic in a cleaner way.

    The case statement version is definitely easier to read since it’s just a simple list of conditions. But the advantage of the hash approach is that the data driving the program now lives separately, and that opens the door to storing it somewhere else, like in a YAML file or a database.

    One cool side note from this section was how Sandi pointed out that your syntax highlighter can give you hints about your code’s design. Fewer color changes usually mean simpler, more object-oriented code — bigger chunks of the same abstraction. In contrast, when you see lots of color changes, that’s often procedural code with many shifting ideas packed together.

    class BottleNumber
      def self.for(number)
        Hash.new(BottleNumber).merge(
          0 => BottleNumber0,
          1 => BottleNumber1,
          6 => BottleNumber6)[BottleNumber].new(BottleNumber)
      end
    end
    

    Watch 5: Dispersing the Choosing Logic

    After the three types of factories we saw case statements, metaprogramming, hash all of them knew everything about the BottleNumber which is the value of the number, the class names that might be chosen, and how to map which class was needed.

    Having the factory know all these things makes sense, especially if the choosing logic is really simple like we have here, where we’re just testing for simple equality.

    If the logic necessary to choose changes in lockstep with the code in the class being chosen, you want to co-locate those things, you would like the choosing logic to be somehow over in that class.

    You could take the choosing logic and disperse it into each class that might be chosen, and have the factory iterate over those classes and ask each of them if they’re the one.

    class BottleNumber
      def self.for(number)
        [BottleNumber6, BottleNumber1, BottleNumber0, BottleNumber].
          find { |candidate| candidate.handles?(number) }.new(number)
      end
    ...
    
    class BottleNumber0 < BottleNumber
      def self.handles?(number)
        number == 0
      end
    ...
    
    class BottleNumber1 < BottleNumber
      def self.handles?(number)
        number == 1
      end
    ...
    
    class BottleNumber6 < BottleNumber
      def self.handles?(number)
        number == 6
      end
    ...
    

    The biggest thing we did, is we took the logic the choosing logic (qithin factory) that was more closely related to the objects being chosen than to the factory and we dispersed that out into each of the objects, had the factory iterate over that list of objects and ask each one if they were the one.

    Watch 6: Self-registering Candidates

    In the last lesson we extracted the logic from the BottleNumber#self.for to its own classes, then we iterate over each of the options and sent self.handles?(number) but the factory is still closed (hard-coded).

    We have two options:

    • You can get the factory to go, somehow, out in your application and figure out what classes are candidates,

    • or you can do the opposite, you can have each of the candidate classes tell the factory that they want to be on the factory’s list.

    We started with option #2 since it’s possible:

    class BottleNumber
      def self.for(number)
        registry.find { |candidate| candidate.handles?(number) }.new(number)
      end
    
      def self.registry
        @registry ||= []
      end
    
      def self.register(candidate)
        registry.prepend(candidate)
      end
    ...
      BottleNumber.registry(self)
    ...
    class BottleNumber0 < BottleNumber
      BottleNumber.registry(self)
    ...
    
    class BottleNumber1 < BottleNumber
      BottleNumber.registry(self)
    ...
    
    class BottleNumber6 < BottleNumber
      BottleNumber.registry(self)
    ...
    

    This factory is now open for extension, and the choosing logic has been dispersed back into the classes that got chosen.

    That means the factory can manufacture instances of classes whose types it doesn’t know, for reasons that it’s unaware.

    Here is another proposal:

    class BottleNumber
      def self.for(number)
        registry.find { |candidate| candidate.handles?(number) }.new(number)
      end
    
      def self.registry
        @@registry ||= []
      end
    ...
    class BottleNumber6 < BottleNumber
      register(self)
      # BottleNumber.registry(self)
    ...
    

    Above, Sandi compares two nearly identical lines of code, one that calls register directly and another that explicitly references BottleNumber.register.

    At first glance, the version without the class name might seem cleaner or less dependent, but in reality, both have the same number of dependencies: one depends on inheritance (the implicit self), and the other depends on knowing the factory’s class name.

    The key idea is to choose the more stable dependency. Since inheritance structures are more likely to change over time than the name of a class, it’s usually safer to depend on the class name directly. That makes the code clearer, more flexible, and easier for future developers to understand.

    Code that depends on inheritance, that gives me a couple of vulnerabilities.

    Change is inevitable, and every design decision you make is basically a bet on the future. Some bets will save you time and money later, while others will cost you.

    The key is to stay aware that you’re always guessing and to pay attention to how those guesses turn out over time. That’s how you develop better instincts as a programmer.

    Sandi emphasizes that she avoids betting on inheritance, not because she dislikes it, but because experience has shown her it’s less stable than other dependencies.

    The takeaway is to be deliberate about what you depend on.

    Watch 7: Auto-registering Candidates

    This time, instead of having candidates (BottleNumber) registering themselves, we’re going to have the factory who should be candiadtes, and do the register for them.

    We’ve already seen an example of this idea back in the first section, where we made an open factory using const_get. All of the class names followed a convention BottleNumber#.

    Now, we can add a method in common to all classes or have the calsses to be 24-characters-long anything that can make them unique.

    In this case all classes BottleNumber# inherit from BottleNumber and we are now going to leverage the “<” operator to create a subclass in Ruby (e.g., class BottleNumber6 < BottleNumber), Ruby automatically calls the inherited() method on the superclass, passing the new subclass as an argument. By overriding this hook, we can automatically register each subclass in our factory without manual registration calls. This only works if you’re committed to using inheritance.

    class BottleNumber
      def self.for(number)
        registry.find { |candidate| candidate.handles?(number) }.new(number)
      end
    
      def self.registry
        @registry ||= [BottleNumber]
      end
    
      def self.register(candidate)
        registry.prepend(candidate)
      end
    
      def self.inhereted(candidate)
        register(candidate)
      end
    ...
      BottleNumber.registry(self)
    ...
    class BottleNumber0 < BottleNumber
      # BottleNumber.registry(self)
    ...
    
    class BottleNumber1 < BottleNumber
      # BottleNumber.registry(self)
    ...
    
    class BottleNumber6 < BottleNumber
      # BottleNumber.registry(self)
    ...
    

    This is one of those situations we’re placing bets on dependencies. It’s possible that some later change will come and we will wish we’d done this one way or another. Just pay attention to how it turns out. Maybe you’ll learn something that’ll help you make better guesses in the future.

    Alter Base Classes (Monkeypatching)

    In this lesson we examinated the pros and cons of monkeypatching

    This line of code has three dependencies. What are they? Well, one, you know the name of the class, the BottleNumber class. Two, you know the name of a message that you can send to that class, the for message. And three, you know the number, you know the value of a number. That’s three dependencies.

    Can we accomplish the task here in two dependencies instead of three?

    class BottleNumber0 < BottleNumber
      def successor
        BottleNumber.for(99)
      end
    end
    

    These questions may help you decide what and how reduce dependencies.

    • Is the value of the number optional?
    • Do you unconditionally need to know the value of the number?

    Yes, because that’s the thing that you’re turning into a BottleNumber.

    class Integer
      def to_bottle_number
        BottleNumber.for(self)
      end
    end
    
    class BottleNumber0 < BottleNumber
      def successor
        99.to_bottle_number
      end
    ...
    class BottleNumber1 < BottleNumber
      def successor
        1.to_bottle_number
      end
    ...
    class BottleNumber
      def successor
        (number - 1).to_bottle_number
      end
    ...
    

    Here is another approach to monkeypatch the Integer class with module.

    module ToButtleNumber
      refine Integer do
        def to_bottle_number
          BottleNumber.for(self)
        end
      end
    end
    
    # add the keyword "using"
    using ToButtleNumber
    
    class BottleNumber0 < BottleNumber
      def successor
        99.to_bottle_number
      end
    ...
    class BottleNumber1 < BottleNumber
      def successor
        1.to_bottle_number
      end
    ...
    class BottleNumber
      def successor
        (number - 1).to_bottle_number
      end
    ...
    

    Third and last proposal for reducing the dependencies:

    def BottleNumber(number)
      return number if number.kind_of?(BottleNumber)
      BottleNumber.for(number)
    end
    

    What we’re really working with here is conversion functions. That’s the sweet spot for these kinds of refinements.

    # CamelCase
    def BottleNumber(number)
      return number if number.kind_of?(BottleNumber)
      BottleNumber.for(number)
    end
    class BottleNumber0 < BottleNumber
      def successor
        BottleNumber(99)
      end
    ...
    class BottleNumber1 < BottleNumber
      def successor
        BottleNumber(1)
      end
    ...
    class BottleNumber
      def successor
        BottleNumber(number - 1)
      end
    ...
    

    Don’t alter the behavior of the standard library, never, ever, under any circumstances whatsoever.

    Methods like to_bottle_number that are so unique to the problem that you’re working on that it feels really unlikely that going to get a conflict with some more general gem.

    Sandi’s summary:

    Maintainable OO code rests on polymorphism, on constructing applications from families of small, interchangeable objects that represent variants of a role. Instead of writing classes that contain a bunch of conditionals that choose behavior, polymorphism asks you to disperse variants of behavior into classes of their own.

    Placing variants into separate classes eliminates the need for conditionals inside those classes, but it does not completely eliminate the need for conditionals; it just kicks the proverbial conditional can down the road (or back in the stack). In every situation where a role-playing object is needed, some code, somewhere has to know enough to pick the right one.

    Enter factories.

    Factories are where conditionals go to die. They contain conditionals that select classes, and they isolate those conditionals in a single, easily-tested place. They hide the names of role-playing classes and so allow the rest of your application to depend on the API of a role rather than on the concrete names of whatever classes currently exist.

    This block explored the various forms a factory might take, and considered the trade-offs involved. No factory, whether open or closed, whether it owns the choosing logic or asks candidates if they should be chosen, or whether it reaches out for registrants or accept volunteers, is perfect for every situation. All factories, however, enable polymorphism and thus improve your code.

    Managing Dependencies

    • You made changes as requested, then you are told they meant a different thing, so you hacked it and got it back to original state, then they asked another changes, and you create/delete files and you end up hating rails, ruby!

    • In the beginning the app felt so great, you got so much done, you were very effective. We feel happy when we are doing our best work.

    • You were cost-effective, now you’re a money pit.

    • You gotta need to understand the mess

    • Things are so woven together that you cannot pull a single thread without breaking many things.

    • This mess is made up of knowledge. Your app is full of objects and those objects know a lot about themselves and about others.

    • And it’s knowledge of others that weaves objects together.

    • When an object knows something about the other, that is a dependency.

    • When something you depend on changes, you might be forced yo change in turn.

    • You cannot avoid dependencies. Objects collaborate, they always know things about one another.

    • There are many ways to arrange code to solve any problem. And easily arrange dependencies that turn things badly.

    • Controlling dependencies needs understanding of the stability of the various bits of knowledge of your app.

    • It doesn’t matter how stable it’s as long as it’s more stable than the things the object depend upon.

    • Write code that you future self would love

    • OOP helps you:

    • Label the knowledge
    • Complicat the code (refactorring)
    • Isolate the inestability

    • Concrete code is easy to understand, costly to change.

    • Abstract code is harder to understand when you first look at it but it’s much cheaper to change.

    • OO principles push your code to more abstractions, it’s not that there’s not cost involved but the benefits outweigh the costs.

    • If messy code cost you nothing, you can walk away and leave it as it’s.

    • Preference, I’d rather send a message than implement behavior and know things of other object.

    • Instead of asking “Where should i put this code?” you should ask “What message should i send?”

    In the following link you’ll find 50 flashcards and a 10 questions-quiz based on Sandi’s video “Go ahead, make a mess (dependencies)”

    It’s a Google NotebookLM link to exercise Watch: Appreciating the Mechanical Process