I'm developing a theory that I don't really see the classic design patterns of "Head First Design Patterns" by Eric and Elizabeth Freeman (with Kathy Sierra and Bert Bates) so much in production code. The first pattern in the book is the Strategy pattern and its example is just awful and makes me want to not continue reading the book. In it a decoy duck is a type of a duck in child-to-parent inheritance and right away that smells bad. I recreated their decoy duck example with my own RubberDuck example in C# instead of Java. Let's talk through it and why I'm slow to both fall in love with the Strategy pattern and come up with excuses to use it everywhere. First of all, these three tests all pass in my example:
using FowlApp.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FowlApp.Tests
{
[TestClass]
public class RubberDuckTests
{
private RubberDuck _rubberDuck;
[TestInitialize()]
public void SetupTests()
{
_rubberDuck = new RubberDuck();
}
[TestMethod]
public void RubberDuck_cannot_fly()
{
Assert.AreEqual(_rubberDuck.Fly(), "I can't fly.");
}
[TestMethod]
public void RubberDuck_cannot_quack()
{
Assert.AreEqual(_rubberDuck.Quack(), "I can't quack.");
}
[TestMethod]
public void RubberDuck_looks_rubbery()
{
Assert.AreEqual(_rubberDuck.Display(), "I look rubbery.");
}
}
}
Alright, let's say you have a variety of common things that merit a child-to-parent inheritance of some shape, and let's say that many of the children, but not all of them, have some common functionality. In the name of maintainability and not repeating oneself, it would be nice if the common code could be kept in one spot and there is no way to really tuck it into the parent without getting hacky as not every child has the common behavior. The Strategy pattern would have you keep a field for the common behavior as an interface. The Duck that RubberDuck inherits from has two such fields, one for flight and one for speech. It looks like this:
namespace FowlApp.Core
{
public abstract class Duck
{
public IFlyBehavior FlyBehavior { get; set; }
public IQuackBehavior QuackBehavior { get; set; }
public abstract string Display();
public string Fly()
{
return FlyBehavior.Fly();
}
public string Quack()
{
return QuackBehavior.Quack();
}
}
}
The "problem" with this, and the thing that is going to make it, in my opinion, esoteric and infrequent in occurrence as opposed to a use-it-daily staple of my tool belt is that you have to have two or more categorized concerns to merit using the pattern as otherwise the problem may simply be solved by inheritance. If we more generically imagine a pattern using Bird in lieu of more specifically Duck, and Ostrich, Emu, Flamingo, and, yes, Duck are types of Bird, we can handle a limited repetition of flight behavior in which Ostrich and Emu can't fly and Flamingo and Duck can by introducing two middlemen in the inheritance chain. FlightlessBird and FlyingBird could inherit from Bird and Ostrich and Emu could inherit from FlightlessBird while Flamingo and Duck inherit from FlyingBird. Problem solved!
namespace FowlApp.Core
{
public interface IFlyBehavior
{
string Fly();
}
}
I see the inheritance chain thing a lot in code I work upon, and not just in my current role, also in my three years of little jobs here and there before my current job. I've seen a variety of things in many Austin environments and I see Eric Evans-flavored call-things-what-they-are (in a ubiquitous language way) simple structures a lot more than the "design patterns." What I do not see is the Strategy solution. What would be a second category in our Bird example that would force this example into the Strategy Pattern shape? Perhaps if the bird is extinct or not? If the prior examples were examples of birds both flightless and flighty while not extinct then perhaps Dodo could be an example of an extinct flightless bird and, I dunno, AmericanBaldEagle could be an example of an extinct bird that could fly? I guess I'm getting ahead of myself a little with that example. At least these are all legitimate inheritances for Bird. Going back to the original Duck example, though, I don't know that IQuackBehavior merits differentiation that merits the Strategy thing. It really seems like a bad example making me wonder what a good example would look like.
namespace FowlApp.Core
{
public interface IQuackBehavior
{
string Quack();
}
}
The problem with this is that all ducks quack and the ones that don't quack which are jammed into this example are teaching us bad uses of inheritance. Here and here are examples from a "motivational calendar" on the SOLID principles and yes, if it looks like a duck, quacks like a duck, but needs batteries, you probably have the wrong abstraction. I don't know if this example is explicitly supposed to poke fun at the silly example in "Head First Design Patterns." It feels like it, huh? A rocket-powered model duck is even given in the book of an example of a duck which cannot quack, yet fly!
namespace FowlApp.Core
{
public class NoFlyBehavior : IFlyBehavior
{
public string Fly()
{
return "I can't fly.";
}
}
}
This SOLID talk by Chris Weldon touched on how it is a Liskov bug to have inheritances overload the save method in a parent so that it did nothing, and the not flying, not quacking duck thing seems to smell the same way. They, the authors of "Head First Design Patterns," really had to club together a bad example to give us an example at all making me wonder what a good example even looks like. The crisscross of managed bags of somewhat common functionality could be thought of in a grid shape like so...
Extinct? | |||
Yes | No | ||
Flying? | Yes | Argentavis | Flamingo and Duck |
No | Dodo | Ostrich and Emu |
...or in a cube shape if there were three such concerns, and so on. It has to make sense though. Something else Chander Dhall mentioned in the talk detailed here, here, and here is that it a bunch of rockstars on a team acting like rockstars may be counterintuitive. A simple solution is probably better than a slick one. The tricks the Strategy and Decorator patterns bring in with inheritance are, well, tricks, and sort of fly in the face of what one might usually think of as inheritance. A non-quacking duck really doesn't make sense.
namespace FowlApp.Core
{
public class NoQuackBehavior : IQuackBehavior
{
public string Quack()
{
return "I can't quack.";
}
}
}
When I started doing .NET in coming from a recent history of PHP, I found I had to learn some OOP like it or not. Even if I jammed everything I could into the code behind for a web form there was still a divide between a button click event and the event for loading a page so I had to understand methods. I think most of us have embraced OOP more and more, but only so much as our surroundings have pushed us to. We've all finally gotten on board with Onion Architecture and writing tests, but in just about everything I've bumped into it stops there. We don't stray wildly out into the crazy stuff because our peers are also not doing it. We do want to keep it somewhat simple so that our code is easy to read, and to that end we keep doing what we've done which is what everyone knows how to handle. If we are to dive deeper we need a good reason to do so and I'm not seeing one in some of the "common patterns."
namespace FowlApp.Core
{
public class RubberDuck : Duck
{
public RubberDuck()
{
FlyBehavior = new NoFlyBehavior();
QuackBehavior = new NoQuackBehavior();
}
public override string Display()
{
return "I look rubbery.";
}
}
}
No comments:
Post a Comment