Search code examples
c#genericsinheritancepolymorphismchildren

C# Cascading Derived Classes with Derived Properties


So I am designing a framework that has multiple classes, which derive from one another in a cascading fashion in order to perform more and more specific tasks. Each class also has its own settings, which are their own classes. However, the settings should have a parallel inheritance relationship to that of the classes themselves. It is important to the nature of this question that the settings MUST be separate from the class itself as they have constraints specific to my framework. (Edit: I also want to avoid the Composition pattern with settings as it greatly complicates the workflow in the framework I am using. So the settings must be single objects, not compositions of multiple objects.) That is I think of this as sort of like two parallel class hierarchies. One relevant example would perhaps be this:

Suppose you have a Vehicle class, and an accompanying class, VehicleSettings, that stores the relevant settings every vehicle should have, say topSpeed and acceleration.

Then suppose you have now a Car and an Airplane class, both of which inherit from the Vehicle class. But now you also create CarSettings, which inherits from the VehicleSettings class but adds the member gear. Then also have AirplaneSettings, inheriting from VehicleSettings, but adding the member drag.

To show how I might continue this pattern, suppose now I have a new class SportsCar, which inherits from Car. I create corresponding settings SportsCarSettings, which inherits from CarSettings and I add a member sportMode.

What is the best way to set up such a class hierarchy such that I can also account for the derived classes for the settings?

Ideally, I would like such an example to work like this:

public class VehicleSettings
{
    public float topSpeed;
    public float acceleration;
    // I am explicitly not adding a constructor as these settings get initialized and
    // modified by another object
}

public class Vehicle
{
    public float speed = 0;
    protected VehicleSettings settings;
    
    // Similarly here, there is no constructor since it is necessary for my purposes 
    // to have another object assign and change the settings
    
    protected virtual float SpeedEquation(float dt)
    {
        return settings.acceleration * dt;
    }
    
    public virtual void UpdateSpeed(float dt)
    {
        speed += SpeedEquation(dt)
        if(speed > settings.topSpeed)
        {
            speed = settings.topSpeed;
        }
    }
}

public class CarSettings : VehicleSettings
{
    public int gear;
}
    
public class Car : Vehicle
{
    // Won't compile
    public CarSettings settings;
    
    // Example of the derived class needing to use
    // its own settings in an override of a parent
    // method
    protected override float SpeedEquation(float dt)
    {
        return base.SpeedEquation(dt) * settings.gear;
    }
}

public class AirplaneSettings : VehicleSettings
{
    public float drag;
}
    
public class Airplane : Vehicle
{
    // Won't compile
    public Airplane settings;
    
    // Another example of the derived class needing to use
    // its own settings in an override of a parent
    // method
    public override void UpdateSpeed(float dt)
    {
        base.UpdateSpeed(dt) 
        speed -= settings.drag * dt;
    }
}

public class SportsCarSettings : CarSettings
{
    public bool sportMode;
}
    
public class SportsCar : Car
{
    // Won't compile
    public SportsCarSettings settings;
    
    // Here is again an example of a further derived class needing
    // to use its own settings to override a parent method
    // This example is here to negate the notion of using generic
    // types only once and not in the more complicated way mentioned
    // below
    public override float SpeedEquation(float dt)
    {
        return (settings.acceleration + (settings.sportMode ? 2 : 1)) * dt;
    }
}

Looking at a few possible solutions

  1. Use generic types. For example, I could have it so that it is public class Vehicle<T> : where T : VehicleSettings and I could have it have the line T settings so that when Car and Airplane inherit from Vehicle they can do that like this: public Car<T> : Vehicle<T> where T : CarSettings and public Airplane<T> : Vehicle<T> where T : AirplaneSettings, and so on. This does complicate things somewhat if I want to instantiate, for example, a Car without plugging in a generic type. Because then I would have to create a child class Car of Car<T> as follows: public class Car : Car<CarSettings> { }. And I would have to do similarly for every derived type.

  2. Use type casting in the necessary methods. For example, I could modify the Car class as follows:

     public class Car : Vehicle
     {
         // Don't reassign settings and instead leave them
         // as VehicleSettings
    
         // Cast settings to CarSettings and store that copy
         // locally for use in the method
         protected override float SpeedEquation(float dt)
         {
             CarSettings settings = (CarSettings)this.settings;
             return base.SpeedEquation(dt) * settings.gear;
         }
     }
    
  3. I also saw one recommendation to use properties as in this example, but this seems very clunky mainly since it doesn't seem to actually solve the problem. You would still have to cast the returned value to your desired type even if the dynamic type is returned from the property. If that is the proper way to go about it, I would appreciate an explanation as to how to properly implement that.

  4. It may be possible to use the new keyword to hide the parent class's version of settings and replace it with a new variable called settings of the correspond child class's settings type, but I believe this is generally advised against, mainly for reasons of complicating the relationship to the original parent class's 'settings', which affects the scope of that member in inherited methods.

