Adapter
Concept
Adapter is for adapting types that can not be modified, such like types from external libraries or sealed classes. Assuming that there is a extern class called Adaptee(Adaptee is the type to be adapted), it is not possible to modify the type(such like members or implemented interfaces) but eager to invoke its functionalities without breaking Open-Close Principle, use Adapter!
Basic Implementation
The type to be adapted Adaptee
is sealed to simulate the extern class. This pattern helps to prevent from break existing implementation like ExistingImpl.DoWorkWith(IAdapteeCannotImpl)
, and especially when ExistingImpl
is not modifiable.
interface IAdapteeCannotImpl
{
string DoWork();
}
sealed class Adaptee
{
public string DoSpecialWork() =>
$"{this.GetType().Name} can do something but you cannot simply invoke using existing implementation.";
}
class Adapter : IAdapteeCannotImpl
{
private readonly Adaptee _adaptee = new();
public string DoWork() => _adaptee.DoSpecialWork();
}
static class ExistingImpl
{
public static string DoWorkWith(IAdapteeCannotImpl obj) => obj?.DoWork()!;
}
public class AdapterTest
{
private readonly ITestOutputHelper? _helper;
public AdapterTest(ITestOutputHelper testOutputHelper) => _helper = testOutputHelper;
[Fact]
public void Test()
{
Adapter adapter = new();
_helper!.WriteLine(ExistingImpl.DoWorkWith(adapter));
}
}
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
Adapter Caching
Models
Assuming that we have a IGeometry
interface and a Geometry
class that references the IGeometry
to simulate the adapter pattern.
interface IGeometry
{
void Draw();
}
static class Geometry
{
public static void Draw(IGeometry geometry) => geometry.Draw();
}
2
3
4
5
6
7
8
9
Then we have two internal classes Point
, Line
implement IGeometry
and an extern class VectorObject
that cannot implement IGeometry
which is the target to be adapted. Although VectorObject
does access the internal Line
, it's just a example.
record class Point(int X, int Y) : IGeometry
{
public void Draw() => Console.WriteLine($"...Drawing point({X}, {Y})");
}
record class Line(Point Start, Point End) : IGeometry
{
public void Draw()
{
Start.Draw();
End.Draw();
}
}
// Adaptee
class VectorObject : Collection<Line>
{
public VectorObject(params Line[] lines) => lines.ToList().ForEach(this.Add);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Apply Adapter
Since VectorObject
cannot implement IGeometry
, we need to make a adapter for it so that it can be accepted by Geometry.Draw(IGeometry)
. The VectorObjectAdapter
accepts a VectorObject
to construct a sequence of Point
, and then implements the IGeometry
using Point.Draw()
. This is how VectorObject
was adapted.
class VectorObjectAdapter : Collection<Point>, IGeometry
{
public VectorObjectAdapter(VectorObject obj)
{
obj.ToList().ForEach(x =>
{
this.Add(x.Start);
this.Add(x.End);
});
}
public void Draw() => this.ToList().ForEach(x => x.Draw());
}
2
3
4
5
6
7
8
9
10
11
12
Problem
When invoke Geometry.Draw()
, VectorObjectAdapter
works fine.
VectorObject vec = new(
new Line(new(1, 1), new(2, 2)),
new Line(new(1, 2), new(3, 4))
);
VectorObjectAdapter vecAdapter = new(vec);
Geometry.Draw(vecAdapter);
2
3
4
5
6
7
However if you draw it twice, problem comes. If you want to draw a geometry in a diagram, it should be drew only once, or it may cause a bunch of memory issues and looks bad when displayed.
Solution - Apply Caching
Use static Dictionary<int, VectorObjectAdapter>
to record vectors that have been drawn. Since VectorObject
cannot be modified, so instead of overriding its GetHashCode()
, we implement it for VectorObjectAdapter
internal class VectorObjectAdapter : Collection<Point>, IGeometry
{
private static Dictionary<int, VectorObjectAdapter> _cache = new();
public VectorObjectAdapter(VectorObject obj)
{
obj.ToList().ForEach(x =>
{
+ if (!_cache.ContainsKey(x.GetHashCode()))
+ {
this.Add(x.Start);
this.Add(x.End);
+ }
});
}
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ this.ToList().ForEach(hash.Add);
+ return hash.ToHashCode();
+ }
+ private static bool HasDrew(VectorObjectAdapter obj) => _cache.ContainsKey(obj.GetHashCode());
public void Draw()
{
+ if (!HasDrew(this))
+ {
+ this.ToList().ForEach(x => x.Draw());
+ _cache.Add(this.GetHashCode(), this);
+ }
}
}
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
You might thought that we need to override GetHashCode()
for Point
and Line
first. Well, since Point
and Line
are record class, we don't really need to do that on our own. But this is what we can do unnecessarily.
record class Point(int X, int Y) : IGeometry
{
public void Draw() => Console.WriteLine($"...Drawing point({X}, {Y})");
+ // record class auto overrides GetHashCode()
+ // but to be explicit
+ // I don't want to elide it here
+ public override int GetHashCode() => HashCode.Combine(X, Y);
}
record class Line(Point Start, Point End) : IGeometry
{
public void Draw()
{
Start.Draw();
End.Draw();
}
+ // same as above
+ public override int GetHashCode() => HashCode.Combine(Start, End);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Generic Adapter
Due to some limitations of generic in C#
, when implementing some generic types, we have to apply adapters to overcome those limitations.
Numeric Vector Simple Implementation
Let's code a simple Vector
in math! Vector
in math takes some numbers to represent the value in different dimensions, such as [1, 2, 3]
for 3-dim, element of a vector can be a Vector
/Array
. Multiple vectors can be combined as a matrix, but that's not what we are talking about.
Limitation: Type Cannot be Literal
In C#
, generic type parameter cannot be a literal, so we cannot easily specify the dimension of Vector
, one approach is specifying dimension using constructor, but we will do the another way.
class Vector<TData, TDim>
{
protected TData[]? data;
public Vector()
{
// Error! TDim is a type, not a integer!
data = new TData[TDim];
// ^^^^
}
}
2
3
4
5
6
7
8
9
10
Well, TData
can be Vector
/Array
or other collection type, so we don't constraint it as struct
. Not really a matter.
To pass literal information with a type, we need abstraction. Let IDimension
as interface with some class implementing it. To be organized, specific dimension class can be nested in a static
/abstract
class or placed in a different namespace
interface IDimension
{
ushort Dimension { get; }
}
abstract class Dimension
{
public class Two : IDimension
{
public ushort Dimension => 2;
}
public class Three : IDimension
{
public ushort Dimension => 3;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
With implementation above, we now can modify the Vector<TData, TDim>
as
class Vector<TData, TDim>
where TDim : IDimension, new()
{
public static ushort Dimension => dim.Dimension;
protected static readonly TDim dim = new();
protected TData[] data;
public Vector()
{
data = new TData[dim.Dimension];
}
}
2
3
4
5
6
7
8
9
10
11
Since Dimension
property of IDimension
belongs to a instance, so TDim
shall have a new()
constraint to access the target property.
Now we can say, we already done one case of generic adapter!
Vector<int, Dimension.Two> vec = new();
Vector<int, Dimension.Three> vec = new();
2
No problem! At least for specifying dimension.
Implement Operators for Vector<TData, TDim>
One approach of implementing operators for Vector<TData, TDim>
is using interface
like IMultiplyOperators<TSelf, TOther, TResult>
and IAdditionOperators<TSelf, TOther, TResult>
in the .NET
BCL. But with this approach, TData
shall implement such interfaces
, it will work for numerics since they have already implemented those interfaces
, but won't work for numeric arrays because we can't modify the source code of .NET
.
Implementation for Numerics Only
If we don't care scenarios of arrays, we can implement it for numerics like
class Vector<TData, TDim> :
IMultiplyOperators<Vector<TData, TDim>, Vector<TData, TDim>, Vector<TData, TDim>>,
IAdditionOperators<Vector<TData, TDim>, Vector<TData, TDim>, Vector<TData, TDim>>
where TData : IMultiplyOperators<TData, TData, TData>, IAdditionOperators<TData, TData, TData>
where TDim : IDimension, new()
{
public static ushort Dimension => dim.Dimension;
protected static readonly TDim dim = new();
protected TData[] data;
public Vector() => data = new TData[dim.Dimension];
public Vector(params TData[] data)
{
if (data.GetLength(0) != Dimension) throw new InvalidDataException($"Dimension of {nameof(data)} doesn't match.");
this.data = data;
}
public static Vector<TData, TDim> operator *(Vector<TData, TDim> left, Vector<TData, TDim> right) =>
new(left.data.Zip(right.data, (x, y) => (x, y)).Select(x => x.x * x.y).ToArray());
public static Vector<TData, TDim> operator +(Vector<TData, TDim> left, Vector<TData, TDim> right) =>
new(left.data.Zip(right.data, (x, y) => (x, y)).Select(x => x.x + x.y).ToArray());
public TData this[ushort index]
{
get => data[index];
set => data[index] = value;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- It's worth noting that
params
version constructor overload accepts both unlimited count parameters and a array, it's actually two overloads. - If a type has only
params
version constructor, when no parameter were passed, it still pass a empty array.
Implementation includes Array
To make vector operators valid when TData
is array type, we can create a intermediate generic class that takes specific element type argument and leave TDim
to be specified.
Generic Adapter - Reducing Type Parameter
class VectorOfIntArray<TDim> : Vector<int[], TDim>
where TDim : IDimension, new()
{
public VectorOfIntArray() : base() { }
public VectorOfIntArray(params int[][] values) : base(values) { }
}
2
3
4
5
6
Now we need some modification on Vector<TData,TDim>
to check uniformity for each dimension of vector which is a array type.
class Vector<TData, TDim>
where TDim : IDimension, new()
{
public static ushort Dimension => dim.Dimension;
protected static readonly TDim dim = new();
protected TData[] data;
public Vector() => data = new TData[dim.Dimension];
public Vector(params TData[] data)
{
if (data.GetLength(0) != Dimension) throw new Exception($"Dimension of {nameof(data)} doesn't match.");
if (data[0] is Array f)
{
int dim = f.Length;
foreach (var array in data)
{
if (array is Array a && a.Length != dim)
{
throw new Exception($"Length of array in dim{Array.IndexOf(data, a) + 1} of {nameof(data)} doesn't match.");
}
}
}
this.data = data;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Now we can check whether it's valid.
// Error! Two arrays does not have the same length.
VectorOfIntArray<Dimension.Two> vectorOfInt = new(new[] { 1, 2 }, new[] { 1, 2, 3 });
2
Then we can impalement the IMultiplyOperators<TSelf, TOther, TResult>
and IAdditionOperators<TSelf, TOther, TResult>
for vectorOfInt<TDim>
.
class VectorOfIntArray<TDim> :
Vector<int[], TDim>,
IMultiplyOperators<VectorOfIntArray<TDim>, VectorOfIntArray<TDim>, VectorOfIntArray<TDim>>,
IAdditionOperators<VectorOfIntArray<TDim>, VectorOfIntArray<TDim>, VectorOfIntArray<TDim>>
where TDim : IDimension, new()
{
public VectorOfIntArray() : base() { }
public VectorOfIntArray(params int[][] values) : base(values) { }
public static VectorOfIntArray<TDim> operator +(VectorOfIntArray<TDim> left, VectorOfIntArray<TDim> right) =>
new(); // elide details
public static VectorOfIntArray<TDim> operator *(VectorOfIntArray<TDim> left, VectorOfIntArray<TDim> right) =>
new(); // elide details
}
2
3
4
5
6
7
8
9
10
11
12
13
VectorOfIntArray<Dimension.Two> vec1 = new(new[] { 1, 2, 3 }, new[] { 1, 2, 3 });
VectorOfIntArray<Dimension.Two> vec2 = new(new[] { 2, 2, 2 }, new[] { 1, 1, 1 });
Console.WriteLine(vec1 * vec2 is VectorOfIntArray<Dimension.Two>); // true
2
3
This is already an adapter! Well, not really generic enough. For each numeric array type, we need to repeat this approach. Next, we shall explore a more flexible way using recursive generic type parameter.
Recursive Generic Parameter
Now the most problem is, for each concrete TData
type, we need a new adapter class which is ugly, and for each adapter class we need to implement operators again and over again. Christ! Is there any possibility to "inherit" the operator? Well, operators are static
, we cannot inherit them in general, but the two parameters of a operator is covariant by default. Using this feature, we can do something just like operator overriding.
Introducing Another Type Parameter
What is the return type of the operator???
public static <Type> operator *(<Type> left, <Type> right)
Remember, we are aiming to implement operator overriding, so the return type must be the derived type of Vector<TData, TDim>
. But we cannot define a generic return type for any method or operator, so the lacked type information shall be added as a new type parameter for the class Vector<TData, TDim>
.
class Vector<TSelf, TData, TDim>
where TSelf : Vector<TSelf, TData, TDim>, new()
where TDim : IDimension, new()
{
//...elided
public static TSelf operator *(TSelf left, Vector<TSelf, TData, TDim> right) {...}
// ^^^^^^^^^^^^^^^^^^^^^^^^^
}
2
3
4
5
6
7
8
Now we have a new type parameter that inherits from Vector<TSelf, TData, TDim>
, which is the Recursive Generic Parameter.
- It is worth noting that at least one of two parameters of a operator should be the the instance of type the operator belongs to, or the compiler yells. That's why we make
right
as aVector<TSelf, TData, TDim>
.
Now let's implement the detail.
public static TSelf operator *(TSelf left, Vector<TSelf, TData, TDim> right)
{
if (right is not TSelf)
throw new InvalidCastException($"Cannot cast {nameof(right)} to {typeof(TSelf).Name}");
if (default(TData) is ValueType)
{
var opMultiInterface = typeof(IMultiplyOperators<,,>).MakeGenericType(typeof(TData), typeof(TData), typeof(TData));
var interfaceMethod = typeof(TData).GetMethod(
$"{opMultiInterface.Namespace}.{opMultiInterface.Name[..^2]}<{typeof(TData).FullName},{typeof(TData).FullName},{typeof(TData).FullName}>.op_Multiply",
(BindingFlags)(-1)
);
var ordinaryMethod = typeof(TData).GetMethod("op_Multiply", (BindingFlags)(-1));
var twoPossibleOperators = new[] { interfaceMethod, ordinaryMethod };
var value = left.data.Zip(right.data, (x, y) => (x, y)).Select(x =>
{
return (TData)twoPossibleOperators.Where(x => x is not null).First()!.Invoke(null, new object[] { x.x!, x.y! })!;
}).ToArray();
return Vector<TSelf, TData, TDim>.Create(value);
// ^^^^^^
}
else
{
// elide details
return new TSelf();
}
}
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
Limited by .NET
implementations for some value type, we need to use reflection, and the whole operation includes some sort of boxing, not quite efficient. Anyway, one thing we should notice is that we use Factory Method Vector<TSelf, TData, TDim>.Create(value)
to generate new instance of TSelf
, because in generic class we are not able to call constructors with any parameter.
The factory method is quite simple. Do remember dimension checking.
private static void CheckDimension(TData[] data)
{
if (data.GetLength(0) != Dimension) throw new Exception($"Dimension of {nameof(data)} doesn't match.");
if (data[0] is Array f)
{
int dim = f.Length;
foreach (var array in data)
{
if (array is Array a && a.Length != dim)
{
throw new Exception($"Length of array in dim{Array.IndexOf(data, a) + 1} of {nameof(data)} doesn't match.");
}
}
}
}
public static TSelf Create(params TData[] values)
{
CheckDimension(values);
return new TSelf { data = values };
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Finally! Let's test our new Vector<TSelf, TData, TDim>
! First we create a new derived class as VectorOf<TData, TDim>
because we can't use Vector<TSelf, TData, TDim>
directly, we can't simply say
Vector<Vector<Vector<..., int, Dimension.Two>, int, Dimension.Two>, int, Dimension.Two> vec = ...;
It's Recursive!!! Impossible to declare.
The only way to solve this problem is inheriting from the class with recursive type parameter. We can say
class VectorOf<TData, TDim> : Vector<VectorOf<TData, TDim>, TData, TDim>
where TDim : IDimension, new() { }
2
We don't have to implement any constructor because we have got Factory Method Vector<TSelf, TData, TDim>.Create(params TData[])
. Now it's totally valid.
VectorOf<decimal, Dimension.Two> v1 = VectorOf<decimal, Dimension.Two>.Create(1.2m, 2.2m);
VectorOf<decimal, Dimension.Two> v2 = VectorOf<decimal, Dimension.Two>.Create(1.4m, 2.3m);
Console.WriteLine(v1 * v2);
VectorOf<float[], Dimension.Two> va1 = VectorOf<float[], Dimension.Two>.Create(new[] { 1.2f, 1.3f }, new[] { 1.4f, 1.5f });
VectorOf<float[], Dimension.Two> va2 = VectorOf<float[], Dimension.Two>.Create(new[] { 1.2f, 1.3f }, new[] { 1.4f, 1.5f });
Console.WriteLine(va1 * va2);
2
3
4
5
6
7
Congratulations! This approach is completed at least logically, the compiler did't yell. Operators do works for every derived class of Vector<TSelf, TData, TDim>
and the result is just fine if you debug or implement your ToString()
for base class.
You may wonder, where is the adapter? You see, to actually use the recursive generic class Vector<TSelf, TData, TDim>
, we made a new adapter class VectorOf<TData, TDim>
, that's it!
Adapter using Dependency Injection
Let's start with a example. We got CommandOpen
and CommandClose
implement the ICommand
interface. The Button
class requires a ICommand
field to perform particular behavior. The Editor
class contains a sequence of Button
.
interface ICommand
{
void Execute();
}
class CommandOpen : ICommand
{
public void Execute() => Console.WriteLine("Opening...");
}
class CommandClose : ICommand
{
public void Execute() => Console.WriteLine("Closing...");
}
class Button
{
private ICommand _command;
public Button(ICommand command) => _command = command;
public void Click() => _command.Execute();
}
class Editor
{
private IEnumerable<Button> _buttons;
public Editor(params Button[] buttons) => _buttons = buttons;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
To do an adapter, we first register some type.
var builder = new ContainerBuilder();
builder.RegisterType<CommandOpen>().As<ICommand>();
builder.RegisterType<CommandClose>().As<ICommand>();
builder.RegisterType<Editor>();
2
3
4
Then we say
using var container = builder.Build();
var editor = container.Resolve<Editor>();
2
Now what's the status of editor
? Autofac
has initialized IEnumerable<Button> _buttons
using constructors with parameter that is dependent on corresponding type recursively. So now _buttons
has only one element where its _command
is CommandClose
, because CommandClose
is the last type we registered as ICommand
. The problem is, how to resolve Editor
making each ICommand
class a element of _buttons
?
Try this!
var builder = new ContainerBuilder();
builder.RegisterType<CommandOpen>().As<ICommand>();
builder.RegisterType<CommandClose>().As<ICommand>();
//+
builder.RegisterAdapter<ICommand, Button>(cmd => new Button(cmd));
//+
builder.RegisterType<Editor>();
using var container = builder.Build();
var editor = container.Resolve<Editor>();
2
3
4
5
6
7
8
9
Now the _buttons
has two elements where its _command
type as CommandOpen
and CommandClose
because they are the only two classes we registered as ICommand
.