In this article series, I talk about the coding practices that I found to be the most beneficial in my experience.
In Part 1, I talked about the most important practice i.e. Automated testing. More specifically, the practice of making sure that there are simple tests written that allow you to verify you didn’t break anything, when you modified your code.
Read more about it in Coding Practices: The most important ones – Part 1.
In this part, I will talk about the
- Separation of data and behavior and
- Immutability (making data objects immutable)
Note: In this article, I give you advice based on my 9+ years of experience working with applications. Although I have worked with many kinds of applications, there are probably kinds that I did not work with. Software development is more of an art than a science. Make sure that the advice I give you here makes sense in your case before using it.
Coding Practices
Practice #2: Separating data and behavior
There should be a clear separation between units of code that model data, and units of code that model behavior.
The following are two C# classes that model two data types:
public sealed class Employee
{
public string Name { get; }
public string Department { get; }
public Employee(string name, string department)
{
Name = name;
Department = department;
}
}
public sealed class City
{
public string Name { get; }
public string State { get; }
public City(string name, string state)
{
Name = name;
State = state;
}
}
..and the following are C# static methods that model behavior:
public static Employee GetMostActiveEmployee(
City city,
Func<City, IEnumerable<Employee>> getEmployeesInCity,
Func<Employee, int> getNumberOfClosedDeals)
{
return getEmployeesInCity(city)
.OrderByDescending(getNumberOfClosedDeals)
.First();
}
public static IEnumerable<Employee> GetEmployeesInCity(
DatabaseContext dbContext,
City city)
{
//Return data from database
}
public static int GetNumberOfClosedDeals(
DatabaseContext dbContext,
Employee employee)
{
//Return data from database
}
The GetMostActiveEmployee method gets the employee with the most closed deals in a particular city. It has two dependencies: getEmployeesInCity and getNumberOfClosedDeals.
The GetEmployeesInCity and GetNumberOfClosedDeals methods can be used for such dependencies. These two methods return data from the database using Entity Framework.
In C#, you can create a whole application from just static methods and data types. You can even do Dependency Injection with static methods. The three methods above can be composed like this:
static void Main(string[] args)
{
using (var context = new DatabaseContext())
{
var mostActiveEmployee = EmployeeModule.GetMostActiveEmployee(
city: new City("Paris", "France"),
getEmployeesInCity: city => EmployeeModule.GetEmployeesInCity(
context, city),
getNumberOfClosedDeals: employee => EmployeeModule.GetNumberOfClosedDeals(
context, employee));
}
}
However, this is not how most developers write code in C#. Nor am I necessarily telling you to write your code this way.
I still ask you to separate data from behavior like I showed, but if you want to, you can do it with classes instead of static methods.
Here is how the three units of behavior would look like:
public class MostActiveEmployeeGetter : IMostActiveEmployeeGetter
{
private readonly IEmployeesInCityGetter employeesInCityGetter;
private readonly INumberOfClosedDealsGetter numberOfClosedDealsGetter;
public MostActiveEmployeeGetter(
IEmployeesInCityGetter employeesInCityGetter,
INumberOfClosedDealsGetter numberOfClosedDealsGetter)
{
this.employeesInCityGetter = employeesInCityGetter;
this.numberOfClosedDealsGetter = numberOfClosedDealsGetter;
}
public Employee GetMostActiveEmployee(City city)
{
return employeesInCityGetter.GetEmployeesInCity(city)
.OrderByDescending(numberOfClosedDealsGetter.GetNumberOfClosedDeals)
.First();
}
}
public class EmployeesInCityGetter : IEmployeesInCityGetter
{
private readonly DatabaseContext dbContext;
public EmployeesInCityGetter(DatabaseContext dbContext)
{
this.dbContext = dbContext;
}
public IEnumerable<Employee> GetEmployeesInCity(City city)
{
//return data from database
}
}
public class NumberOfClosedDealsGetter : INumberOfClosedDealsGetter
{
private readonly DatabaseContext dbContext;
public NumberOfClosedDealsGetter(DatabaseContext dbContext)
{
this.dbContext = dbContext;
}
public int GetNumberOfClosedDeals(
Employee employee)
{
//return data from database
}
}
And here is how you would compose them:
static void Main(string[] args)
{
using (var context = new DatabaseContext())
{
var mostActiveEmployeeGetter = new MostActiveEmployeeGetter(
new EmployeesInCityGetter(context),
new NumberOfClosedDealsGetter(context));
var mostActiveEmployee = mostActiveEmployeeGetter
.GetMostActiveEmployee(new City("Paris", "France"));
}
}
From the perspective of separating data from behavior, the two ways of programming are identical.
Before going any further, I want to show an example of how to not separate data and behavior.
public class Document
{
private readonly string identifier;
private readonly string content;
//Dependencies and constructor here
public void Print()
{
//..
}
public Document Translate(Language language)
{
//..
}
public void Archive()
{
//..
}
}
The Document class contains private fields to model document data. It also contains method to model behavior. It is likely that this class has dependencies that will be used inside the Print, Translate, and Archive methods. For example, it could have a translation server url to use when translating the document.
Why am I advising you to separate data from behavior?
The simple answer is that separating data from behavior makes modifying the behavior of an application easier.
In the Data and Encapsulation in complex C# applications article, I went into details of what happens when we try to extend the behavior, without separating data and behavior. Check that article to see how hard it is to modify application behavior if we insist on keeping data encapsulated inside objects.
Once I started to separate data and behavior in the applications I maintain, my code became much easier to modify.
Before moving on to the next practice, I want to discuss the following case:
public class Document
{
public string Identifier { get; }
public string Content { get; }
public Document(string identifier, string content)
{
Identifier = identifier;
Content = content;
}
public Document Translate(string translationServerUrl)
{
var translatedContent =
TranslationModule
.TranslateContent(translationServerUrl, this.Content);
return new Document(this.Identifier, translatedContent);
}
}
The Document class in this case has public read-only properties to model data. But it also has a Translate method.
Is there a problem with this code?
There isn’t.
The reason is that although the Translate method exists physically in the Document class, it is separate from it logically. I can move the Translate method into its own class as a static method like this:
public static class DocumentTranslationModule
{
public static Document Translate(this Document document, string translationServerUrl)
{
var translatedContent =
TranslationModule.Translate(
translationServerUrl, document.Content);
return new Document(document.Identifier, translatedContent);
}
}
I was able to do that because the data in the Document class are not encapsulated. That is, any class can read them.
Notice the “this” modifier on the document parameter. This makes the Translate method an extension method. This means that I can call this method as if it was an instance method on the Document class.
Still, even if C# has extension methods, it might be more convenient to put methods in the same class. As long as there is no data encapsulation, you don’t lose the advantage of easier modifiability. Because the data is publicly accessible, you can always create a new Translate method in another class that translates the document in a different way. E.g.:
public static class DocumentTranslationModule
{
public static Document TranslateUsingThirdPartyLibrary(
this Document document,
ThirdPartyLibrarySettings settings)
{
var translatedContent =
ThirdPartLibrary.Translate(
settings, document.Content);
return new Document(document.Identifier, translatedContent);
}
}
Instead of invoking the Translate method on a document instance directly, consumers can take a dependency on Func<Document,Document> to do the translation:
public static class ProcessingModule
{
public static void ProcessDocument(Document document, Func<Document, Document> translate)
{
if (IsNotInEnglish(document))
{
document = translate(document);
}
if (IsLegalDocument(document))
{
SaveToLegalDatabase(document);
}
else if (IsFinancialDocument(document))
{
SaveToFinancialDatabase(document);
}
else
{
SaveToGeneralDatabase(document);
}
}
}
And from the Composition Root, you can always select which Translate method to pass as an argument for the translate parameter.
public static class CompositionRoot
{
public static void Compose1()
{
Action<Document> process = document => ProcessingModule.ProcessDocument(
document,
d => d.TranslateUsingThirdPartyLibrary(new ThirdPartyLibrarySettings()));
}
public static void Compose2()
{
Action<Document> process = document => ProcessingModule.ProcessDocument(
document,
d => d.Translate("http://localhost/translationServer"));
}
}
This is what I mean when I say that modifying applications becomes easier when we separate data from behavior. Had the data been encapsulated as private fields inside the Document class, we could not have changed how we want to translate the document as easily.
Note: In the example above, I use static methods and delegates. The same reasoning applies if we use classes and interfaces.
Once we separate data and behavior, we can talk about the design of data objects and behavior objects (functions) separately.
Practice #3: Make your data objects immutable
Immutable data objects are data objects whose contents cannot be changed after these objects are created. This can be done in C# by having read-only properties (or fields). All the data objects in the code examples I provided in this article are immutable.
When designing data objects to be immutable, not only do you need to make your properties read-only, you also need to make sure that the types of these properties are themselves immutable.
Immutability helps mitigate some of the problems that we introduce when we break data encapsulation and make data public. By making data immutable, we at least prevent outsiders from updating the data. In some sense, immutability is enough encapsulation.
For more information about immutability and its benefits, you can read the Designing Data Objects in C# and F# article.
Conclusion:
This article is about the coding practices that I found to be the most beneficial during my work in software development.
In this part, Part 2, I talk about the #2 and #3 most important practices: separating data and behavior, and making data objects immutable.
If we insist on encapsulating data inside classes as private fields, modifying application behavior becomes a harder thing to do. On the other hand, if we separate data and behavior, this gives us more freedom when we decide to change application behavior. To mitigate the issues that arise when we break data encapsulation, we can make data objects immutable.
In some sense, this preserves some encapsulation. There are of course other benefits to making data objects immutable. You can read about them here.
This article was technically reviewed by Damir Arh.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Yacoub Massad is a software developer and works mainly on Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at
criticalsoftwareblog.com. He is also the creator of DIVEX(
https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a
YouTube channel about Roslyn, the .NET compiler.