0

I have a couple of pieces of functionality that are similar but not quite the same. They both involve converting a type that I own to other types that are owned by a third party library, so out of my control.

I am relatively inexperienced at using design patterns properly and it is something I am trying to get better at. I feel like this problem is something that could be solved with something like an adapter but the two don't quite marry up. Here is a very watered down/simplified version of what my code kind of looks like:

public class ThirdPartyTypeConverter : ITypeConverter
{
    ...

    public ThirdPartyTypeA[] Convert(MyType source)
    {
        switch (source.EnumCondition)
        {
            ...
            case ConditionWeCareAbout:
                return source.Collection.Select(c => new ThirdPartyTypeA
                {
                    Id = c.Id,
                    Name = c.BuildName,
                    Action = c.Action
                };
        }
    }
}

...

public class AnotherThirdPartyTypeConverter : ITypeConverter
{
    ...

    public ThirdPartyTypeB Convert(MyType source)
    {
        switch (source.EnumCondition)
        { 
            ...
            case ConditionWeCareAbout:
                return new ThirdPartyTypeB
                {
                    Id = c.Id,
                    Name = c.BuildName,
                    Action = c.Action
                };
        }
    }
}

Those 2 conversions on those switch cases do pretty much the same thing, except that they map to different types. Initially I wondered whether I could have a layer in between to extract that away but there's no way to know how to generate a type on the fly (in reality they don't perfectly match).

Is this something that can be broken out or is it just something that has to be lived with?

  • What's in the other cases? are there multiple other ways of creating ThirdPartyTypeA[]s and ThirdPartyTypeBs from MyTypes or is it return null;? – Caleth Jul 18 '18 at 13:44
2

The core of the issue here is that you're unable to create code that works for both ThirdPartyTypeA and ThirdPartyTypeB, because there is no formal link between them, and because of that the compiler refuses to understand/acknowledge that the posted code works for both types.

There are several ways around this:

1. Implement inheritance

Less appropriate for third party types, but if all types were owned by you, you should first ask yourself if there is a common inheritance between the two, before moving to step 2.

I want to stress here that inheritance is not always better than interfaces. Inheritance can be abused, and excessive inheritance is a pain to deal with.

I'm suggesting looking at inheritance before interfaces because problems solved by inheritance often exist on a larger scope than problems solved by interfaces. Look at the macro (inheritance) before you look at the micro (interfaces).

2. Implement interfaces

When there is no inheritance logic; the second option is to implement a basic interface between them.

A simple example:

public interface IFoo
{
    string Name;
}

public class ThirdPartyTypeA : IFoo
{
    public String Name { get; set; }
}

public class ThirdPartyTypeB : IFoo
{
    public String Name { get; set; }
}

And then:

public T CreateFromMyType<T>(MyType myType) where T : IFoo, new()
{
    var obj = new T();

    obj.Name = myType.Name;

   return obj;
}

And its usage:

ThirdPartyTypeA obj = CreateFromMyType<ThirdPartyTypeA>(myType);

This is just a rudimentary example to show you how to implement it on a basic level. There are multiple unknowns in your example code (e.g. how diverse the enum conditions are, what actually changes between different enum values, ...) that need to be addressed before you can create a fitting solution.

3. Reflection, if you must.

I don't like excessive use of reflection. However, it does give you the ability to get/set properties based on a string name, thus bypassing the type safety that C# provides.

Unless you can find a justifiable reason to not use interfaces, I consider reflection an inferior solution to interface implementation. Not because it doesn't work, but because it throws type safety out the door, trades compile time errors for runtime errors, and it's easy to shoot yourself in the foot with (the nameof() operator helps to not shoot yourself in the foot, but it's a partial improvement).

4. Convert to/from a string-serialized type

Similar to reflection, you could convert your data to a JSON string (with the common property name) and then convert that JSON to a third party type. String-based serializers access properties based on their name and ignores C#'s type safety.

This is really just using reflection by a different name. It slightly avoids type unsafety (because you can define your intermediary DTO class in code), but it still throws type safety out the door and trades compile time errors for runtime errors.

5. Move to a type-unsafe language.

I'm aware that this is not a realistic solution, but I do want to point this out.

Type safety exists for a reason. Before type safety existed, applications heavily depended on the developer's aptitude at managing a complex web of property references.

A similar thing happened for C/C++ pointers. While very powerful, they were also very complicated, easy to shoot yourself in the foot with, and nigh impossible to easily debug. This is why C# has moved on to use reference types, effectively having the compiler do the memory allocation for you.
Properly written C++ outperforms C#, but the difficulty in writing that perfect C++ is so much higher than working with reference types in C#.

With type safety, we're at the same crossroads. Do we rely on developers managing it all themselves, or do we automate it in order to make things so much easier (at the cost of minor inconveniences)?

Interesting discussions on this topic can be found by following "Typescript vs Javascript" discussions. Typescript was created specifically to introduce type safety to the otherwise type-unsafe Javascript. I would argue that the current rising popularity of Typescript proves that type safety is something the development sector wants to have.

Unless you create a formal link between the two third party classes (inheritance or interface), you're effectively trying to ignore the globally enforced rule of type safety. This suggests that you approach is simply not compatible with C#'s approach.

I suggest adapting your approach instead of trying to go around the ideology of the language you're programming in.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.