Design Patterns — Adapter .NET

This time I`ll remind you about a pretty helpful pattern named adapter. It helps to unify different contracts into one and makes code easier to maintain and read

adapter design pattern in .net

Introduction

The adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.

Let`s examine a problem when we face a situation where systems with incompatible interfaces should collaborate. I will highlight the most trivial case from the top of my mind. We have different sources of information, and we have to retrieve or put some data from/to all of them. But as it always happens, each external source has a different contract.

If we were terrible developers, we would probably write something like the following code example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public interface IMainAnalyticsLib
{
Task<IEnumerable<(DateTime date, int eventCount)>> GetAnalyticsAsync(string someId);
}

public interface IAlternativeAnalyticsLib
{
public class DataItem
{
public DateTime DateTime { get; set; }
public int EventCount { get; set; }
}

Task<IEnumerable<DataItem>> GetHistorycalDataAsync(Guid someId);
}


public class AnalyticItem
{
public DateTime Date { get; set; }
public int Count { get; set; }
public string Source { get; set; }
}

public class AnalyticsRetriever
{
private IMainAnalyticsLib _mainAnalyticsLib;
private IAlternativeAnalyticsLib _alternativeAnalyticsLib;
private IMapper _mapper;
public async Task<IEnumerable<AnalyticItem>> GetItemsAsync(string source, string streamId)
{
switch (source)
{
case "MainAnalytics":
{
var items = await _mainAnalyticsLib.GetAnalyticsAsync(streamId);
return _mapper.Map<IEnumerable<AnalyticItem>>(items);
}
case "AlternativeAnalytics":
{
var items = await _alternativeAnalyticsLib.GetHistorycalDataAsync(streamId);
return _mapper.Map<IEnumerable<AnalyticItem>>(items);
}

}
throw new Exception("Unknown source");
}
}

In the example above, we have two different sources of analytic data and a single method in AnalyticRetriever which hides from external client complexity of calling proper analytic source.

Problems

With the new analytic source, we have to make changes in AnalyticRetriever. An additional case in switch breaks the open-close principle. A lot of logic must be covered in unit tests for AnalyticRetriever. With new analytic sources, the number of tests will increase drastically. We can say that in this situation, GetItemsAsync is a GOD method that has a lot of knowledge about external sources.

The adapter is a silver bullet for such a case

As the adapter description states, its main goal is a collaboration between incompatible interfaces. To achieve this, we need to refactor the code above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public interface IMainAnalyticsLib
{
Task<IEnumerable<(DateTime date, int eventCount)>> GetAnalyticsAsync(string someId);
}

public interface IAlternativeAnalyticsLib
{
public class DataItem
{
public DateTime DateTime { get; set; }
public int EventCount { get; set; }
}

Task<IEnumerable<DataItem>> GetHistorycalDataAsync(Guid someId);
}

public interface IExternalAnalyticsSourceAdapter
{
public string SourceType { get; }
Task<IEnumerable<AnalyticItem>> GetItemsAsync(string someId);
}

public class MainAnalyticsAdapter : IExternalAnalyticsSourceAdapter
{
private readonly IMainAnalyticsLib _mainAnalyticsLib;
private readonly IMapper _mapper;
public MainAnalyticsAdapter(IMainAnalyticsLib mainAnalyticsLib, IMapper mapper)
{
_mainAnalyticsLib = mainAnalyticsLib;
_mapper = mapper;
}

public string SourceType => "Main";

public async Task<IEnumerable<AnalyticItem>> GetItemsAsync(string someId)
{
var items = await _mainAnalyticsLib.GetAnalyticsAsync(someId);
return _mapper.Map<IEnumerable<AnalyticItem>>(items);
}
}

public class AlternativeAnalyticsAdapter : IExternalAnalyticsSourceAdapter
{
private readonly IAlternativeAnalyticsLib _alternativeAnalytics;
private readonly IMapper _mapper;
public AlternativeAnalyticsAdapter(IAlternativeAnalyticsLib alternativeAnalytics, IMapper mapper)
{
_alternativeAnalytics = alternativeAnalytics;
_mapper = mapper;
}
public string SourceType => "Alternative";

public async Task<IEnumerable<AnalyticItem>> GetItemsAsync(string someId)
{
var externalId = Guid.Parse(someId);
var items = await _alternativeAnalytics.GetHistorycalDataAsync(externalId);
return _mapper.Map<IEnumerable<AnalyticItem>>(items);
}
}

public class AnalyticItem
{
public DateTime Date { get; set; }
public int Count { get; set; }
public string Source { get; set; }
}

public class AnalyticsRetriever
{
private Dictionary<string, IExternalAnalyticsSourceAdapter> _sources;
public async Task<IEnumerable<AnalyticItem>> GetItemsAsync(string source, string streamId)
{
if (!_sources.ContainsKey(source))
{
throw new Exception("Unknown source");
}
return await _sources[source].GetItemsAsync(streamId);
}
}

What are the benefits?

As you can see in the adapter example, which is much better, and here is why a new interface was introduced and two different implementations were created for each source, respectively.

Each implementation hides a switch case branch. Such an object approach allows making AnalyticsRetriever so flexible that we don`t need to touch it in case of a new source. New adapter implementation will contain all required logic for a new source.

Also, we have a cleaner unit tests coverage of all business logic for retrieving separated in adapter classes, and it`s straightforward how to cover it with unit tests.

We made unification with the new interface. This is the central part of the adapter pattern that we need to introduce an abstraction that will hide all differences from third-party interfaces.

Conclusion

The adapter gives us an object-oriented way to handle program flow instead of a simple form with ifs or switches. It covers specific cases and makes our code more readable and maintainable.