So my question is which is the best solution to this problem? Is it one of these approaches, or am I missing something pretty significant in the C# syntax?

Edit:

Since there has been some mention of the Curiously Recurring Template Pattern or CRTP, I would like to mention how I think this is different.

In the CRTP, you have something like Class<T> : where T : Class<T>. Or similarly you might confront something like, Derived<T> : T where T : Base<Derived>.

However, that is more about a class which needs to interact with an objects that are of the same type as itself or a derived class that needs to interact with base class objects that need to interact with derived class objects. Either way, the relationship there is circular.

Here, the relationship is parallel. It's not that a Car will ever interact with Car, or that a SportsCar will interact with a Vehicle. Its that a Car needs to have Car settings. A SportsCar needs to have SportsCar settings, but those settings only change slightly as you move up the inheritance tree. So I think it seems kind of nonsensical that if such a deeply OO language like C# requires jumping through so many hoops to support "parallel inheritance", or in other words that it isn't just the object itself which "evolve" in their relationship from the parent to the child, but also that the members themselves do so.

When you don't have a strongly typed language, say Python for example, you get this concept for free since for our example, so long as the settings that I assigned to any particular instance of an object had the relevant properties, its type would be irrelevant. So I suppose it's more that sometimes the strongly typed paradigm can be a hindrance in that I want an object to be defined by its accessible properties rather than its type in this case with settings. And the strongly typed system in C# forces me to make templates of templates or some other strange construct to get around that.

Edit 2:

I have found a substantial issue with option 1. Suppose I wanted to make a list of vehicles and a list of settings. Suppose I have a Vehicle, a Car, an Airplane and a SportsCar, and I want to put them into a list and then iterate over the list of vehicles and settings and assign each setting to its respective vehicle. The settings can be put in a list of type VehicleSettings, however there is no type (other than Object) that the vehicles can be put in since the parent class of Vehicle is Vehicle<VehicleSettings>, the parent class of Car is Car<CarSettings>, etc. Therefore what you lose with generics is the clean parent child hierarchy that makes grouping similar objects into lists, dictionaries, and the like so comfortable.

However, with option 2, this is possible. If there is no way to avoid the aforementioned problem, option 2, despite being uncomfortable in some respects, seems the most manageable way to do it.


