Search code examples
c#design-patternssubclasssolid-principles

Is this a correct way of having a sub class override a super classe's member type ? And does it break SOLID principles and design patterns?


The question should be language agnostic, but in this case C# is used.

There are 2 classes: Context and Context<T>.

Context contains MainObject which is of type dynamic (can be of type object as well) Context<T> needs to specify more the MainObject type.

The main purpose is to be able to use the Context class in services and functions that doesn't have the responsibility of knowing the specific type of MainObject at compile time.

For example The Serialize method accepts Context as an argument while the ResolveUiWidget method accepts Context<UiWidget> as parameter. Also if var context = Context<UiWidget> Serialize(context)` is a valid statement. So this design seems beneficial.

Is this a wrong approach or does it break the SOLID principles and design patterns ? If yes, is there another way of satisfying the parameters of Serialize and ResolveUiWidget methods ?
Maybe this is looking too much into the future but as a learning experience, getting a sense that Likov principle can be broken but not able to fully see a scenario.

This is the current code:

    public class Context
    {
        public virtual dynamic? MainObject { get; set; }
    }

    public class Context<T>: Context
    {
        public new T? MainObject { get; set; }

        public Context()
        {
            MainObject = base.MainObject;
        }
    }

Solution

  • I would suggest moving from classes to interfaces, ideally read-only (i.e. with no set property method).

    Why read-only:

    1. Currently your design has huge problem (even if you were able to compile it):
    Context<int> x = ...;
    
    public void AcceptsBaseContext(Context ctx)
    {
        ctx.MainObject = "haha";
    }
    
    AcceptsBaseContext(x);
    
    1. You can make interfaces covariant and implement non-generic interface explicitly, similar to what IEnumerable does:
    public interface IContext
    {
        object? MainObject { get; }
    }
    
    public interface IContext<out T> : IContext
    {
        new T? MainObject { get; }
    }
    
    public class Context<T> : IContext<T>
    {
        public T? MainObject { get; }
        object? IContext.MainObject => MainObject;
    
        public Context(T? obj)
        {
            MainObject = obj;
        }
    }
    

    Or you can change the generic interface to:

    public interface IContext<T> : IContext
    {
        new T? MainObject { get; set; }
    }
    
    public class Context<T> : IContext<T>
    {
        public T? MainObject { get; set; }
        object? IContext.MainObject => MainObject;
    
        public Context(T? obj)
        {
            MainObject = obj;
        }
    }
    

    If you need to write to the property.

    Either way method which are context type agnostic can work with it via IContext interface while you still preserve type safety.