Click here to Skip to main content
15,868,080 members
Articles / Programming Languages / C#
Article

How to Avoid Producing Legacy Code at the Speed of Typing

Rate me:
Please Sign up or sign in to vote.
4.89/5 (88 votes)
26 Feb 2019CPOL7 min read 72.8K   80   56
This article provides a recipe on how to avoid producing legacy code at the speed of typing by using a proper architecture and unit testing.

Introduction

As an enterprise software developer, you are in a constant fight against producing legacy code – code that is no longer worthy of maintenance or support. You are constantly struggling to avoid re-writing stuff repeatedly in a faint hope that next time you will get it just right.

The characteristics of legacy code is, among others, poor design and architecture or dependencies on obsolete frameworks or 3rd party components. Here are a few typical examples that you might recognize:

You and your team produced a nice, feature-rich Windows application. Afterwards, you realize that what was really needed was a browser or mobile application. That is when you recognize the tremendous effort it would take to provide an alternative UI to your application, because you have embedded too much domain functionality within the UI itself.

Another scenario might be that you made a backend that is deeply infiltrated in a particular ORM – such as NHibernate or Entity Framework – or highly dependent on a certain RDBMS. At one point, you want to change backend strategy to avoid ORM and use file-based persistence, but then you realize it is practically impossible because your domain functionality and the data layer are tightly coupled.

In both of the above scenarios, you are producing legacy code at the speed of typing.

However, there is still hope. By adapting a few simple techniques and principles, you can change this doomed pattern of yours forever.

The Architectural Evolution

In the following, I will describe 3 phases in a typical architectural evolution for a standard enterprise software developer. Almost any developer will make it to phase 2, but the trick is to make it all the way through phase 3 which will eventually turn you into an architectural Ninja.

An evolution into a Nija architect

Phase 1 - Doing it Wrong

Most developers have heard about layered architecture, so very often the first attempt on an architecture will look something like this – two layers with separated responsibilities for frontend and backend functionality:

2-layered architecture diagram. Frontend-Backend

So far so good, but quite soon, you will realize that it is a tremendous problem that the domain logic of your application is entangled into the platform-dependent frontend and backend.

Phase 2 – A Step Forward

Thus, the next attempt is to introduce a middle layer – a domain layer – comprising the true business functionality of your application:

3-layered diagram. Frontend-Domain-Backend