Solution

  • For my proposed solutions, we will use cast, which makes it similar to solution 2. But we will only have them in properties (but not properties like solution 3). We will also use the new keyword as in solution 4.

    I avoided solutions using generics (solution 1) because of the issue described under "Edit 2" on the question. However, I want to mention that you could have class Vehicle<T> : Vehicle where T : VehicleSettings, which I believe is what Charlieface meant in comments. As per that bringing back to solution 2, well, you can do it like I do in this answer plus generics if you want.


    Proposed solution

    This is my proposed solution:

    class VehicleSettings{}
    
    class Vehicle
    {
        private VehicleSettings _settings;
    
        public VehicleSettings Settings{get => _settings; set => SetSettings(value);}
        
        protected virtual void SetSettings(VehicleSettings settings)
        {
            _settings = settings;
        }
    }
    

    Notice, I am creating a private field. This is important. It allows us to control access to it. We will control how we read it, and how we write it.

    We will only read it in the Settings getter:

    public VehicleSettings Settings{get => _settings; set => SetSettings(value);}
    

    If you imagine that _settings may contain a derived type, there is no problem in going from that to the base type. Thus, we don't need derived classes to mess with that part.

    On the other hand, we will only write it the SetSettings method:

        protected virtual void SetSettings(VehicleSettings settings)
        {
            _settings = settings;
        }
    

    This method is virtual. This will allow us to throw if other code is trying to set the wrong settings. For example, if we have a class derived from Vehicle, say Car, but it is stored as a variable of type Vehicle and we try to write a VehicleSettings to Settings... The compiler can't prevent this. As far as the compiler knows, the type is correct. The best we can do is throw.

    Remember that those must be the only places where _settings is used. So, any other method must use the property:

    class VehicleSettings
    {
        public float Acceleration;
    }
    
    class Vehicle
    {
        private VehicleSettings _settings;
    
        public VehicleSettings Settings{get => _settings; set => OnSetSettings(value);}
        
        protected virtual void OnSetSettings(VehicleSettings settings)
        {
            _settings = settings;
        }
        
        public virtual float SpeedEquation(float dt)
        {
            return Settings.Acceleration * dt;
        }
    }
    

    And that would, evidently, extend to derived classes. This is how you go about implementing them:

    class CarSettings : VehicleSettings
    {
        public int Gear;
    }
    
    class Car : Vehicle
    {   
        public new CarSettings Settings
        {
            get => (CarSettings)base.Settings; // should not throw
            set => base.Settings = value;
        }
        
        protected override void OnSetSettings(VehicleSettings settings)
        {
            Settings = (CarSettings)settings; // can throw
        }
        
        public override float SpeedEquation(float dt)
        {
            return base.SpeedEquation(dt) * Settings.Gear;
        }
    }
    

    Now, I have added a new Settings property, that has the correct type. That way, code using it will get it. However, we do not want to add a new backing field. So this property will simply delegate to the base property. And that should be the only place where we access the base property.

    We also know that the base class will call OnSetSettings, so we need to override that. It is important that OnSetSettings ensures the type is correct. And by using the new property instead of the property in the base type, the compiler reminds us to cast.

    Note that these casts will result in an exception if the type is wrong. However, the getter should not throw as long as OnSetSettings is correct and you don't write to _settings anywhere else. You may even consider to write the getter with base.Settings as CarSettings.

    Furthermore, you only need those two casts. The rest of the code can, and should, use the property.

    Third generation class? Sure. Same pattern:

    class SportsCarSettings : CarSettings
    {
        public bool SportMode;
    }
    
    class SportsCar : Car
    {
        public new SportsCarSettings Settings
        {
            get => (SportsCarSettings)base.Settings;
            set => base.Settings = value;
        }
        
        protected override void OnSetSettings(VehicleSettings settings)
        {
            Settings = (SportsCarSettings)settings;
        }
        
        public override float SpeedEquation(float dt)
        {
            return (Settings.Acceleration + (Settings.SportMode ? 2 : 1)) * dt;
        }
    }
    

    As you can see, one drawback of this solution is that it needs some level of extra discipline. The developer must remember to not access _settings other than the getter of Settings and the method SetSetting. Similarly do not access the base Settings property outside of the Settings property (in the derived class).

    Consider using code generation to implement the pattern. Either T4, or perhaps the newly fangled Source Generators.


    If we don't need setters

    Do you need to write the settings? As I explained earlier, the setter may throw. If you have a variable of type Vehicle that has an object of type Car and try to set VehiculeSettings to it (or something else, like SportCarSettings). However, the getter does not throw.

    In C# 9.0 there is return type covariance for virtual methods (and properties, actually). So you can do this:

    class VehicleSettings {}
    
    class CarSettings: VehicleSettings {}
    
    class Vehicle
    {
        public virtual VehicleSettings Settings
        {
            get;
            //set; // won't compile
        }
    }
    
    class Car: Vehicle
    {
        public override CarSettings Settings
        {
            get;
            //set; // won't compile
        }
    }
    

    But if you add the setters, that won't compile. Ah, but you could easily add a "SetSettings" methods. It would make usage somewhat backwards, but implementation easier:

    class VehicleSettings {}
    
    class Vehicle
    {
        private VehicleSettings _settings;
        
        public virtual VehicleSettings Settings => _settings;
        
        public void SetSettings(VehicleSettings Settings)
        {
            OnSetSettings(Settings);
        }
        
        protected virtual void OnSetSettings(VehicleSettings settings)
        {
            _settings = settings;
        }
    }
    

    Similarly to the proposed solution, we will restrict access to the backing field. The difference is that now the property is virtual and does not have a setter (so we can override it with the appropiate type, instead of using the new keyword), instead we have a SetSettings method.

    class CarSettings: VehicleSettings {}
    
    class Car: Vehicle
    {
        public override CarSettings Settings{get;}
        
        public void SetSettings(CarSettings settings)
        {
            base.OnSetSettings(settings);
        }
    
        protected override void OnSetSettings(VehicleSettings settings)
        {
            SetSettings((CarSettings)settings);
        }
    }
    

    Also notice we don't have to use the keyword new on the methods. Instead, we are adding a new overload. Just like the proposed solution, this one requires dicipline. It has the drawback that not casting is not a compile error, it is just calling the overload (which results in a stack overflow, and I'm not talking about this website).


    If we can have constructors

    Are constructors a no-no? Because you could do this:

    class VehicleSettings {}
    
    class Vehicle
    {
        protected VehicleSettings SettingsField = new VehicleSettings();
    
        public VehicleSettings Settings
        {
            get => SettingsField;
            set
            {
                if (SettingsField.GetType() == value.GetType())
                {
                    SettingsField = value;
                    return;
                }
    
                throw new ArgumentException();            
            }
        }
    }
    

    And derived class:

    class CarSettings: VehicleSettings {}
    
    class Car: Vehicle
    {
        public Car()
        {
            SettingsField = new CarSettings();
        }
    
        public new CarSettings Settings
        {
            get => (CarSettings)base.Settings;
            set => base.Settings = value;
        }
    }
    

    Here, the setter enforces that the new value of Settings must be of the same type it currently has. That of course, poses a problem: we need to initialize it with the valid type, thus we need a constructor. I still add a new property for convinience.

    One important advantage is that nothing is virtual. So we don't need to worry about overriding and overriding correctly. We still need to remember to initialize.


    I considered using IsAssignableFrom or IsInstanceOfType, but that would allow to narrow the type. That is, it would allow to assign a SportsCarSettings to a Car, but then you would not be able to assign a CarSettings back.

    A workaround would be store the type… Set it from the constructor (or a custom attribute). So you would declare protected Type SettingsType = typeof(VehicleSettings); set it from the derived class constructor with SettingsType = typeof(CarSettings); and compare using SettingsType.IsAssignableFrom(value?.GetType()).

    Now, if you ask me, that is a lot of repetition.


    If we can have reflection

    Look at this code:

    class CarSettings: VehicleSettings {}
    
    class Car: Vehicle
    {
        public new CarSettings Settings
        {
            get => (CarSettings)base.Settings;
            set => base.Settings = value;
        }
    }
    

    Can it work? With reflection! By declaring this property we have specified the type that the base class should test against.

    The base class is this creature:

    class VehicleSettings {}
    
    class Vehicle
    {
        protected VehicleSettings? SettingsField;
        private Type? SettingsType;
    
        public VehicleSettings Settings
        {
            get => SettingsField ??= new VehicleSettings();
            set
            {
                SettingsType ??= this.GetType().GetProperties()
                    .First(p => p.DeclaringType == this.GetType() && p.Name == nameof(Settings))
                    .PropertyType;
                if (SettingsType.IsAssignableFrom(value?.GetType()))
                {
                    SettingsField = value;
                    return;
                }
    
                throw new ArgumentException();            
            }
        }
    }
    

    This time I have opted for lazy initialization, so no constructor. However, yes, that reflection works in constructor. It is getting the type of the property declared on the derived type via reflection, and checking if the value that we are trying to set matches.

    I had to use GetProperties because GetProperty would either give me System.Reflection.AmbiguousMatchException or null, depending on binding flags.

    A consequence is that forgetting to declare Settings in the derived class means that you won't be able to set the property (it will throw because it does find the property).

    And, yes, this continues for future generations. Just don't forget to declare a new Settings:

    class SportsCarSettings : CarSettings{}
    
    class SportsCar: Car
    {
        public new SportsCarSettings Settings
        {
            get => (SportsCarSettings)base.Settings;
            set => base.Settings = value;
        }
    }