Thursday, 25 October 2012

The Layer Anti-Pattern


Introduction

In brief this post is about a pattern of design that is often wrongly used - that of structuring a design into layers.  Layers can sometimes be a good design but often are not.  Even the most experienced software designers are prone to misuse layers - I give some examples below.
Anti-Patterns      

I first encountered the idea of anti-patterns when I stumbled on this Wikipedia page:
Anti-patterns

Though this list a muddle of various ideas many of them show some insight and it is worth a read.

Over the years I have encountered numerous design problems that can be traced back to this fundamental problem.  Generally having an understanding of good design, and being aware of a natural tendency to use layers, can help you avoid this anti-pattern.

Design Fundamentals

In my first draft of this post I wrote a summary of how to divide a problem into sub-problems by way of introduction.  This summary became too long so I put it in a separate post - see my previous post.  If you don't want to do that, here is a summary of that summary in the form of a few guidelines:
  • Each module should just do one thing (SRP)
  • Duplicate code is indicative of poor partitioning (DRY)
  • Partition the problem at places that creates simple, well-understood interfaces
  • Try to minimize the number of other modules a module uses (loose coupling)
  • Remove unnecessary information and control from interfaces (information hiding)
  • Separate the parts that are likely to change from the rest of the system (encapsulate what varies)
  • Create flexible interfaces if likely to change and forward/backward compatibility is required
  • Provide separate read and modify interfaces
  • Avoid unreachable code

Layers, Hierarchies and the Brain

One more thing, before we get to the point of this post, is to do with the human brain. The human brain seems to have an affinity for giving related objects an ordering. (Come to think of it, this is not just a human trait as even chickens have a pecking order.)  An ordering of objects is easily visualized using layers.

Not everything can fit into a linear ordering so the slightly more flexible concept of a hierarchy is used for organizing a myriad of things, from the directory structure on your hard disk to the classification of all lifeforms.  Of course layers and hierarchies are closely related.  Layers can be thought of as a linear hierarchy.  And hierarchies are often organized into layers for simplicity.

Although layers may assist our understanding they do not always make sense in the problem domain.  As an example, the taxonomic classification in biology mentioned above is based on 7 layers (kingdom, phylum, class, order, family, genus, species). Despite the enormous effort that has been put into this classification it is recognized that the practical usefulness of it is limited at best.  For example, many lifeforms have evolved so little that there is little point in having that many levels, whereas others have branched so often that seven levels is insufficient.

Even hierarchies are not always the best way to represent information.  This is why hierarchical databases are no longer used, having been usurped by relational databases. Though relational databases are in some ways more difficult to understand, they have great benefits in their ability to split data into tables and relate them to each other.  In fact they afford many of the advantages that we are seeking in the design of software, such a eliminating duplication (DRY), SRP, etc.

Use Of Layers

Danish Approach

I remember an old TV ad promoting some sort of Danish pastry with the catchphrase "layer upon layer upon layer" (said in a supposedly Danish accent).  This Danish approach may be good for creating delicate pastry but delicate software is a bad idea. Of course, we typically don't stretch and fold the code to make layers so how do they form?

The most common way that layers form is through a tendency to move subservient code into a lower layer.  It is a great idea to divide code into different modules but doing it this way is often too simplistic.

As we saw above the proclivity to use layers is related to how we think and also to how we are taught to write code.  To most programmers it is obvious that when a function gets too large, you try to extract a portion of that function and push it down into a sub-function.  Most of my university training was in Pascal and this practice was heavily promoted by most, if not all, of my tutors and lecturers.  (It is especially easy to do this in Pascal as a nested subroutine can access the local variables of its parent).

This is not always the wrong approach but often there are better ways to sub-divide a problem than by simply pushing the details into lower and lower layers.

Wrappers

Another way that layers are used is by adding a "wrapper" around an existing module.  Again this can be useful but is often misused.  First we will look at when wrappers should be used then consider how they are misused.

By far the best reason to add a wrapper is to simplify an interface.  By hiding irrelevant parts of the interface you aid decoupling and assist understanding of how the module is used.

A wrapper may also be used to restrict use of a module in some way.  For example you might need a read-only interface to a container class (see dual interfaces discussion in my previous post).  Doing this is really just a special case of the above (ie, creating a simplified interface) but is not done to assist understanding but to enforce security.