This architecture looks deceptively well-structured and de-coupled. However, it is not. The problem is the red dependency arrow indicating that the domain layer has a hard-wired dependency on the backend – typically, because you are creating instances in the domain layer of backend classes using the new keyword (C# or Java). The domain layer and the backend are tightly coupled. This has numerous disadvantages:

  • The domain layer functionality cannot be reused in isolation in another context. You would have to drag along its dependency, the backend.
  • The domain layer cannot be unit tested in isolation. You would have to involve the dependency, the backend.
  • One backend implementation (using for example, a RDBMS for persistence) cannot easily be replaced by another implementation (using for example file persistence).

All of these disadvantages dramatically reduce the potential lifetime of the domain layer. That is why you are producing legacy code at the speed of typing.

Phase 3 – Doing it Right

What you have to do is actually quite simple. You have to turn the direction of that red dependency arrow around. It is a subtle difference, but one that makes all the difference:

3-layered architecture diagram. Using Dependency Inversion Principle. Frontend-Domain-Backend

This architecture adheres to the Dependency Inversion Principle (DIP) – one of the most important principles of object-oriented design. The point is that once this architecture is established – once the direction of that dependency arrow is turned around – the domain layer dramatically increases its potential lifetime. UI requirements and trends may switch from Windows to browsers or mobile devices, and your preferred persistence mechanism might change from being RDBMS-based to file-based, but now that is all relatively easily exchangeable without modifying the domain layer. Because at this point, the frontend as well as backend is de-coupled from the domain layer. Thus, the domain layer becomes a code library that you theoretically never ever have to replace – at least as long as your business domain and overall programming framework remain unchanged. Now, you are efficiently fighting that legacy code.

On a side note, let me give you one simple example on how to implement DIP in practice:

Maybe you have a product service in the domain layer that can perform CRUD operations on products in a repository defined in the backend. This very often leads to a dependency graph like the one shown below, with the dependency arrow pointing in the wrong direction:

Dependency diagram 1

This is because somewhere in the product service, you will "new" up a dependency to the product repository:

C#
var repository = new ProductRepository();

To inverse the direction of the dependency using DIP, you must introduce an abstraction of the product repository in form of an IProductRepository interface in the domain layer and let the product repository be an implementation of this interface:

Dependency injection diagram 2

Now, instead of "newing" up an instance of the product repository in the product service, then you inject the repository into the service through a constructor argument:

C#
private readonly IProductRepository _repository;
 
public ProductService(IProductRepository repository)
{
    _repository = repository;
}

This is known as dependency injection (DI). I have previously explained this in much more detail in a blog post called Think Business First.

Once you have established the correct overall architecture, the objective of the fight against legacy code should be obvious: move as much functionality as you can into the domain layer. Make those frontend and backend layers shrink and make that domain layer grow fat:

3-layered architecture diagram. Fat domain layer

A very convenient bi-product of this architecture is that it makes it easy to establish unit tests of the domain functionality. Because of the de-coupled nature of the domain layer and the fact that all of its dependencies are represented by abstractions (such as an interface or an abstract base class), it is quite easy to establish fake objects of these abstractions and use them when establishing unit test fixtures. So it is “a walk in the park” to guard the entire domain layer with unit tests. You should strive for nothing less than a 100% unit test coverage – making your domain layer extremely robust and solid as a rock which again will increase the lifetime of the domain layer.

You are probably starting to realize that not only traditional frontends or backends, but all other components – including the unit tests or for example an http-based Web API – should act as consumers of the domain layer. Thus, it makes a lot of sense to depict the architecture as onion layers:

onion layer architecture diagram

The outer layer components consume the domain library code – either by providing concrete implementations of domain abstractions (interfaces or base classes) or as a direct consumer of domain functionality (domain model and services).

However, still remember: the direction of coupling is always toward the center – toward the domain layer.

At this point, it might all seem a bit theoretic and, well…, abstract. Nevertheless, it does not take a lot to do this in practice. In another CodeProject article of mine, I have described and provided some sample code that complies with all of the principles in this article. The sample code is simple, yet very close to real production code.

Summary

Being an enterprise software developer is a constant battle to avoid producing legacy code at the speed of typing. To prevail, do the following:

  • Make sure all those dependency arrows point toward the central and independent domain layer by applying the Dependency Inversion Principle (DIP) and Dependency Injection (DI).
  • Constantly nourish the domain layer by moving as much functionality as possible into it. Make that domain layer grow fat and heavy while shrinking the outer layers.
  • Cover every single functionality of the domain layer by unit tests.

Follow these simple rules and it will all come together. The code that you write will potentially have a dramatically longer lifetime than before because:

  • The domain layer functionality can be reused in many different contexts.
  • The domain layer can be made robust and solid as a rock with a 100% unit test coverage.
  • Implementations of domain layer abstractions (for example, persistence mechanisms) can easily be replaced by alternative implementations.
  • The domain layer is easy to maintain.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Denmark Denmark
I am a software architect/developer/programmer.

I have a rather pragmatic approach towards programming, but I have realized that it takes a lot of discipline to be agile. I try to practice good craftsmanship and making it work.

Comments and Discussions

 
QuestionStep by step Pin
AstroTheDog24-Jun-19 0:05
AstroTheDog24-Jun-19 0:05 
QuestionHow do you inject repository to ProductService? Pin
joreyesm8-May-19 11:22
joreyesm8-May-19 11:22 
AnswerRe: How do you inject repository to ProductService? Pin
L. Michael13-May-19 19:19
L. Michael13-May-19 19:19 
QuestionMy concerns Pin
James Lonero20-Mar-19 5:57
James Lonero20-Mar-19 5:57 
AnswerIn a perfect world Pin
objectvill27-Feb-19 22:13
objectvill27-Feb-19 22:13 
GeneralRe: In a perfect world Pin
L. Michael28-Feb-19 0:08
L. Michael28-Feb-19 0:08 
SuggestionRe: In a perfect world Pin
Lechuss1-Mar-19 5:25
Lechuss1-Mar-19 5:25 
PraiseThank you so much for sharing! Pin
jediYL27-Feb-19 18:27
professionaljediYL27-Feb-19 18:27 
GeneralRe: Thank you so much for sharing! Pin
L. Michael27-Feb-19 23:40
L. Michael27-Feb-19 23:40 
General[My vote of 2] No. Pin
jelamid27-Feb-19 12:01
jelamid27-Feb-19 12:01 
GeneralRe: [My vote of 2] No. Pin
L. Michael28-Feb-19 3:16
L. Michael28-Feb-19 3:16 
GeneralRe: [My vote of 2] No. Pin
jelamid28-Feb-19 4:15
jelamid28-Feb-19 4:15 
QuestionWhat is the meaning of the arrow's direction? Pin
joreyesm27-Feb-19 10:02
joreyesm27-Feb-19 10:02 
AnswerRe: What is the meaning of the arrow's direction? Pin
L. Michael27-Feb-19 20:29
L. Michael27-Feb-19 20:29 
Questionseems like an awful lot of text so say Pin
Sacha Barber27-Feb-19 9:08
Sacha Barber27-Feb-19 9:08 
AnswerRe: seems like an awful lot of text so say Pin
L. Michael28-Feb-19 2:58
L. Michael28-Feb-19 2:58 
QuestionLegacy code is merely code that has survived Pin
Zebedee Mason27-Feb-19 2:56
Zebedee Mason27-Feb-19 2:56 
AnswerRe: Legacy code is merely code that has survived Pin
Jim Bednarek27-Feb-19 8:38
Jim Bednarek27-Feb-19 8:38 
QuestionConfused Pin
Delphi.7.Solutions27-Feb-19 1:50
Delphi.7.Solutions27-Feb-19 1:50 
AnswerRe: Confused Pin
L. Michael27-Feb-19 20:46
L. Michael27-Feb-19 20:46 
QuestionIronically.... Pin
Marc Clifton26-Feb-19 14:52
mvaMarc Clifton26-Feb-19 14:52 
PraiseVoted 5 Pin
KristianEkman26-Feb-19 8:58
KristianEkman26-Feb-19 8:58 
GeneralRe: Voted 5 Pin
PeejayAdams27-Feb-19 2:00
PeejayAdams27-Feb-19 2:00 
GeneralRe: Voted 5 Pin
L. Michael28-Feb-19 0:02
L. Michael28-Feb-19 0:02 
QuestionAnd then the next step Pin
Dean Roddey26-Feb-19 6:10
Dean Roddey26-Feb-19 6:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.