0

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;
        }
    }
EEAH
  • 715
  • 4
  • 17

2 Answers2

2

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Thank you. Seems a more reasonable approach. It should also solve the casting problem. But this is another post that shattered my previously read posts that teaches when to use interface vs abstract class : \ (Use interface for action intended methods). Any documentation or explanation that gives a somewhat general rule to when to use an interface vs abstract class ? – EEAH Sep 15 '22 at 21:23
  • A paper that dives deep in details maybe ? Or covers multiple use cases ? – EEAH Sep 15 '22 at 21:30
  • 1
    @EEAH interface is basically a contract, personally I think that working with abstraction over concrete implementation is preferable approach for medium to big projects cause it allows to decouple a lot of things and make code more unit testable and potentially more flexible. Abstract class allows you to share some implementation (which is also a point for debate - composition over inheritance is principle which gained quite big following) and it applicable in quite a lot of scenarios but in this particular it seems you care about contract cause there is not much to share in implementation. – Guru Stron Sep 15 '22 at 21:31
1

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.

StepUp
  • 36,391
  • 15
  • 88
  • 148