Design patterns provide us with guidelines to help us implement clear and concise maintainable code. When implementing object-oriented design, both duck typing and the Tell Don't Ask pattern go hand in hand to produce easily composable and maintainable code. Also functional programming and common interface techniques such as Monads by design implement Tell Don't Ask.
Here we'll focus on implementing an easy-to-read, easy-to-update code base with object-oriented design, specifically using the Tell Don't Ask principle.
How Code Design Can Go Wrong
Until you learn from making mistakes in design, the proper way to use design patterns tends to not entirely sink in.
For example, I decided to build a command line flash card game for learning the translations of Japanese characters. So it started out pretty simply -- I made the mappings of the Japanese characters available in a YAML file. Loaded those as a raw hash and then proceeded to reference that in the game loop logic. Since this was simple enough, I went ahead and abstracted the game system out into a MVC (Model/View/Controller) arrangement with ERB templates for the view and added I18n support for all of the menu and game display words. At this point, I was very proud of what I had achieved.
But then an idea occurred to me. It's possible to enter in Japanese characters right from the keyboard, so why not add a typing game in along with the translation game? This seemed like an easy thing to do with the current code base simply by adding details about what game modes are available to the YAML files. So I wrote in character mappings for both translation and typing modes.
But then to work with the existing code base, I had to tell each object within the code base about these two states and how to handle each one individually and I needed to maintain the state of the current mode.
When I got this finished and working well, I was still proud of it but one class stood out to me as a total violation of the Tell Don't Ask pattern. Every method in it was an external query method asking for current state or behavior. These methods are from that class and are an example of what not to do:
def mapped_as case @mapping.first when :k @mapped_as.keys.first else @mapped_as.values.first end end def match? input, comp_bitz case comp_bitz.mapping when :k comp_bitz.value == comp_bitz.collection[input] when :v comp_bitz.value == input end end def choose_display key, value case @mapping.last when :k key when :v value end end def choose_expected key, value case @mapping.first when :k key when :v value end end
This is bad design and leads to code that is hard to maintain. This is not duck typing nor is it Tell Don't Ask. The code base depended on this for the behavior throughout and I knew I would have to refactor this...so a year later I did just that.
Refactoring to Tell Don't Ask
When taking up the challenge to refactor my project, I first looked at the problem area I knew I had which was the query object I've demonstrated above. After examining the code base for some time, I realized that it wasn't revisable in small steps but required a big rewrite, and I'll tell you why.
The problems turned out to be more than having a query object on which the system depended. Its root problem was the lack of proper abstraction, a common occurrence when writing code that has "a love for raw types.” By importing the flash cards and using them directly as a hash (a raw type) throughout the application, everything that worked with it had to have code written specifically to understand hashy things and the meanings behind the hash's structure.
When you fail to abstract data into representational objects, then you get stuck with implementing the methods for the abstraction at every interaction of that object rather than just once with creating an object as the abstraction. So I had to Indiana-Jones the refactor -- I had to rebuild it along the side with test-driven development and then swap the code in place, hoping not to set off the cave traps.
The first step to implement and test was to create the abstraction for individual flash cards. In this instance, a flash card is a card with a Japanese character on the face and a translation on the back. A card is to have no other responsibility than to hold these two pieces of information.
When the game was first implemented as only a translation game, it felt like things adhered to the Single Responsibility Principle (where each class object and method is responsible for just one thing) since only one thing was being handled. But introducing a fundamental change to the system made it evident that the current design was not following SRP, as many of the functions needed to handle a duality of behaviors.
Creating the abstraction for a flash card immediately upon loading the data from a YAML file, with only the methods for its two pieces of data, follows SRP and is a core building block from which to implement Tell Don't Ask with duck types.
When designing the proper types for adhering to these design patterns, I find it's best to write out the ideas for your application's abstraction on paper so you can have a clearer bird's-eye view to proceed. Having an application written at all also helps envision its design, even if it is a mess, but if you can skip the mess first by thinking through the design, it will make for an easier time when refactoring.
Thinking through the design, we have flash cards now; a collection of them would be a card set. From there, we can take the card set and give it to one of two kinds of game mode, either typing practice or translation. These two types of game mode will be of the same duck type game.
Now when we pass this duck type of either game through the user interface, it won't care which game is which; they'll both have the same methods defined on each of them, so there aren't any external query methods asking anything. This is our Tell Don't Ask pattern.
Implementing Tell Don't Ask
Here let's look at the Tell Don't Ask implementation. Instead of writing code that would query what mode of game play we're in, we'll just tell it the mode and have it produce what we want by returning our game.
class CardSet attr_reader :card_set def initialize(card_hash) @card_set = card_set_builder(card_hash) end def game(mode) case mode when :translate Modes::Translate.new(self) when :typing_practice Modes::TypingPractice.new(self) else raise "Invalid Game Mode!" end end # ... end
Here we have a class CardSet
, which initializes its set of cards to hold and has no other responsibility. The game
method here is our telling method. We tell it which mode we're going to use, and it returns to us the duck type of the game we're going to play.
Both Modes::Translate
and Modes::TypingPractice
are subclassed to a Modes::Game
class and only define (the same) methods where they differ.
class Translate < Game def match? input current.translation.any? {|value| value == input } end def mode :translate end end class TypingPractice < Game def match? input "#{current}" == input end def mode :typing_practice end end
Now when our game sends the mode
method for display, we have the name provided, and the match?
method is the specific logic that differentiates the two games. This behavior is specific to the game only and therefore where this logic belongs. It also provides an infallible calling mechanism between handling each game, which is the entire purpose of duck types.
Now you may notice that the use of this method match?
is a query method used externally which violates the Tell Don't Ask pattern. And that's with good reason.
Patterns are a general rule to help you, but there are always exception to the rule in programming. Scoring is outside the responsibility of our game duck type and belongs to our user interface's scope of behavior. Also the scoring has been implemented with raw types such as Boolean and integer, so we've stepped out of the role of duck typing for this game mechanic. This area of code has a very low impact with future changes to the system's code and is an acceptable and optimal time to use raw types.
Summary
When I finished the refactor, I had removed around 600 lines of code and added in 400. The code base went from hard to understand and maintain to easy to read and easily updatable with new features. And that's what Tell Don't Ask offers you when you follow this design principle. It gives you much easier to understand code and a much better guarantee for composability.
Whenever you're building a system and you bring in data from an outside source other than the language itself, it's highly recommended you immediately abstract that data into simple abstractions. This will maximize the flexibility you have. Create objects that only have one role following the Single Responsibility Principle.
Last, you can use the Tell Don't Ask principle as the code demonstrates above by telling behavior rather than querying for it, and you may have that method wrap your current object's self
with your duck type and continue forward in the code path.
In object-oriented design, wrapping one object in another object prevents many anti-patterns from occurring and helps keep scopes pure and untampered. And this is exactly what we want.