Blogs
Understand This Design Principle

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.

IMover.cs
1
public interface IMover
2
{
3
int Move();
4
}

Let’s apply this to 2 of our Behavior classes:

Behaviors
1
public class Runner : IMover
2
{
3
private readonly Random random = new();
4
private readonly ILogger logger;
5
6
public Runner(ILogger logger)
7
{
8
this.logger = logger;
9
}
10
11
public int Move()
12
{
13
var steps = random.Next(5, 50);
14
logger.LogEmphasis($"Running for {steps} steps.");
15
return steps;
16
}
17
}
18
19
public class Walker : IMover
20
{
21
private readonly Random random = new();
22
private readonly ILogger logger;
23
24
public Walker(ILogger logger)
25
{
26
this.logger = logger;
27
}
28
29
public int Move()
30
{
31
var steps = random.Next(1, 10);
32
logger.LogEmphasis($"Walking {steps} steps.");
33
return 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.

Behaviors Folder
1
public class Attacker
2
{
3
private readonly Random random = new();
4
private readonly ILogger logger;
5
6
public Attacker(ILogger logger)
7
{
8
this.logger = logger;
9
}
10
11
public int Attack()
12
{
13
var rand = random.Next(5, 50);
14
logger.LogEmphasis($"Attacking for {rand} hit points!");
15
return rand;
16
}
17
}
18
19
public class Healer
20
{
21
private readonly Random random = new();
22
private readonly ILogger logger;
23
24
public Healer(ILogger logger)
25
{
26
this.logger = logger;
27
}
28
29
public void Cure()
30
{
31
var rand = random.Next(1, 50);
32
logger.LogEmphasis($"Using Cure for {rand} magic points!");
33
}
34
}
35
36
37
public class Swordsman
38
{
39
private readonly Random random = new();
40
private readonly ILogger logger;
41
42
public Swordsman(ILogger logger)
43
{
44
this.logger = logger;
45
}
46
47
public int FastBlade()
48
{
49
var rand = random.Next(5, 50);
50
logger.LogEmphasis($"Using Fast Blade for {rand} hit points.");
51
return rand;
52
}
53
}
54
55
public class Marksman
56
{
57
private readonly Random random = new();
58
private readonly ILogger logger;
59
60
public Marksman(ILogger logger)
61
{
62
this.logger = logger;
63
}
64
65
public int RapidFire()
66
{
67
var rand = random.Next(5, 50);
68
logger.LogEmphasis($"Using Rapid Fire for {rand} hit points!");
69
return rand;
70
}
71
}

Now, here is where the ability of DI comes in and helps. Let’s first apply this principle to Archer

Jobs
1
// Before
2
public class Archer
3
{
4
private readonly Walker walker = new();
5
private readonly Attacker attacker = new(new ConsoleLogger());
6
private readonly Marksman marksman = new(new ConsoleLogger());
7
8
9
public void RapidFire() => marksman.RapidFire();
10
public void Walk() => walker.Walk();
11
public void Attack() => attacker.Attack();
12
}
13
14
// After
15
public class Archer
16
{
17
private readonly IMover mover; // Could be a Walker or Runner
18
19
// TODO: Fix these
20
private readonly Attacker attacker = new(new ConsoleLogger());
21
private readonly Marksman marksman = new(new ConsoleLogger());
22
23
public Archer(IMover mover)
24
{
25
this.mover = mover;
26
}
27
28
public void RapidFire() => marksman.RapidFire();
29
public int Move() => mover.Move();
30
public void Attack() => attacker.Attack();
31
}

Now apply this to the rest:

Jobs Folder
1
public class Warrior
2
{
3
private readonly IMover mover;
4
5
// TODO: Fix these
6
private readonly Attacker attacker = new(new ConsoleLogger());
7
private readonly Marksman marksman = new(new ConsoleLogger());
8
9
public Warrior(IMover mover)
10
{
11
this.mover = mover;
12
}
13
14
public void FastBlade() => swordsman.FastBlade();
15
public int Move() => mover.Move();
16
public void Attack() => attacker.Attack();
17
}
18
19
public class WhiteMage
20
{
21
private readonly IMover mover;
22
23
// TODO: Fix this
24
private readonly Healer healer = new(new ConsoleLogger());
25
26
public WhiteMage(IMover mover)
27
{
28
this.mover = mover;
29
}
30
31
public int Walk() => mover.Move();
32
public void Cure() => healer.Cure();
33
}

Let’s then create a simple factory to create our desired movemnet type:

Factories Folder
1
public static class BehaviorFactory
2
{
3
public static IMover CreateMover(string type, ILogger logger)
4
{
5
return type switch
6
{
7
nameof(Walker) => new Walker(logger),
8
_ => throw new Exception()
9
};
10
}
11
12
// attacker, etc...
13
}
Program.cs
1
var logger = new ConsoleLogger();
2
IMover mover = BehaviorFactory.CreateMover(nameof(Walker), logger);
3
4
var player1 = new Warrior(mover);
5
logger.LogSuccess($"{nameof(player1)} joins the battle!");
6
player1.Move();
Output
1
player1 joins the battle!
2
Walking 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:

1
using Microsoft.AspNetCore.Mvc;
2
3
namespace Practice.Service.Controllers;
4
5
[ApiController]
6
[Route("api/purchase")]
7
public class PurchaseFacadeController : ControllerBase
8
{
9
// Subsystems
10
private readonly INotificationService m_notificationService;
11
private readonly IPaymentService m_paymentService;
12
private readonly IInventoryService m_inventoryService;
13
14
private readonly ILogger<PurchaseFacadeController> m_logger;
15
16
public PurchaseFacadeController(
17
INotificationService notificationService,
18
IPaymentService paymentService,
19
IInventoryService inventoryService,
20
ILogger<PurchaseFacadeController> logger)
21
{
22
m_notificationService = notificationService;
23
m_paymentService = paymentService;
24
m_inventoryService = inventoryService;
25
m_logger = logger;
26
}
27
28
[HttpPost]
29
public IActionResult Post([FromBody] SubmitPaymentDto payment)
30
{
31
var inventory = m_inventoryService.Get();
32
33
if (!inventory.Any(item => item == payment.Item)) return BadRequest();
34
35
m_paymentService.Pay(payment.Amount, payment.Item);
36
37
m_notificationService.Send($"Item: {payment.Item} purchase with amount {payment.Amount}");
38
39
m_logger.LogInformation($"Submitted a payment of {payment.Item} with amount {payment.Amount}");
40
41
return Ok();
42
}
43
}

I hope this was useful!

x, gio