The six anti-patterns I will discuss in this article are Spaghetti Code, Golden Hammer, Boat Anchor, Dead Code, Proliferation of Code and the God Object.
Anti-patterns are certain patterns in software development that are considered bad programming practices. As opposed to design patterns which are common approaches to common problems which have been formalized and are generally considered a good development practice, anti-patterns are the opposite and are undesirable.
1. Spaghetti Code. This is one of the most common types of anti-pattern you will find in software development.
An anti-pattern is the evil twin of a good design pattern. A good design pattern is a reusable requirement, design, or implementation that solves hard problems in a standard way, which is well designed, well documented, easy to maintain, and easy to extend.
The only anti-pattern I've seen is over-monadization, and since monads can be incredibly useful this falls somewhere in between a bad practice and an anti-pattern.
Suppose you have some property P
that you want to be true of some of your objects. You could decorate your objects with a P monad (here in Scala, use paste
in the REPL to get the object and its companion to stick together):
class P[A](val value: A) {
def flatMap[B](f: A => P[B]): P[B] = f(value) // AKA bind, >>=
def map[B](f: A => B) = flatMap(f andThen P.pure) // (to keep `for` happy)
}
object P {
def pure[A](a: A) = new P(a) // AKA unit, return
}
Okay, so far so good; we cheated a little bit by making value
a val
rather than making this a comonad (if that's what we wanted), but we now have a handy wrapper in which we can wrap anything. Now let's suppose we also have properties Q
and R
.
class Q[A](val value: A) {
def flatMap[B](f: A => Q[B]): Q[B] = f(value)
def map[B](f: A => B) = flatMap(f andThen Q.pure)
}
object Q {
def pure[A](a: A) = new Q(a)
}
class R[A](val value: A) {
def flatMap[B](f: A => R[B]): R[B] = f(value)
def map[B](f: A => B) = flatMap(f andThen R.pure)
}
object R {
def pure[A](a: A) = new R(a)
}
So we decorate our object:
class Foo { override def toString = "foo" }
val bippy = R.pure( Q.pure( P.pure( new Foo ) ) )
Now we are suddenly faced with a host of problems. If we have a method that requires property Q
, how do we get to it?
def bar(qf: Q[Foo]) = qf.value.toString + "bar"
Well, clearly bar(bippy)
isn't going to work. There are traverse
or swap
operations that effectively flip monads, so we could, if we'd defined swap
in an appropriate way, do something like
bippy.map(_.swap).map(_.map(bar))
to get our string back (actually, a R[P[String]]
). But we've now committed ourselves to doing something like this for every method that we call.
This is usually the wrong thing to do. When possible, you should use some other abstraction mechanism that is equally safe. For instance, in Scala you could also create marker traits
trait X
trait Y
trait Z
val tweel = new Foo with X with Y with Z
def baz(yf: Foo with Y) = yf.toString + "baz"
baz(tweel)
Whew! So much easier. Now it is very important to point out that not everything is easier. For example, with this method if you start manipulating Foo
you will have to keep track of all the decorators yourself instead of letting the monadic map
/flatMap
do it for you. But very often you don't need to do a bunch of in-kind manipulations, and then the deeply nested monads are an anti-pattern.
(Note: monadic nesting has a stack structure while traits have a set structure; there is no inherent reason why a compiler could not allow set-like monads, but it's not a natural construct for typical formulations of type theory. The anti-pattern is a simple consequence of the fact that deep stacks are tricky to work with. They can be somewhat easier if you implement all the Forth stack operations for your monads (or the standard set of Monad transformers in Haskell).)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With