Understand this one design principle
When you get into the world of high level architectures, the Dependency Inversion Principle (DIP) shows up again and again. As I’ve worked extensively in large codebases at Microsoft, understanding the flow of control is crucial to understand the architectural boundaries (i.e abstractions and concrete). This principle implies that systems dependencies refer only to abstractions—not concretions.
Benefits of Dependency Inversion Principle
- Decoupling: By adhering to DIP, the code becomes more modular, making it easier to maintain and extend.
- Testability: Decoupled code is easier to test, as each module can be tested independently.
- Reusability: Modules become more reusable since they are less dependent on other modules.
To implement the Dependency Inversion Principle, you should focus on creating abstractions (interfaces or abstract classes) and making both high-level and low-level modules depend on them. By doing so, you can avoid direct dependencies between high-level and low-level modules.
Let’s dive into some code.
Example
Say we’re building a game, and currently we’re trying to implement the Jobs and Behaviors for the available characters. We will use a contract to abstract the types of Movements. Our Jobs
shouldn’t depend on a concrete implementation
and instead they should not know the types of movemnets it will be doing until we’ve choose the movement type via a Factory which will
be a dependency for the Jobs
.
1public interface IMover2{3int Move();4}
Let’s apply this to 2 of our Behavior
classes:
1public class Runner : IMover2{3private readonly Random random = new();4private readonly ILogger logger;56public Runner(ILogger logger)7{8this.logger = logger;9}1011public int Move()12{13var steps = random.Next(5, 50);14logger.LogEmphasis($"Running for {steps} steps.");15return steps;16}17}1819public class Walker : IMover20{21private readonly Random random = new();22private readonly ILogger logger;2324public Walker(ILogger logger)25{26this.logger = logger;27}2829public int Move()30{31var steps = random.Next(1, 10);32logger.LogEmphasis($"Walking {steps} steps.");33return steps;34}35}
Here are seperate behaviors that might have a contract of IAttacker
, IHealer
, or ISwordsman
. I will not implement
this, I will leave this up to you.
1public class Attacker2{3private readonly Random random = new();4private readonly ILogger logger;56public Attacker(ILogger logger)7{8this.logger = logger;9}1011public int Attack()12{13var rand = random.Next(5, 50);14logger.LogEmphasis($"Attacking for {rand} hit points!");15return rand;16}17}1819public class Healer20{21private readonly Random random = new();22private readonly ILogger logger;2324public Healer(ILogger logger)25{26this.logger = logger;27}2829public void Cure()30{31var rand = random.Next(1, 50);32logger.LogEmphasis($"Using Cure for {rand} magic points!");33}34}353637public class Swordsman38{39private readonly Random random = new();40private readonly ILogger logger;4142public Swordsman(ILogger logger)43{44this.logger = logger;45}4647public int FastBlade()48{49var rand = random.Next(5, 50);50logger.LogEmphasis($"Using Fast Blade for {rand} hit points.");51return rand;52}53}5455public class Marksman56{57private readonly Random random = new();58private readonly ILogger logger;5960public Marksman(ILogger logger)61{62this.logger = logger;63}6465public int RapidFire()66{67var rand = random.Next(5, 50);68logger.LogEmphasis($"Using Rapid Fire for {rand} hit points!");69return rand;70}71}
Now, here is where the ability of DI comes in and helps. Let’s first apply this principle to Archer
1// Before2public class Archer3{4private readonly Walker walker = new();5private readonly Attacker attacker = new(new ConsoleLogger());6private readonly Marksman marksman = new(new ConsoleLogger());789public void RapidFire() => marksman.RapidFire();10public void Walk() => walker.Walk();11public void Attack() => attacker.Attack();12}1314// After15public class Archer16{17private readonly IMover mover; // Could be a Walker or Runner1819// TODO: Fix these20private readonly Attacker attacker = new(new ConsoleLogger());21private readonly Marksman marksman = new(new ConsoleLogger());2223public Archer(IMover mover)24{25this.mover = mover;26}2728public void RapidFire() => marksman.RapidFire();29public int Move() => mover.Move();30public void Attack() => attacker.Attack();31}
Now apply this to the rest:
1public class Warrior2{3private readonly IMover mover;45// TODO: Fix these6private readonly Attacker attacker = new(new ConsoleLogger());7private readonly Marksman marksman = new(new ConsoleLogger());89public Warrior(IMover mover)10{11this.mover = mover;12}1314public void FastBlade() => swordsman.FastBlade();15public int Move() => mover.Move();16public void Attack() => attacker.Attack();17}1819public class WhiteMage20{21private readonly IMover mover;2223// TODO: Fix this24private readonly Healer healer = new(new ConsoleLogger());2526public WhiteMage(IMover mover)27{28this.mover = mover;29}3031public int Walk() => mover.Move();32public void Cure() => healer.Cure();33}
Let’s then create a simple factory to create our desired movemnet type:
1public static class BehaviorFactory2{3public static IMover CreateMover(string type, ILogger logger)4{5return type switch6{7nameof(Walker) => new Walker(logger),8_ => throw new Exception()9};10}1112// attacker, etc...13}
1var logger = new ConsoleLogger();2IMover mover = BehaviorFactory.CreateMover(nameof(Walker), logger);34var player1 = new Warrior(mover);5logger.LogSuccess($"{nameof(player1)} joins the battle!");6player1.Move();
1player1 joins the battle!2Walking 4 steps.
In summary, we’ve inverted depencies. After appying this design principle, our design should look like this:
Real world applications
How might this be applied in the real world? Well, I’ve used this across various types of scenarios. One example you might find youreself doing this is when creating Facades.
For instance, saw we have a simple API:
1using Microsoft.AspNetCore.Mvc;23namespace Practice.Service.Controllers;45[ApiController]6[Route("api/purchase")]7public class PurchaseFacadeController : ControllerBase8{9// Subsystems10private readonly INotificationService m_notificationService;11private readonly IPaymentService m_paymentService;12private readonly IInventoryService m_inventoryService;1314private readonly ILogger<PurchaseFacadeController> m_logger;1516public PurchaseFacadeController(17INotificationService notificationService,18IPaymentService paymentService,19IInventoryService inventoryService,20ILogger<PurchaseFacadeController> logger)21{22m_notificationService = notificationService;23m_paymentService = paymentService;24m_inventoryService = inventoryService;25m_logger = logger;26}2728[HttpPost]29public IActionResult Post([FromBody] SubmitPaymentDto payment)30{31var inventory = m_inventoryService.Get();3233if (!inventory.Any(item => item == payment.Item)) return BadRequest();3435m_paymentService.Pay(payment.Amount, payment.Item);3637m_notificationService.Send($"Item: {payment.Item} purchase with amount {payment.Amount}");3839m_logger.LogInformation($"Submitted a payment of {payment.Item} with amount {payment.Amount}");4041return Ok();42}43}
I hope this was useful!
x, gio