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?

Viewed 79

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;
        }
    }
2 Answers

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.

In my view, the generic version fits for your requirements:

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

And then you can use any type:

var test = new Context<dynamic>();
test.MainObject = "1";

or:

var test = new Context<string>();
test.MainObject = "1";

or:

var test = new Context<int>();
test.MainObject = 1;

UPDATE:

The Serialize method accepts Context as an argument while the ResolveUiWidget method accepts Context as parameter.

So we can inherit from Context<T>. And code would like this:

public class Context : Context<dynamic> 
{ }

And you can use Context class in Serialize() method.

It is true that it is not possible to cast base class to derived.

However, we can write custom converter and use it:

public class ContextCast<T>
{
    public Context ToContext(Context<T> genericContext) 
    {
        return new Context() { MainObject = genericContext.MainObject};
    }
}

And your code would look like this:

Context<string> contextStr = new Context<string>() { MainObject = "Hello!"};

Context cntxt = new ContextCast<string>().ToContext(contextStr);

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 ?

In my view, it does not break SOLID principles as MainObject is just property which can be read or set. Behaviour of this object is always the same in base and derived classes.

Related