Are you a dependency slob?
Tracking in user interface sludge all throughout the polished wood floor of your business logic layer?
I’ve done it.
We rush to meet deadlines, slinging code wild west style, no shame at all in hardcoding object instantiation with the new
keyword, slipping some logging code into our formerly SOLID methods, or forcing dependencies on the clients of our classes by requiring class implementations as parameters.
As long as it works, right?
This article marks the next step in our look at the book Adaptive Code via C#. The goal of Chapter 2: Dependencies and Layering is to show us why we should take more care and consideration when we connect the pieces of our applications together and some ways in which we can begin.
Instead of summarizing each chapter section, piece by piece, I’ve taken 3 useful nuggets of wisdom from the author and put them into a list.
Keep in mind, like most advice in software development, there is rarely a time where you should do something no matter what and in every case. Take each suggestion, roll it around in your mind a bit, and think about when it would make sense and when it wouldn’t.
Don’t cross layer boundaries
The concept of a dependency moves along a spectrum from macro to micro. A web server can depend on a database server, a WinForms app depends on the Windows OS, the components of the app can be made up of a user interface which depends on a business logic layer and a data access layer. This of course continues down to assembly, class, method, and variable dependency chains.
The universe is like, a huge web of dependencies, man…
Now, this tip might seem obvious, but it’s critical and needs to be said. If you (or developers before you) have designed a system made up of different layers, it’s up to you to be diligent about keeping them separate so that you retain the benefit.
What is the benefit, you say?
Let’s use an example.
Let’s say you have an application that has three layers, a UI layer, a business logic layer, and a data access layer, and your UI layer is made up of ASP.NET Razor pages or WebForms controls.
You write an email address format validation method in your business logic layer, and without thinking, you pass the entire WebForms TextBox
control to the method and let the method handle grabbing the actual text. It was convenient for you at the time, but now you’ve linked implementation details (the WebForms platform) to your business logic layer, making it unusable to platforms that don’t support WebForms.
Of course, the better move would be to pass the email address as a plain old string
. By keeping parameter types as basic as possible, you decrease dependencies and increase portability.
Avoid “spaghetti potentials” by putting compassion into your public parameter types
This follows along with the previous tip, take some time when creating public methods to consider their future clients.
In the book, the author uses the example of an API method for changing a user’s password. In the example, an AccountsController
API controller depends on a SecurityService
class for changing the password. The SecurityService
has a ChangePassword
method that takes an entire User
object as a parameter.
public class AccountsController {
...
[HttpPost]
public void ChangePassword(int userID, string newPassword)
{
var userRepository = new UserRepository(); //yuck...
var user = userRepository.GetByID(userID);
this.securityService.ChangePassword(user, newPassword);
}
}
Although that may make logical sense, the issue is that now every client that wants to use that method must know how to create User
objects. Notice how the controller method above had to first get ahold of a repository and get the full user object just to call the ChangePassword
method.
Worse yet, the User
class and the repository likely have their own set of dependencies, so you’ve created what I am calling a spaghetti potential. There is now potential for future developers to thread dependency chains throughout various assemblies if they find themselves rushing to implement a password change feature.
The better move here would be to let the SecurityService
create the User
object itself, and only require clients to pass the integer ID of the user.
public class AccountsController {
...
[HttpPost]
public void ChangePassword(int userID, string newPassword)
{
this.securityService.ChangePassword(userID, newPassword);
}
}
Purify your methods by using Aspects
The term cross-cutting concern is when you’ve got a method that is laced with tasks that may be related but are ultimately separate. The best example of this is logging code.
You’ve got a method that updates a user’s email address, but you’ve also thrown in some audit code to record that the change was made so you can go back and see who did it and when.
The author shows a great example of turning the logging code (as well as the code involved in wrapping a database call in a transaction) into separate aspects with method attribute decorators.
Replacing this:
public void UpdateEmail(int userID, string newEmail)
{
Log.Write("user (ID:{0}) email changed to {1}", userID, newEmail);
using (var transaction = session.BeginTransaction)
{
//code actually updating email.
}
}
With this:
[Log]
[Transactional]
public void UpdateEmail(int userID, string newEmail)
{
//JUST code actually updating email.
}
Another example you may be more familiar with is ActionFilter
attributes in .NET Web API. Separating your model validation into it’s own filter like shown in my API birds-eye view guide keeps code clean and clear.
Onward!
Now that you’ve seen a few ways we can be more discerning and thoughtful as we code, I know that next time you’re behind the keyboard, you’ll slow down and be deliberate about the dependencies you create.
You’ll respect layer boundaries as best you can, so not to pollute differing components. Keeping assemblies and layers loosely coupled gives us the best chance at easy future changes, testability, and reusability.
You’ll consider all current and future clients of your public methods. What parameter types would create the fewest dependencies? Will you be forcing clients to know too much?
And finally, you’ll watch out for cross-cutting concerns, looking for ways to keep your methods as single-focused as possible so that they can be understood (and tested) easily by others.
Top comments (0)