Click here to Skip to main content
15,867,756 members
Articles / Web Development / ASP.NET

Let your code speak for itself

Rate me:
Please Sign up or sign in to vote.
4.73/5 (120 votes)
27 Oct 2014CPOL10 min read 139.9K   474   139   113
How to combine a bunch of techniques to improve code readability.

Table of Contents

Introduction

Surely there are a lot of best practices, patterns and advices like 'name your variables proper', 'keep your methods short', 'don't repeat yourself' and so on. These practices are more or less called 'clean code'. But even with all this advises there is something you can not get rid of: the noise coming from your programming language. If you use a general-purpose language like C# than you have to deal with the limited vocabulary of it. So, without some effort you will always have to read the noise in order to understand what the code does. And even more, you will have to reengineer parts of the code to come across its intention. Just have a look at a small example and take a minute to try to get the intention of this piece of code:

C#
var now = DateTime.Now;
if (now.Month == 12 && (now.Day >= 1 && now.Day <= 23)) {
	if (MessageBox.Show("Give a discount?", Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) {
		invoice.Amount = Math.Round(invoice.Amount - (invoice.Amount*15/100), 2, MidpointRounding.AwayFromZero);
        MessageBox.Show("The discount was given.", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information);
	}
}

Got it? So, this is a piece of code with proper naming (discount, invoice, amount) - I guess, without it it would have took you a minute longer.

But who can read it? It's you, me and other people with some programming background.

Normally you would describe the intention of this code like that:
If today is between the 1. and 23. December and the user confirms to give a (you can call it 'x-mass-') discount then the amount of the invoice is reduced by 15 percent and the user is informed about this process.

Well, this sounds easy but compared to the code... the code does not look that easy at all. What the heck is going on with all these MessageBoxes with their buttons and icons, a DialogResult? Math is used but you have to figure out what Math.Round(invoice.Amount - (invoice.Amount*15/100), 2, MidpointRounding.AwayFromZero) exactly does and what date is crucial by understanding now.Month == 12 && (now.Day >= 1 && now.Day <= 23). So the code does not really show what our description looks like some sentences above.

Now, to get an idea what this article is about, have a look at the following piece of code and read on if you are interested in how it was achieved:

C#
if (DateTime.Now.IsBetween(1.December(), 23.December())) {
	if (User.Confirms("Give a discount?")) {
		invoice.Amount = invoice.Amount - 15.Percent();
		Inform.User.About("The discount was given.");
	}
}

Who can read it now? I think your grandma could ;)

Some words of advice

This article will show you how to achieve the readable code above. To some people it will look like overengineered. But remember, this is just an example what could be achieved. This article will introduce you into those techniques which leaded to my second example above. As ever, there is not an "all or nothing". Choose your tools wise and cut it down to a practical solution. But with these techniques in mind I am sure you will think about your approach in another way from time to time and using one of these techniques might bring you up with a cleaner solution. Beside of that, all was done by standard C# syntax - no magic, no hacks.

The downloadable project contains all code that is shown in this article. But it is not complete in any way nor tested and it brings you nothing that you can link to your projects ready to use - sorry for that...

All examples are written in C# but I think a lot of the shown techniques are portable to other languages.

Let's dive in

First of all I want to repeat the importance of naming your classes, methods and variables meaningful. It is a good starting point to use names like 'invoice' or 'amount'. It does not cost you anything and it is the easiest way to let your code speak for itself. Did I mentioned the importance of it? Name your types meaningful, name your types meaningful,...

Introducing new types

Well, from a domain driven approach it is a pattern to create types for all your domain specific members - missing them or not. One of its intentions is (what a surprise) to let your code reflect the domain for better understanding. So I think you figured out that my code snippet uses a domain type as well: the 'invoice' and I am sure that you use this approach, too - for typical scenarios. But we could use this approach much more often. Obviously I introduced two more types: an User and a class named Inform. But under the hood there are some more types which make the code looking so smooth. But you didn't recognize them, did you? There is a Day, an Amount and a Percentage - and that's not the full list of new types...

OK, now I can read your mind: "WTF - that's soooo overengineered".

Let me explain: you feel comfortable with the types the .net framework offers and you use what is there out of the box - even if it does not really fit on your solution!

  • E.g., we are interested in a particular day - without the year. But there is only a DateTime and it causes noise if we have to use it for our approach.
  • We are interested in an amount always being rounded to two decimals. But there is only a Decimal and it causes noise if we have to use it for our approach.
  • We are interested in some percentage calculation. But there is only a Decimal and it causes noise if we have to use it for our approach.

I think, you got it. And have a look at one of these types - on its own it is far away from being overengineered:

C#
public struct Day {
	public int Month { get; private set; }
	public int DayOfMonth { get; private set; }

	public Day(int month, int dayOfMonth)
		: this() {
		Month = month;
		DayOfMonth = dayOfMonth;
	}
}

These types are called Value Types and introducing them into your code opens the door to creating a fluent API by using Extension Methods and Operator Overloading.
(for Value Types you can have a look at one of my other articles) ;)

