The sections below are about enabling an application to evolve and be maintained with minimal risks and effort. It is not easy to interpret a lot of complex information derived from the organizational structure of a source code. By separating concerns (link), we minimize complexity. Different responsibilities are maintained in different places. Separation of concerns is about dividing to conquer, about modularity, encapsulation, defining layers, about individual pieces of code that are developed and maintained individually and independently.
Instead of worrying about different sections of the code, we need to focus on localized changes in the right (and expected) places.
Orthogonality
In geometry, Euclidean vectors are orthogonal if they are perpendicular, i.e., form a right angle. Even if these vectors grow infinitely in space, they will never cross. Well designed software are orthogonal. Their components can grow or be modified without affecting other components.
Orthogonal design is built upon two pillars: cohesion and coupling. These concepts form the basis of software design. However, although well known, they are constantly ignored or misunderstood.
Coupling
Coupling (also known as Dependency) is a degree to which one program unit (e.g., a class, module, subsystem) relies on other units. It is a measure of strength of the interconnections between elements, which should be minimized.
We want elements that are independent of each other. In other words, we want to develop applications that exhibit loose (rather than tight) coupling.
However, since parts need to communicate among themselves, we do not have completely independent modules. As interconnections grow between the parties involved, one module will need more information about the other, increasing the dependency between them.
The code below is a sample of content coupling. It occurs when one component depends (by modifying or relying) on internal data or behavior of another component. Changing elementary structure or behavior of one component leads to refactoring of other components.
public class LoggedUsersController
{
public Dictionary<int, DateTime> LastUserLoginDateAndTime { get; set; }
public List Users { get; set; }
}
public class BusinessRule
{
private LoggedUsersController loggedUsers =
new LoggedUsersController();
public User RegisterUserLogin(int userId)
{
User user = getUserFromDatabase(userId);
if (loggedUsers.Users.Exists(u => u.Id == userId))
throw new UserAlreadyExistsException();
loggedUsers.Users.Add(user);
if (!loggedUsers.LastUserLoginDateAndTime.ContainsKey(user.Id))
loggedUsers.LastUserLoginDateAndTime.Add(user.Id,DateTime.Now);
else
loggedUsers.LastUserLoginDateAndTime[user.Id] = DateTime.Now;
return user;
}
}
Since RegisterUserLogin
performs direct access to inner content of LoggedUsersController
, it contributes to a tighter coupling from the caller to the behavior of LoggedUsers
. A better approach is to isolate the behavior inside LoggedUsersController
.
public class LoggedUsersController
{
private Dictionary<int, DateTime> LastUserLoginDateAndTime;
private List Users;
public void AddUser(User user)
{
if (this.Users.Exists(u => u.Id == user.Id))
throw new UserAlreadyExistsException();
this.Users.Add(user);
if (!this.LastUserLoginDateAndTime.ContainsKey(user.Id))
this.LastUserLoginDateAndTime.Add(user.Id, DateTime.Now);
else
this.LastUserLoginDateAndTime[user.Id] = DateTime.Now;
}
}
public class BusinessRule
{
private LoggedUsersController loggedUsers =
new LoggedUsersController();
public User RegisterUserLogin(int userId)
{
User user = getUserFromDatabase(userId);
loggedUsers.AddUser(user);
}
}
Now, the BusinessRule
class is not tied to the implementation of LoggedUsersController
. Instead, it is interested only in the responsibilities of its interface. It does not know details about the implementation of LoggedUsersControllers
anymore, which contributes to a looser coupling. Moreover, all the logic related to the data of LoggedUsersControllers
is handled closer, eliminating the inappropriate intimacy, which increases cohesion of the class.
Types of Coupling
The types are listed in order of the highest to the lowest coupling.
- Content coupling (worst) occurs when one component depends on internal data or behavior of another component. This is the worst degree of coupling, since changes to one component will almost certainly require modification to others.
- Common coupling occurs when modules share common data, like global variables. As we all know, globals are evil. Changing the shared resource implies changing all the modules using it.
- Control coupling occurs when one service or module knows something about the implementation of another and passes information to control its logic.
- Stamp coupling occurs when modules share objects and use only a part of it. Sharing more data than what was needed allows the called module to access more data than it really needs.
- Data coupling occurs when one module or service shares data between each other. Data passed as parameter to a function call is included in this type of coupling. Although services with many parameters are a bad sign of design, well handled data coupling is preferable when compared to other forms of coupling.
- No coupling (best) – No intersection between modules.
If two or more components need to communicate, they should exchange as little information as possible.
Cohesion
Cohesion is a measure of responsibility and focus of an application component. It is the degree to which the elements of a module belong together, which should be maximized.
We want strong-related responsibilities in a single component. Thus, we want to develop highly cohesive code.
In a highly cohesive code, all data, methods and responsibilities are kept close. Services tend to be similar in many aspects.
A simple and intuitive way to test the cohesion of a class is to check if all the data and methods that it contains have a close relationship with the class name. Considering this, you should be aware that generic class names tend to generate cohesion problems, because they can get many different responsibilities over time. In fact, classes that have a vague name might one day become god objects (an anti-pattern that defines an “all-knowing” object that contains tons of features and services with many different purposes, which dramatically compromises cohesion and coupling of components).
The Law of Demeter: Talk Only To Your Closest Friends
Also known as Principle of Least Knowledge or just LoD, the law of Demeter governs the interconnection between components. It reinforces loose coupling and high cohesion by stating that your object-oriented entities should only be talking to their closest friends.
The Law of Demeter states that a method of a given object should only access methods and accessors belonging to:
- The object itself
- Parameters passed in to the method
- Any object created within the method
- Direct component elements of the object
Long chaining of accessors and methods is a sign of bad design.
For example:
public class BusinessRule
{
public Bid GetCarAuctionBestBid(int carId)
{
return bidsLogic.Bids.AllBids.GetBestBid(b => b.CarId = carId);
}
}
Even if you need a particular information that is at the end of the chain, digging out the methods and accessors yourself is a terrible idea.
public class BusinessRule
{
public Bid GetCarAuctionBestBid(int carId)
{
return bidsLogic.GetBestBid(carId);
}
}
Now you are only talking to your closest friend. “bidsLogic
” resolves its properties internally and exposes services that are explicitly needed by others components. There is another thing going on here. The law of Demeter is not just about chaining. When you do not have to worry about navigating accessor and methods (i.e., when you have what you need by just calling a method or property of a nearby object), we say that you are telling the object what to do. The principle of least knowledge is closely related to the principle “Tell, don’t ask”!
Tell, Don’t Ask
The problem is not to use “get” accessor to understand a behavior of an object. The problem is to make decisions based on that. You do not want to ask the object about its inner state, make decisions about that state and then perform some dark operation. Object-oriented programming tells objects what to do.
The sample below is an example of code that asks too much. The code makes decisions about the state of the bill by adding prices contained in the list of Items assuming that the sum represents the total value of the bill.
public class BusinessRule
{
public double CalculateDinnersCost(List<Bill> bills)
{
return bills != null ? bills.SelectMany(b => b.Items).Sum(i => i.Price) : 0;
}
}
public class Bill
{
public Bill()
{
this.Items = new List<Item>();
}
public List<Item> Items { get; private set; }
public void AddItem(Item item) {...}
public void Remove(int itemId) {...}
}
Instead of asking that much, if the state of an object can be inferred by examining closer accessors and methods, we should consider relocating the logic inside the right object. This is the “Tell, don’t ask” principle: instead of developing procedural code, we tell the objects what to do.
public class BusinessRule
{
public double CalculateDinnersCost(List<Bill> bills)
{
return bills != null ? bills.Sum(b => b.CalculateTotalCost()) : 0;
}
}
public class Bill
{
private List<Item> items;
public Bill()
{
this.items = new List<Item>();
}
public void AddItem(Item item) {...}
public void Remove(int itemId) {...}
public double CalculateTotalCost()
{
return this.items.Sum(i => i.Price);
}
}
Single Responsibility Principle
The Single Responsibility Principle is straightforward:
Every class should have a single responsibility and have one, and only one, reason to change.
Imagine an entity called MessagesRegister
, which is responsible for registering alerts and notifications.
This is how MessagesRegister
works:
- It reads a configuration file
- It processes some business logic to create notifications.
This class can be changed for two reasons. First, the source of the configuration can evolve into something more elaborate over time (instead of XML, a configuration through graphical user interface). Furthermore, the actual processing rule might change.
It is a bad design decision to keep together two pieces of information that are changed for different reasons. The cohesion (i.e., focus and responsibility of the class) is reduced and the principle of separation of concerns is violated.
Putting It All Together
So… where should I put this code? While coupling measures the degree of dependence between different entities of the system, cohesion is a measure of how focused the responsibilities of a given entity are.
By following these principles, we can enumerate some major achievements in our design:
- Maintenance is child’s play: we keep together things that need to be maintained together and things that are not directly related can be changed without affecting other components.
- The code is readable and algorithms become more self-documenting.
- Classes have well-defined responsibilities and code duplication is drastically reduced.
Related Readings