Extending base model classes for specific use cases

We have an application that is generally used as a “common” product by several different customers, and in general the same functions are available for all of them. However, there are some individually designed components, especially those related to importing data from the respective internal customer systems.

Until now, this has mainly meant converting different input formats into the same data structures, but it is becoming increasingly important to save additional data for specific clients for certain types and use them again in separate parts of the code that also “know” about this particular customer. This usually will not be many different places in the application.

To do this without the “polluting” classes of the main models, this “extensibility” is currently achieved by the fact that the classes in question are obtained from DynamicObject, and then have some code for dynamically determining properties associated with magic strings, and in other places they are checked and retrieved, which is not even safe, since you need to know what to use for a possible value.

I am rather dissatisfied with this and would like to propose a solution that is somewhat reminiscent of "monkey switching" and more idiomatic in a statically typed environment. The question is which is the best option for this. (An additional requirement is that additional data should be easily serialized / deserialized with the rest of the object.)

  • The “explicit” thing in an object-oriented language, of course, will subclass model types, but this creates a number of new problems around creating and copying objects and, as a rule, I don’t like what I really like (and it just breaks the “composition over inheritance ").
  • A solution that still uses inheritance but limits its impact will be adding a single property ExtensionDatato the actual class, its type is an empty class *ExtensionData, which is then subclassed for each client. Using this method involves some type checking and type casting for the actual extension data object, but beyond that, it will be completely type safe. I think this is my preferred method right now.
  • , , Dictionary<string, object>. , , , - ( , , ).

, ; , , - , .

( , F #, ExtensionData DU, , .)

"" #? , ?

+4
2

-

public class BaseClass
{
    Dictionary<Type, object> propertys = new Dictionary<Type, object>();

    public void Add<T>(T instance)
    {
        propertys[typeof(T)] = instance;
    }

    public T Get<T>()
    {
        return (T)propertys[typeof(T)];
    }

    public void Test()
    {
        Add<string>("Hello World");

        string helloWorld = Get<string>();
    }
}

- / .

, . , (er). ,

using System.Linq.Expressions;

// There will never be an implementation of this interface
// Its only there to "define" the property names and types without
// magic strings and in a type safe way
public interface IBaseClassExtension
{
    public string ExtensionProperty { get; set; }
    public int ExtensionProperty2 { get; set; }
}

public class BaseClass
{
    Dictionary<string, object> propertys = new Dictionary<string, object>();

    public void Add<T, U>(Expression<Func<T, U>> expr, U instance)
    {
        var propExpr = expr.Body as MemberExpression;
        // The declaring types name could be used in addition to be sure there 
        // is no naming conflict with members of other types that have the same member name
        //string name = propExpr.Member.DeclaringType.FullName + propExpr.Member.Name;
        propertys[propExpr.Member.Name] = instance;
    }

    public U Get<T, U>(Expression<Func<T, U>> expr)
    {
        var propExpr = expr.Body as MemberExpression;
        //string name = propExpr.Member.DeclaringType.FullName + propExpr.Member.Name;
        return (U)propertys[propExpr.Member.Name];
    }

    public void Test()
    {
        Add<IBaseClassExtension, string>(bce => bce.ExtensionProperty, "Hello World");
        string helloWorld = Get<IBaseClassExtension, string>(bce => bce.ExtensionProperty);
    }
}

,

# 6.0, , . .

public interface IBaseClassExtension
{
    public string ExtensionProperty { get; set; }
    public int ExtensionProperty2 { get; set; }
}

public class BaseClass
{
    Dictionary<string, object> propertys = new Dictionary<string, object>();

    public void Add(string name, object instance)
    {
        propertys[name] = instance;
    }

    public T Get<T>(string name)
    {
        return (T)propertys[name];
    }

    public void Test()
    {
        Add(nameof(IBaseClassExtension.ExtensionProperty), "HelloWorld");
        string helloWorld = Get<string>(nameof(IBaseClassExtension.ExtensionProperty));
    }
}

Edit:

/ ,

public class BaseClass
{
}

public class BaseClass<T, U, V> : BaseClass
{
    public T Extension1 { get; set; }
    public U Extension2 { get; set; }
    public V Extension3 { get; set; }
}

public class ClassThatIsUsingSpecificBaseClass
{
    public void Test(BaseClass<int, int, string> baseClass)
    {
        //baseClass.Extension1 ...;
        //baseClass.Extension2 ...;
    }
}

public class ClassThatIsUsingBaseClass
{
    public void Test(BaseClass baseClass)
    {
    }
}

typeafe, , BaseClass, /

0

, , , , .

, , , , , , , , .

, . , "" gist ( 200 ).

, .

// Marker interface, allowing reflection & discovery
public interface IExtendable {}

// Marker interface, allowing reflection & discovery
public interface IExtensionData {}

:

// Making it type safe
public interface IExtendableOf<T> : IExtendable where T : class, IExtendable 
{
    IExtensionDataFor<T> ExtensionData { get; set; }
}

// Making it type safe & discoverable through reflection.
public interface IExtensionDataFor<T> : IExtensionData where T : class
{
}

, .

// Factory implemented for each specific customer, discoverable.
public interface IExtensionProvider 
{
    IExtensionDataFor<TExtendable> CreateExtensionData<TExtendable>()
        where TExtendable : class, IExtendableOf<TExtendable>;

    IExtensionDataFor<TExtendable> CreateExtensionDataFor<TExtendable>(TExtendable instance)
        where TExtendable : class, IExtendableOf<TExtendable>;
}

( ), , "" PhoneNumber, :

// Example "core" class with extension data
public class PhoneNumber : IExtendableOf<PhoneNumber>
{
    public enum Kind { Internal, External }
    public string AreaCode { get; set; }
    public string Number { get; set; }
    public Kind NumberKind { get; set; }
    public virtual IExtensionDataFor<PhoneNumber> ExtensionData { get; set; }
}

"CustomerOne" , , :

// Customer specific extension data for a phone number
public class PhoneNumberExtension : IExtensionDataFor<PhoneNumber>
{
    public string Prefix { get; set; }
}

, , , , . :

// A quick & dirty example.
public static class TryExtensions
{
    static TryExtensions()
    {
        ExtensionProvider.Use(new TryOuts.ExtensionData.CustomerOne.ExtensionProvider());
    }

    public static void Run()
    {
        var phoneNumberOne = new PhoneNumber { NumberKind = PhoneNumber.Kind.Internal, AreaCode = "(231)", Number = "567 891 123" }.Extend();
        var phoneNumberTwo = new PhoneNumber { NumberKind = PhoneNumber.Kind.External, AreaCode = "(567)", Number = "555 666 777" }.Extend();
        Console.WriteLine("Phone Number 1: {0}", phoneNumberOne.ExtensionData.ToDictionary()["Prefix"]);
        Console.WriteLine("Phone Number 2: {0}", phoneNumberTwo.ExtensionData.ToDictionary()["Prefix"]);
        CodeThatKnowsCustomerOne(phoneNumberOne);
    }

    public static void CodeThatKnowsCustomerOne(PhoneNumber number)
    {
        var extensionData = number.ExtensionData as TryOuts.ExtensionData.CustomerOne.PhoneNumberExtension;
        Console.WriteLine("Prefix: {0}", extensionData.Prefix);
    }
}
0

All Articles