Keep in mind that it is not always necessary to use these types directly in your code. They could be used as a connector to other types or results as shown further on. Just make sure that these types reflect your intention - not more!

Extension Methods

Extension Methods are around for a long time now. So I think that you have heard of them. In short - you can extend any member with your own methods and add functionality. It is a great way for creating domain specific languages which are readable much more fluently. So, you could also have heard about Fluent Interfaces.

In my code you will find the IntExtension which returns a Day by calling 1.December() for example:

C#
public static Day December(this int dayOfMonth) {
	return new Day(12, dayOfMonth);
}

In the code above these Days are delegated to a DateTimeExtension which returns true if the given DateTime is between the given days (well, you see the code speaks for itself...):

C#
public static bool IsBetween(this DateTime value, Day from, Day to) {
	// Compare a dummy leap year instead.
	var comparableValue = new DateTime(2000, value.Month, value.Day);
	var comparableFrom = new DateTime(2000, from.Month, from.DayOfMonth);
	var comparableTo = new DateTime(2000, to.Month, to.DayOfMonth);
	return comparableFrom <= comparableValue && comparableTo >= comparableValue;
}

All in all this lets you write:

if (DateTime.Now.IsBetween(1.December(), 23.December())) {...}

Nice, isn't it?

By the way, I feel much more comfortable by having my operations near the instance of the relevant type. So instead of using the .net framework method String.IsNullOrEmpty(customerName) I got used to write my own String extension used as customerName.IsNullOrEmpty(). With extension methods you stay tuned to your instance - not using the general type String any more. I think this is much more readable.

If your method returns a bool name it with 'Is...', 'Has...', 'Can...' and so on. Introducing a new extension method on Collections would let you read if(listOfArticles.HasEntries()) {...} instead of if(listOfArticles.Count > 0) {...} - again your intention is doubtless.

But let's come back to my my leading code example...

Another IntExtension is named Percent which let's you write 15.Percent():

C#
public static Percentage Percent(this int value) {
	return new Percentage(value);
}

This returns an instance of the new type Percentage which looks like that:

C#
public struct Percentage {
	public decimal Value { get; private set; }

	public Percentage(int value) : this() {
		Value = value;
	}

	public Percentage(decimal value) : this() {
		Value = value;
	}
}

With this new type we are ready for the next trick...

Operator Overloading

Did you wonder why we can write invoice.Amount = invoice.Amount - 15.Percent() and everything works fine? Where is our formula gone?

Remember: 15.Percent() returns an instance of Percentage. Introducing this new type we are now able to tell this type how to handle all the common operators like minus (-). That's great (to my mind, obviously)! This is called Operator Overloading and looks like that:

C#
public static decimal operator -(decimal value, Percentage percentage) {
	return value - (value*percentage.Value/100);
}

So here is our formula - never ever confusing you in the rest of your code and the intention of the written code (invoice.Amount - 15.Percent()) is absolutely clear.

Implicit Conversion

I am not going to hold back the third new type I mentioned before - the Amount. For the amount of the invoice I first created the following piece of code:

C#
public struct Amount {
	public decimal Value { get; private set; }

	public Amount(decimal value) : this() {
		Value = value.Rounded(decimals: 2);
	}
}

Again, you can see the advantage of a new type: the rounding is now done in the constructor and so you never have to think about that. It is hidden in your regular code - removing noise and complexity.

If you have made it thoughtful through this article so far you might have recognized that the operator overloading from our Percentage simply returns a Decimal. But this Decimal value is assigned to the Amount property of the invoice - being of type Amout - not Decimal. How is this done?

Easy - there are some more operator overloads - called Implicit Conversion (or again another article of mine - last promotion for today ;)):

C#
public static implicit operator Amount(decimal value)
{
	return new Amount(value);
}

public static implicit operator decimal(Amount value)
{
	return value.Value;
}

They are simply made responsible for the conversion from Decimal to Amount and vice versa. So the compiler knows how to handle your assignment properly and it integrates noiseless into your code.

Static helpers

I think you have done the most tricky parts of this article, now.

All there is left to get rid of from our noisy code from the starting point are those noisy MessageBoxes. This was simply done with some new static classes which let you call:

User.Confirms("Give a discount?")<br />
Inform.User.About("The discount was given.") 

Here is the code:

C#
public static class User {
	public static bool Confirms(string text) {
		return MessageBox.Show(text, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes;
	}
}

public static class Inform {
	public static class User {
		public static void About(string text) {
			MessageBox.Show(text, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information);
		}
	}
}

Easy but effective in reducing noise.

You might worry about getting in conflict with other common classes like 'User'. For that you could layer a class named 'The' on top so you could write The.User.Confirms("Give a discount?"). As a rule of thumb you could name your class as a verb (like Inform) if there is not return value on this method. Verbs are rarely candidates for naming conflicts.

That's it!

Conclusion

As you have seen there are a lot of possibilities for getting noise out of your code. My example might be extreme somehow but it was my intention to show what is possible with some effort - just with tools coming out of the box of C#. My favorite concept is to introduce new types when other types do not represent your needs. This approach was borrowed from the domain driven design (DDD). DDD points out to introduce types for all your domain specific terms. Using that I asked myself why not using this approach for general types, e.g. Day, Percentage or Amount? By introducing them I recognized that it leads to a much more readable code by hiding calculations, using operator overloads and implicit conversions. As a side effect, your code will stay at a single dedicated place (the new type) - tested once, leading to less errors.

Using Extension Methods builds a nice fluent syntax - introduce them if suitable.

Using static helper classes is just freestyle on top - less necessary - but not less readable. Concider them if your code has a lot of noise.

Again, choose wise by knowing your tools. Thank you for reading.

What's next?

