Let us continue with our discussion on DRY. We discussed the below two issues in our previous tutorials.
In this tutorial, we are going to discuss another issue -- Repeated Logic. First, you should brush up your knowledge about Don't Repeat Yourself (DRY) Design Principle.
Repeated Logic
Consider that we have the below two domain objects.
Material Object
- public class Material {
- public long Id {
- get;
- set;
- }
- public string Description {
- get;
- set;
- }
- }
Order Object
- public class Order {
- public long Id {
- get;
- set;
- }
- }
Let’s say that the IDs are not automatically generated when inserting a new row in the database. Instead, it must be calculated. So, we come up with the following function to construct an ID which is probably unique.
- private long GenerateId() {
- TimeSpan ts = DateTime.UtcNow - (new DateTime(1960, 1, 1, 0, 0, 0));
- long id = Convert.ToInt64(ts.TotalMilliseconds);
- return id;
- }
You may include this type of logic in both domain objects.
Update the Material Object as below.
- public class Material {
- public long Id {
- get;
- set;
- }
- public string Description {
- get;
- set;
- }
- Material() {
- Id = GenerateId();
- }
- private long GenerateId() {
- TimeSpan ts = DateTime.UtcNow - (new DateTime(1960, 1, 1, 0, 0, 0));
- long id = Convert.ToInt64(ts.TotalMilliseconds);
- return id;
- }
- }
Update the Order Object as below.
- public class Order {
- public long Id {
- get;
- set;
- }
- Order() {
- Id = GenerateId();
- }
- private long GenerateId() {
- TimeSpan ts = DateTime.UtcNow - (new DateTime(1960, 1, 1, 0, 0, 0));
- long id = Convert.ToInt64(ts.TotalMilliseconds);
- return id;
- }
- }
- This situation may arise if two domain objects have been added to your application with a long time delay and you’ve forgotten about the ID generation solution.
- Also, if you want to keep the ID generation logic independent for each object, then you might continue with this solution thinking that someday the ID generation strategies may be different.
At some point, the rules change and all IDs of type long must be constructed using the GenerateId method. Then you absolutely want to have this logic in one place only. Otherwise if the rule changes, then you probably don’t want to make the same change for every single domain object
A very common solution would be to factor out this logic to a static method.
A Helper class would be added.
- public class IDGenerationHelper {
- public static long GenerateId() {
- TimeSpan ts = DateTime.UtcNow - (new DateTime(1960, 1, 1, 0, 0, 0));
- long id = Convert.ToInt64(ts.TotalMilliseconds);
- return id;
- }
- }
Updated the Material Domain Object as below.
- public class Material {
- public long Id {
- get;
- set;
- }
- public string Description {
- get;
- set;
- }
- Material() {
- Id = IDGenerationHelper.GenerateId();
- }
- }
Update the Order Object as below.
- public class Order {
- public long Id {
- get;
- set;
- }
- Order() {
- Id = IDGenerationHelper.GenerateId();
- }
- }
If you have followed the article's SOLID design principles, then you will know by now that static method indicates tight coupling. Here, there is a hard dependency on Material and Product classes on IDGenerationHelper.
If all objects in our domain must have an Id of type long then we may have let every object be derived from a superclass. A solution to factor out this logic is an abstract class.
The base class would be as below.
- public abstract class DomainBase {
- public long Id {
- get;
- private set;
- }
- public DomainBase() {
- Id = GenerateId();
- }
- private long GenerateId() {
- TimeSpan ts = DateTime.UtcNow - (new DateTime(1960, 1, 1, 0, 0, 0));
- long id = Convert.ToInt64(ts.TotalMilliseconds);
- return id;
- }
- }
Updated Material Object
- public class Material: DomainBase {
- public string Description {
- get;
- set;
- }
- public Material() {}
- }
Updated Order Object
- public class Order: DomainBase {
- public Order() {}
- }
If you construct a new Order or Material class elsewhere then the Id will be assigned by the DomainBase constructor automatically.
If we are not happy with the base class approach then Constructor Injection is another approach that can work. We delegate the Id generation logic to the external class which hides behind an interface:
Interface
- public interface IIdGenerator {
- long GenerateId();
- }
We have the below implementation class,
- public class IdGenerator: IIdGenerator {
- public long GenerateId() {
- TimeSpan ts = DateTime.UtcNow - (new DateTime(1970, 1, 1, 0, 0, 0));
- long id = Convert.ToInt64(ts.TotalMilliseconds);
- return id;
- }
- }
We can inject the interface dependency into Order Objects as below,
- public class Order {
- private IIdGenerator _idGenerator;
- public long Id {
- get;
- private set;
- }
- public Order(IIdGenerator idGenerator) {
- if (idGenerator == null) throw new ArgumentNullException();
- this._idGenerator = idGenerator;
- Id = this._idGenerator.GenerateId();
- }
- }
We can apply the same method into Material Object,
- public class Material {
- private IIdGenerator _idGenerator;
- public string Description {
- get;
- set;
- }
- public long Id {
- get;
- private set;
- }
- public Material(IIdGenerator idGenerator) {
- if (idGenerator == null) throw new ArgumentNullException();
- this._idGenerator = idGenerator;
- Id = this._idGenerator.GenerateId();
- }
- }
You can mix the above two solutions with the following DomainBase Super class.
- public abstract class DomainBase {
- private IIdGenerator _idGenerator;
- public long Id {
- get;
- private set;
- }
- public DomainBase(IIdGenerator idGenerator) {
- if (idGenerator == null) throw new ArgumentNullException();
- this._idGenerator = idGenerator;
- Id = this._idGenerator.GenerateId();
- }
- }
So far, we have discussed some of the possible solutions that we can employ to factor out common logic so that it becomes available for different objects.
Obviously, if this logic occurs only within the same class then just simply create a private method for it.
Next, we will see how can we avoid If statements in software.
- If statements are very important building blocks of an application.
- It would probably be impossible to write any real-life app without them.
- It does not mean they should be used without any limitation.
Consider the following domains,
- public abstract class Shape {}
- public class Triangle: Shape {
- public int Base {
- get;
- set;
- }
- public int Height {
- get;
- set;
- }
- }
- public class Rectangle: Shape {
- public int Width {
- get;
- set;
- }
- public int Height {
- get;
- set;
- }
- }
We can simulate a database mock as below.
- private static IEnumerable < Shape > GetAllShapes() {
- var shapes = new List < Shape > () {
- new Triangle() {
- Base = 7, Height = 3
- },
- new Rectangle() {
- Height = 8, Width = 4
- },
- new Triangle() {
- Base = 10, Height = 5
- },
- new Rectangle() {
- Height = 5, Width = 2
- }
- };
- return shapes;
- }
If we want to calculate the total area of the shapes in the collection, then first approach may look like below,
- public static double CalculateTotalArea(IEnumerable < Shape > shapes) {
- var area = 0.0;
- foreach(Shape shape in shapes) {
- if (shape is Triangle) {
- Triangle triangle = shape as Triangle;
- area += (triangle.Base * triangle.Height) / 2;
- } else if (shape is Rectangle) {
- Rectangle recangle = shape as Rectangle;
- area += recangle.Height * recangle.Width;
- }
- }
- return area;
- }
This is quite a common approach in a software design where our domain objects are plain collections of properties and are void of any self-contained logic. Look at the Triangle and Rectangle classes, they contain no logic whatsoever, they only have properties. They are reduced to the role of data-transfer-objects (DTOs). If you don’t understand at first what’s wrong with the above solution then I would suggest you go through the Liskov Substitution Principle here.
You may ask what this method has to do with DRY at all as we do not seem to repeat anything. Yes, we do, although indirectly. Our initial intention was to create a class hierarchy so that we can work with the abstract class Shape elsewhere. Well, guess what, we’ve failed miserably. In this method, we need to reveal not only the concrete implementation types of Shape but we’re forcing an external class to know about the internals of those concrete types.
Tell-Don’t-Ask (TDA) principle
It basically states that you should not ask object questions about its current state before you ask it to perform something.
The above piece of code is a clear violation of TDA although the lack of logic in the Triangle and Rectangle classes forced us to ask these questions.
The solution – or at least one of the viable solutions – will be to hide this calculation logic behind each concrete Shape class,
- public abstract class Shape {
- public abstract double CalculateArea();
- }
- public class Triangle: Shape {
- public int Base {
- get;
- set;
- }
- public int Height {
- get;
- set;
- }
- public override double CalculateArea() {
- return (Base * Height) / 2;
- }
- }
- public class Rectangle: Shape {
- public int Width {
- get;
- set;
- }
- public int Height {
- get;
- set;
- }
- public override double CalculateArea() {
- return Width * Height;
- }
- }
The updated total Area calculation method looks like below,
- public static double CalculateTotalArea(IEnumerable < Shape > shapes) {
- var area = 0.0;
- foreach(Shape shape in shapes)
- area += shape.CalculateArea();
- return area;
- }
We’ve got rid of the “if” statements, we don’t violate TDA and the logic to calculate the area is hidden behind each concrete type. This allows us even to follow the above mentioned Liskov Substitution Principle.
In the upcoming tutorial, we will discuss the below DRY issue.
- RepeatedExecution Pattern