The Adapter pattern (see Adapter Pattern) is used when you need a different interface to a module and have no control over the module being wrapped.  For example, an interface may be precisely specified so that different modules can be used interchangeably (see Strategy Pattern) -- if a third-party module needs to conform to the interface then you need to create a wrapper to do this.

I can think of two ways in which wrappers are misused.  There are "do-nothing" wrappers that have no real purpose.  There are also wrappers that add features that are not directly related to the module being wrapped and hence violate SRP.

With regard to "do-nothing" wrappers, I should first explain that wrappers that simply add a layer of indirection can still be useful (see Adapter pattern above).  However, when the reason for using them is not valid then they just add complexity (and inefficiency).

As an example, I recently encountered a wrapper for LibXML which was justified on the grounds of decoupling and good programming practice.  However, it did not provide a simplified interface any better than the cut-down interfaces already shipped with LibXML.  It was also not an appropriate use of the Adapter or Strategy pattern since the library was efficient (and open source) and was never going to be replaced.

LibXML is an open source library for working with XML files.  It is highly recommended as it is efficient and comprehensive.  It also provides interfaces for simplified use.
Further, the wrapper by its design duplicated some error handling which affected the performance of the system.  It also added data conversion facilities which is a bad idea too (see below).  Both of these are violations of the DRY principle.

Even worse are wrappers that add features not directly related to the module being wrapped.  It is not uncommon for a wrapper to perform data conversion operations. Often a better design can be found by moving the operations into a separate module which is independent of the called module.  There is also a tendency for code unrelated to the module being wrapped to creep in, which is definitely a bad idea.

So before creating a wrapper, ensure there is a point to doing so.  If you just want to create a wrapper to provide extra features then try to find a design where the extra features are encapsulated in a different module instead.

Inheritance

One special case I feel compelled to mention is the misuse of inheritance in object-oriented languages.  To be honest I have never liked inheritance since the late 1980's when I read the first edition of Stroustrup's "The C++ Programming Language" (and even before that when I read about Smalltalk in an edition of Byte magazine in 1981).

Inheritance has very little applicability to real world software.  It does have its uses, as demonstrated by hundreds of elegant examples in various textbooks, but I find that it is rare to want to model an "is-a" relationship in real-world software.

Now I am not talking about inheritance when applied to derivation from an abstract base class (in C++) -- this is essentially implementing an interface and is useful for implementing polymorphism which is extremely important in many design patterns.  I am talking about inheriting from a working (or at least partially implemented) base class, which is essentially the same as adding another layer (the derived class) on top of an existing module (the base class).

Of course, misuse of inheritance has been discussed at length for more than 20 years (even Stroustrup in the 2nd edition of "The C++ Programming Language" [1991] says that "inheritance can be misused and overused") so I won't go into that, except to say that it is probably the most egregious thing since goto.  It has also affected the design of languages (examples: multiple-inheritance is not permitted in Java and C#, the addition of final (Java) and sealed (C#) keywords, etc).

However, even what is considered acceptable use of inheritance can often be avoided with consequent improvement to the design.  One example would be the use of the Decorator Pattern.

Examples

There are many, many examples of large projects falling victim to the Layer Anti-Pattern.  Many operating systems have a layered architecture that can create problems and inefficiencies.  For example, Windows NT (upon which Windows 2000, and all later versions of Windows are built) originally had a strict layering but this caused problems (in particular for graphics performance) and the layering was later subverted so that the operating system could adequately run games and other graphically intense software.

Here are two well-documented examples, which you can also read about elsewhere.  I also give a C code example in the next section.

Example 1: OSI

Soon after TCP/IP was invented another networking protocol was invented in an attempt to do it better.  This was called OSI (open systems interconnect) and was made a standard by the ISO (international standards organisation.)  Some computer system manufacturers spent many, many millions of dollars in the 1980's developing and marketing it.