  • Encapsulate your own types in an separated DLL and build your own domain 'unspecific' language which could be used in all of you projects.
  • Go on and discuss with me after voting if you find this article useful.

Links

History

  • 27 Oct 2014: thank you all for voting for best "Everything Else" article
  • 9 Sep 2014: link section added
  • 4 Sep 2014: changed introducing and conclusion to clarify intention
  • 2 Sep 2014: fixed some typos

 

License

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


Written By
Software Developer
Germany Germany
--------------------------------
Thank you for voting
Best "Everything Else" Article
of September 2014

Comments and Discussions

 
GeneralRe: My vote of 5 Pin
Thorsten Bruning27-Oct-14 9:50
Thorsten Bruning27-Oct-14 9:50 
QuestionWorth the reading. Pin
_Noctis_23-Oct-14 1:34
professional_Noctis_23-Oct-14 1:34 
AnswerRe: Worth the reading. Pin
Thorsten Bruning23-Oct-14 3:48
Thorsten Bruning23-Oct-14 3:48 
GeneralMy vote of 2 Pin
KarstenK15-Oct-14 21:47
mveKarstenK15-Oct-14 21:47 
GeneralRe: My vote of 2 Pin
Thorsten Bruning15-Oct-14 22:24
Thorsten Bruning15-Oct-14 22:24 
GeneralRe: My vote of 2 Pin
Rafael Nicoletti27-Oct-14 13:43
Rafael Nicoletti27-Oct-14 13:43 
GeneralRe: My vote of 2 Pin
Thorsten Bruning27-Oct-14 21:29
Thorsten Bruning27-Oct-14 21:29 
GeneralRe: My vote of 2 Pin
Louis van Alphen27-Oct-14 21:47
Louis van Alphen27-Oct-14 21:47 
These extra classes are not meaningless. They reduce noise and cognitive load exactly like the OP says. I have been using this approach for some time and it is worth the initial effort.
GeneralRe: My vote of 2 Pin
Thorsten Bruning27-Oct-14 22:44
Thorsten Bruning27-Oct-14 22:44 
QuestionMy Vote of 4 Pin
Ian Shlasko15-Sep-14 11:11
Ian Shlasko15-Sep-14 11:11 
AnswerRe: My Vote of 4 Pin
Thorsten Bruning16-Sep-14 2:09
Thorsten Bruning16-Sep-14 2:09 
GeneralRe: My Vote of 4 Pin
Anurag Gandhi19-Oct-14 23:04
professionalAnurag Gandhi19-Oct-14 23:04 
GeneralRe: My Vote of 4 Pin
Thorsten Bruning20-Oct-14 4:43
Thorsten Bruning20-Oct-14 4:43 
GeneralNice read Pin
Felix Keil8-Sep-14 23:49
Felix Keil8-Sep-14 23:49 
GeneralRe: Nice read Pin
Thorsten Bruning9-Sep-14 0:28
Thorsten Bruning9-Sep-14 0:28 
GeneralMy vote of 4 Pin
VB2tehMax4-Sep-14 10:05
VB2tehMax4-Sep-14 10:05 
GeneralRe: My vote of 4 Pin
Thorsten Bruning4-Sep-14 20:32
Thorsten Bruning4-Sep-14 20:32 
GeneralRe: My vote of 4 Pin
VB2tehMax4-Sep-14 22:28
VB2tehMax4-Sep-14 22:28 
GeneralRe: My vote of 4 Pin
Thorsten Bruning4-Sep-14 22:57
Thorsten Bruning4-Sep-14 22:57 
GeneralMy vote of 5 Pin
Joachim Blank3-Sep-14 20:10
Joachim Blank3-Sep-14 20:10 
GeneralRe: My vote of 5 Pin
Thorsten Bruning3-Sep-14 20:18
Thorsten Bruning3-Sep-14 20:18 
QuestionIntriguing Pin
PeejayAdams3-Sep-14 1:44
PeejayAdams3-Sep-14 1:44 
AnswerRe: Intriguing Pin
Thorsten Bruning3-Sep-14 1:53
Thorsten Bruning3-Sep-14 1:53 
QuestionNot for granny Pin
Stephen Zeng2-Sep-14 15:22
Stephen Zeng2-Sep-14 15:22 
AnswerRe: Not for granny Pin
Thorsten Bruning2-Sep-14 21:03
Thorsten Bruning2-Sep-14 21:03 

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.