This article was originally published on Medium (Appcent publication).
Hello again after a long break 👋
Large software development is not a one-person job — it is teamwork. Especially if you build applications or components used by large audiences, you need a team. Teams develop within their own areas of responsibility, and that is where team management, tasks, processes and methods come into play. One of the most important criteria is always to create sustainable software: an application that can absorb new development, whose maintainability is never neglected, and that is testable.
Every team aims for planned delivery (shipping to production). Planned delivery is the magic phrase here — the goal is clear: we want to release feature X on date Y.
Examples
Let’s go a little deeper with concrete examples, kept as simple as possible. Say we have a simple login application.
public class UserManagerLoginResponse
{
public bool Status { get; set; }
public string Message { get; set; }
public UserManagerLoginResponse(bool status, string message)
{
Status = status;
Message = message;
}
}
public class UserManager
{
public UserManagerLoginResponse Login(string customerNo, string password)
{
//Business-rule check
//Login successful
var result = new UserManagerLoginResponse(true, "Login successful");
return result;
}
}
Let’s say we have a login application containing the code above. Then our customer shares a user story requesting a new feature:
Users whose credentials are correct must receive an SMS for a second check. A code sent to the phone number registered in the system must be requested from the user on the next screen. After these checks the user is considered successfully logged in.
A pretty simple request, right? 😅 Most likely you would quickly wire up the SMS service and write something like the following.
public class SMSSenderService
{
public bool Send(string number, string message)
{
//Sent to the SMS service
var result = true;
return result;
}
}
public class UserManager
{
public UserManagerLoginResponse Login(string customerNo, string password)
{
//Business-rule check
string phoneNumber = "+90XXXXXXXXXX";
SMSSenderService smsService = new SMSSenderService();
string pin = "1545"; //Stored in cache.
string smsMessage = $"Your verification code: {pin}";
var smsServiceResult = smsService.Send(phoneNumber, smsMessage);
//First step successful
var result = new UserManagerLoginResponse(true, "Please enter the code sent to your phone.");
return result;
}
public UserManagerLoginResponse LoginSecondStep(string customerNo, string pin)
{
//Read from cache, verify the pin.
var result = new UserManagerLoginResponse(true, "Welcome");
return result;
}
}
But while writing this code you inevitably created a dependency. The SMSSenderService object became directly coupled to UserManager. On top of that your code became untestable, because you didn’t use Dependency Injection and instead created a concrete object directly. What if we had written an interface? We might need it later.
Keywords: Interface and Abstract · Key Method: Injection
Teams inevitably create dependencies throughout development. Though they look low-cost, these dependencies can cause big problems in the long run. Untestable code leads to serious maintenance/change costs later: you can’t write tests for the code you depend on, and you’ll need extra work to debug or mock it.
If we need to access an object, it’s to our benefit to pass it in via Dependency Injection through the constructor whenever possible.
public class UserManager
{
private SMSSenderService _smsSenderService { get; set; }
public UserManager(SMSSenderService smsSenderService)
{
_smsSenderService = smsSenderService;
}
public UserManagerLoginResponse Login(string customerNo, string password)
{
string phoneNumber = "+90XXXXXXXXXX";
string pin = "1545";
string smsMessage = $"Your verification code: {pin}";
var smsServiceResult = _smsSenderService.Send(phoneNumber, smsMessage);
var result = new UserManagerLoginResponse(true, "Please enter the code sent to your phone.");
return result;
}
}
This loosened the service a bit, but we’re still coupled, because SMSSenderService is still a concrete object. Tomorrow, when you switch from SMS provider A to provider B, the change becomes real work. So let’s first turn the service into an interface; then we can integrate services easily when needed and keep the code sustainable.
public interface ISMSSenderService
{
bool Send(string number, string message);
}
public class ASMSSenderService : ISMSSenderService
{
public bool Send(string number, string message)
{
//Sent via the A SMS service
return true;
}
}
public class BSMSSenderService : ISMSSenderService
{
public bool Send(string number, string message)
{
//Sent via the B SMS service
return true;
}
}
public class Program
{
public static void Main()
{
UserManager userManager = new UserManager(new BSMSSenderService());
}
}
public class UserManager
{
private ISMSSenderService _smsSenderService { get; set; }
public UserManager(ISMSSenderService smsSenderService)
{
_smsSenderService = smsSenderService;
}
}
Did you notice? Inside UserManager I can now use whichever SMS service I want. All I have to do is implement the new service from ISMSSenderService. This way I can even write a unit test with a mock object 😁 That’s how we established a loose-coupling structure.
Let’s make one more request and reinforce the example:
In addition to the SMS messages sent, the same message must also be delivered via push notification to active devices registered to the same phone number.
Same message to the same number… wait, can we do this with an interface? We can.
public interface IMessageSenderService
{
bool Send(string number, string message);
}
public class ASMSSenderService : IMessageSenderService
{
public bool Send(string number, string message) => true; // A SMS
}
public class BSMSSenderService : IMessageSenderService
{
public bool Send(string number, string message) => true; // B SMS
}
public class NotificationSenderService : IMessageSenderService
{
public bool Send(string number, string message) => true; // Notification
}
public class Program
{
public static void Main()
{
List<IMessageSenderService> messageSenders = new List<IMessageSenderService>()
{
new BSMSSenderService(),
new NotificationSenderService()
};
UserManager userManager = new UserManager(messageSenders);
}
}
public class UserManager
{
private List<IMessageSenderService> _messageSenderService { get; set; }
public UserManager(List<IMessageSenderService> messageSenderService)
{
_messageSenderService = messageSenderService;
}
public UserManagerLoginResponse Login(string customerNo, string password)
{
string phoneNumber = "+90XXXXXXXXXX";
string pin = "1545";
string smsMessage = $"Your verification code: {pin}";
foreach (var sender in _messageSenderService)
{
var serviceResult = sender.Send(phoneNumber, smsMessage);
}
return new UserManagerLoginResponse(true, "Please enter the code sent to your phone.");
}
}
Lessons to Take Away
- Even the simplest-looking coupled code can cost a lot of effort in the long run.
- The concepts of Interface and Abstract, and why they matter.
- The Dependency Injection method and why it matters.
- The benefits of creating loose coupling.
Stay well.