The problems with OSI and the reasons for its failure have been discussed extensively.  I believe the fundamental problem was in its layered design.  OSI used a 7-layer model (as opposed to TCP/IP's 3 or 4 layers) which made it complicated to understand and difficult to implement.  It also made it inefficient - for example each layer had its own error-handling which meant that there was a great deal of overhead and redundancy.

OSI shows how even experienced designers are subject to the layer anti-pattern.  For more on this see the section Layering Considered Harmful in the document at http://www.ietf.org/rfc/rfc3439.txt.

Example 2: Linux Scsi Midlayer Mistake

A device driver is software that essentially becomes part of the operating system in order to interact with a piece of hardware.  In the Linux Kernel, SCSI devices are handled specially as there are a large number of devices that support the SCSI standard.  SCSI is handled using two layers: the high-level layer that does SCSI stuff, and low-level drivers for specific hardware.  This is an example of good layering since it nicely separates concerns.

However, with this design it was soon noticed that a lot of the low-level drivers were using the same or similar code.  In an attempt to remove this redundancy (remember DRY) shared code was moved upwards to become a midlayer.  After awhile it was recognized that this was the wrong approach and became known as the midlayer mistake.  A better approach was found that moved the shared code into a library that any of the low-level drivers could link to.  For more on this see Linux kernel design patterns - part 3.

Avoiding Layers

So how do you avoid this anti-pattern?  First understand and be aware of it, then consider design alternatives.  Knowledge of, and experience with, different design patterns is useful.

Waterfall Methodology and Layers       

The waterfall development methodology, though not an approach to software design, can also be seen as an example of the layers anti-pattern.  The phases in the waterfall model are really layers: the design layer is built on the analysis layer; the coding layer builds on the design layer; etc.

Agile methodologies break the layering effect.  A great benefit is that they avoid the problems caused by dividing a project into phases.
One thing to remember is a good design often consists of many small interconnected modules.  The layered approach starts with small low-level modules but tends to build larger and larger modules on top of them.

Code Example

To clarify all this I will give a real world example.  I was going to add a C code example here, but there is a good example in my recent July post on Dependency Injection.  I include the diagrams again here, and a brief explanation.  For the example code, and more details, see the post at Dependency Injection.

The design was taken from a real application that used the standard C library function qsort() to sort an array of file names for display to the user.

Diagram 1.

The problem with this design was that the main display function (which obtained, sorted and displayed a list of files) was a too large at almost 100 lines of code.  It seemed that the sorting of the strings could be separated into a separate module so I tried to push the sorting code into a lower-level SORT module (as I had been trained to do).

Diagram 2. With Layer.

Is this an improvement?  If you think so then you need to re-read this post from the start! This is a classic example of the layer anti-pattern.  The new layer (SORT module) really adds nothing to the design whatsoever and has several problems.

For example, it does not make sense that the SORT module should be dependent on the OPTIONS module.  Thinking even more about the problem, I realized that the SORT module, even though it depends on the code that compares the strings, does not need to know the details of how the comparison is actually performed.

A much better design was found where the OPTIONS module returns a pointer to the current comparison function.  The function pointer is then used by the main DISPLAY module to tell qsort() how to sort the strings.
Diagram 3.  Without Layer.

Note that this solution now avoids the unnecessary layer of code that wraps the qsort() function.  There are many advantages to this design - for example,
A good rule of thumb is that the less modules that need to be modified for a simple change then the better is the design.
if a new sort option is required then only the OPTIONS module needs to change, whereas the design using layers would require at least two modules to change.

Conclusion

When I first started programming I remember that there was a great debate about which approach to design was better: top-down or bottom-up.  Of course, the real problem with this debate is that it emphasized a layered approach.  Do you create the top layer first and use stubs for the lower layers; or do you build up from the bottom layers? Unfortunately the underlying assumption that you have to use layers at all is flawed.

I hope this post has convinced you that using layers is not always, and actually not often, the best approach.

Tuesday, 14 August 2012

Fundamentals of Software Design


As an introduction to my next post I wrote the text below to explain some basic principles of breaking down a program into more easily handled chunks.  Discussion in software design literature, under such names as separation of concerns, information hiding, modularity, encapsulation, etc, is massive.  However, I have never seen a concise summary so I wrote this.  Unfortunately, my summary was not as brief as I hoped so I split it into this separate post.

This post is an introduction to the my next post on the Layer Anti-Pattern.  I believe it is useful in its own right.

Introduction

Going back to my very first blog post you may recall that I discussed the fundamental problem facing software development (complexity) and the most important approach to tackling it (the principle of divide and conquer) - see Software Design Complexity.  Of course, that is just the start of the story.  The real challenge is to find the best way to divide a problem in order to conquer it.  In general, approaches to this are the subject of just about every book you have read or will read about designing software - ie, the best way to split a problem into smaller and smaller sub-problems which are more easily tackled.  Here I distill the essence of commonly described techniques as well as adding a few approaches of my own.

Modules and Interfaces

First I will clarify my nomenclature.  There are many names given to software components designed to handle specific problems and sub-problems.  I am going to use the word module.

Of course, a large module will use sub-modules to handle it own particular sub-problems.  At the highest level a module may be a program or tool (or even a suite of programs) which is part of a larger system.  At the lowest level a module is a function or procedure.  In C, it is common to have clearly defined modules at the source file level.  In OO languages like C++, Java, and C# the most common type of module is a class, which may or may not correspond to a single source file.

Another important idea is what I will call the interface.  This is simply what a module exposes to the outside world.  By definition, all communications between modules take place through interfaces.  In C and C++ the interfaces to most modules are typically specified in individual header files (typically in a .H file with the same name as the corresponding .C file).

Techniques

SRP

One of the simplest, and most obvious, ideas is that a module should do just one thing.  This idea is ubiquitous in the history of software design.  For example, one of the basic design principles of UNIX is that a tool (or any module) should do just one thing (and do it well).  This is what is commonly discussed when people talk about separation of concerns.  More recently the name SRP (single responsibility principle) has become popular.

Although the idea is simple it is not always easy to recognize when a module is doing more than one thing.  For example, many potentially simple routines are often overwhelmed by ancillary concerns, such as data initialization, data retrieval, data conversion, global state management, error handling, logging, etc.  When queried on why the code is structured that way the designer/developer is unaware of how to separate the concerns or even oblivious to the advantages of doing so.

Further, it is often not possible to separate concerns without access to (or understanding of) the necessary technique or technology.  There have many technologies that have been created for this very reason, such as:
  • function pointers: being able to pass around a pointer to code makes many patterns of modularity possible (see my previous posts on IOC and DI)
  • object-oriented: sometimes a function pointer is not enough; OO technologies also allow data and code (methods) to be handled as one
  • exception handling: allows error handling code to be separated from normal flow of control
  • multi-tasking: allows separate processes to run simultaneously with controlled interactions
  • aspect-oriented: allows "cross-cutting" concerns to be isolated from the rest of the code
  • generics: allows concerns to be separated from the rest of the software in a way that does not reduce performance or sacrifice type-safety
Another good example is from my previous post on Dependency Injection.  It is not obvious that comparison can be separated from sorting which might result in the design of Diagram 2.  Once you realize that the sorting algorithm does not need to know the details of how two elements are compared (only that the elements can be given an ordering) a better separation of concerns can be achieved as in Diagram 3.

DRY

Another basic idea is to eliminate duplicate code, and is often known by the acronym DRY (Don't Repeat Yourself).  It is closely related to SRP since if module boundaries are well chosen then the likelihood of duplicate code is reduced.  Duplicate code is not only a maintenance problem it's also indicative of a poor design in general.

Similarly to SRP there are two problems with DRY: recognizing that two or more pieces of code have commonality; and isolating that commonality.

It can be hard, just inspecting two different pieces of code, to detect any commonality.  For example, the code to sort an array of strings may bear no resemblance to code for sorting a linked list of structures.  The original designer should be aware that both pieces of code are performing a sorting operation and attempt to isolate it into a separate module (or re-use an existing sort module).

Of course, sorting is a fairly obvious operation, but a designer may have much more difficulty finding and separating other pieces of commonality.  Generally, this takes practice and a little lateral thinking but mainly depends on the ability of the designer.

A common pattern when duplicate code is found is to isolate it into a separate module.

Duplicate Code Moved to a New module.

Information Hiding

An important part of the design of modules is the idea that implementation details should be hidden from the outside.  This means designing an interface that does not expose internal details that may need to change.  It also means hiding private methods and data from outside use.
What is Information Hiding?
Some authors consider information hiding to be the principle behind good module design.  I use the term simply to mean hiding details of a module's implementation from the outside world.

A common problem is that an interface will expose details of the module's implementation.  In C++ this is often due to using public data members.  For example, consider a 2-D rectangle class that exposes four numbers: bottom, left, top and right; which are effectively the the coordinates of two corners of the rectangle.  If, for some reason, the implementation needs to be changed to use the bottom, left corner plus height and width it is not possible without changing the interface.  Further, a rectangle like this can only have sides parallel to the axes - what if you wanted to add the ability to rotate?

Another problem is when internal parts of the module are accidentally left visible.  For example, in C it is not unusual for functions internal to a module to be globally visible.  This can cause maintenance problems, such as name conflicts or accidental use of similarly named functions.  Anything that is not hidden effectively becomes part of the interface, since it could potentially be used from another module.  Module-private functions in C should always be declared static to avoid their name being globally visible, in the same way as functions internal to a class in C++ should be declared private.

It should be obvious that using a simple, well-defined interface (that only exposes information on what the module does not how it does it) has many advantages.  It enhances maintainability and portability.  Possibly more importantly, it improves understandability of what the module does, which in turn helps you to understand interactions between modules and verify that the overall design is consistent.

Decoupling

Another piece of software design jargon is coupling which is simply how much modules depend on each other.  The aim is to remove dependencies as much as possible.  This partly depends on information hiding but also relates to how modules themselves are inter-connected.  Decoupling has large benefits for the maintainability of software.  Loosely coupled modules allows changes or improvements in one module to be made easily without affecting (or introducing the possibility of inadvertently affecting) other parts of the code.

If two modules are tightly coupled then there is a lot of interaction between them, which if taken to extremes means they effectively become one module.  When most or all modules are tightly coupled you end up with the Ball of Mud anti-pattern.

For effective decoupling, module interfaces should be as simple as possible, visible and well-defined (see information hiding above).  One indicator of poor decoupling is when a minor change to a system requires changes in multiple places.

However, decoupling goes further than just having good interfaces.  Suppose, we have six well-designed modules that each perform a single function with simple, well-understood interfaces.  If each module depends on every other module then there is a high degree of coupling between the modules.

Tightly Coupled Modules.

A good design will try to organize the modules into a hierarchy so that each module is only dependent on a few other modules and there are no circular dependencies.  A hierarchy make the interactions easier to comprehend.  A graph of the dependencies will be a tree or DAG (directed acyclic graph).  Here is an example:

Loosely Coupled Modules

Encapsulate what Varies

A major purpose of dividing software into modules is to enhance its maintainability.  The are other advantages such as understandability, re-usability, verifiability, etc, but in my opinion maintainability is the most important quality attribute of most software - yes even more important than correctness.  (See my blog Developer Quality Attributes.)
Software Designer's Jargon
Encapsulation, modularity, information hiding, reducing coupling, increasing cohesion, etc are all terms that are used in software design literature to describe the same idea of decomposing a design into modules and reducing the coupling between those modules.  Different people have slightly different interpretations of these terms but the important thing is to understand the ideas, not to be too fussed about the jargon.

Hence it makes sense that, when splitting a design into modules, you should try to isolate the parts of the system that are likely to change.  This is given the software design catchphrase encapsulate what varies.

Flexible Interfaces

Having well-defined interfaces means that modules can be changed more easily, but often enhancements are required that mean the actual interface to a module needs to change.  Just having a simple, well-defined interface is not enough -- it's also important to have flexible interfaces that support forwards (and even backward) compatibility.

I think an example (in C/C++) is in order, to clarify this.  Imagine we have module A that enquires of module B the price of a stock using a function call like this:

     /* moduleB.h */
     extern long moduleb.get_price(const char * stock_code);

     /* moduleA.c */
     price = moduleb.get_price("GOOG");    // get Google's stock price

Now imagine this needs to be enhanced to allow the price of the stock to be obtained at any time in the past.  In C++ we can add a defaulted time parameter like this:

     /* moduleB.h */
     extern long moduleb.get_price(const char * stock_code, time_t time = (time_t)-1);

     /* moduleB.cpp */
     long get_price(const char * stock_code, time_t time)
     {
          if (time == (time_t)-1)
               time = time(NULL);     // default parameter value uses current time
          ...
 
     /* moduleA.cpp */
     price = moduleb.get_price("GOOG");
     ...
     price = moduleb.get_price("MSFT", open_time);

This allows the old module A to work with the new module B without any code changes.  This facility (of C++) is useful but module A stills needs to be rebuilt because the defaulted parameter is added by the compiler at compile-time.

If you try to use the old module A with the new Module B or the old module B with the new module A you will get a link error (for statically linked libraries) or a run-time error if using dynamic libraries.

A more flexible interface would allow optional parameters to be passed at run-time.  This is often accomplished using a text-based interface.

     /* moduleB.h */
     extern long moduleb.get_price(const char * params);

     /* moduleA.c */
     price = moduleb.get_price("code=GOOG");

Now if we enhance module B to accept a time then module A can be enhanced like this:

     /* moduleA.c */
     price = moduleb.get_price("code=GOOG, time=10:00");

If the old module A calls the new module B it behaves exactly as before, for backward compatibility.  That is, if the time parameter is missing the new module B should use the current time.

Also if the new module A calls the old module B then it can also behave sensibly, if it has been designed to be forward compatible.  That is, the original module B should ignore anything it does not understand, such as the time parameter.  Of course, this means that the current stock price will be returned instead of the price at 10:00, but this may be preferable to a run-time error.

Of course, the disadvantage of the above system is that you need extra code to create and parse the text strings.  This is one reason that XML is very popular for decoupling modules.  The main advantage of XML is that the parsing and validation can be done for you by using a DTD or schema.  There are other advantages to using XML such as the plethora of code and tools available and the fact that there are standardised, culture-neutral formats for numbers, dates, etc.

Dual Interfaces

Generally modules are written to only provide one interface to the outside world.
The inspiration for this idea came from the use of the "const" keyword in C+ and C, and const iterators in STL.  It allows you to pass a pointer (or reference) to a function and be sure that the function does not modify the object pointed to.
One technique that I have found useful is to provide two interfaces: one interface is read-only (ie is just used for enquiry) and a separate interface is provided that allows making changes.  When understanding the overall design of a system it is often very useful to know that one module does not affect another even though it may use it.

Using two interfaces in this way can help you to understand the design.  (Having more than two interfaces may indicate that you module violates SRP.)  For example, it is not uncommon to find a container passed to a method and not be sure that the method does not modify the contents of the container.

As a more complete example, consider a simple spreadsheet program that I once implemented using MFC.  MFC use a Document-View architecture which is an example of the Observer Pattern and a simplification of MVC (where the MVC model is called the Document in MFC and the MVC View and Controller are combined into the MFC View class).  Note that this is a good example of decoupling as the model need know nothing of its views - to update the views it simply broadcasts a message to which all attached views subscribe.

In MFC the model or Document is essentially a 2-d array that stores the data of all the cells of the spreadsheet.  The model class provides many methods for modifying the data such as changing the contents of cells, deleting rows or columns etc.  There are also methods that just retrieve information, such as cell contents.

In this design you can have multiple views connected to the same model.  For example, you could have two table views in split windows that show different parts of the model in the normal tabular format of a spreadsheet.  You could also have a other types of view such as a graph view which shows the spreadsheet data as a bar or pie graph.  This is shown in the following UML class diagram.

UML Diagram showing Model-View Associations.

The problem with this diagram is that the table and graph views appear equivalent.  In reality, the table view can make changes to the model, such as editing the contents of a cell, but the graph view only reads the data.  Unfortunately, in a UML class diagram such an association is represented by a dotted arrow and there is no way to differentiate between an association that modifies an object from one that only retrieves information.

Eliminate Unused Code

Unreachable code is code that can never be executed when the software runs.  It is similar to redundant code in that it can be indicative of a poor design -- though it is can be just an oversight.  To find such code a code coverage tool should be used to make sure your tests exercise all the code.  If code is not executed then create tests that cover it; if that is not possible remove the code.

One way I have seen unused code created is through misuse of inheritance.  If derived classes always override a method then there is no point having a base class implementation.  This typically indicates that the classes should be inheriting from an abstract base class (or an interface in C#/Java).

Conclusion

This post has looked at some techniques for deciding how to split a design into modules, how to design interfaces, and how to recognize a design that needs improvement.  Generally, good design requires experience and a readiness to learn new ideas, such as the design patterns discussed in the Design Patterns book that I have mentioned in previous posts.

Most software developers understand the benefits of decomposing a large program into modules.  In reality, the best way to do so is not always obvious and consequently most software still suffers from poor design. In my next post I will look at a common approach which is often used, but is rarely appropriate.  It is perhaps one of the worst design anti-patterns.