2

I'm currently working on a codebase and struggling to find an optimal and clean solution. I've removed the context of the problem to help simplify it to its root components. The Scale property is a simplification for a more complex state of the class in the actual codebase. I have an idea (which I'll reference at the bottom) for how I could solve this issue - however the solution feels messy and just avoids the area I want to better understand.

Class Hierarchy

public class GreatGrandparent
{
    public virtual int Scale { get; set; } = 1;
    public virtual int GetTrueScale()
    {
        return Scale;
    }
}

public class Grandparent : GreatGrandparent
{
    public override int Scale { get; set; } = 2;
    public override int GetTrueScale()
    {
        return Scale * base.GetTrueScale();
    }
}

public class Parent : Grandparent
{
    public override int Scale { get; set; } = 8;
}

public class Child : Parent
{
    public override int Scale { get; set; } = 4;
}

Somewhere else in code:

public class Main 
{
    Child aChild = new Child();
    int aChildTrueScale = aChild.GetTrueScale();
}
  • Expected Result: 4 (4×1) (Refer to Edit 1)
  • Actual Result: 16 (4×4)
  • Desired Result: 64 (4×8×2×1)

I want a child to find its relative scale by taking in all factors of scale from its parents, so that would like:

child relative scale = child scale × parent scale × … × base class scale

How can I (if possible) define the GetTrueScale method once in the parent class to get the desired result - which all children inherit - to avoid continuously overriding the method with duplicate implementations (the exception being the GreatGrandparent).

"Messy" Solution

Define a separate property/field in each class, and continuously override the aChildTrueScale() method with a return of ClassScale * base.GetTrueScale() where the ClassScale is a different property on each Class.

Edit 1

The expected result was my initial expectation based on my understanding at the time - thinking that within a base call the Scale reference would respect the change in scope change value to match that of the base class. With some further testing it appears that regardless of what scope when a base method is called, the referenced Scale value is always from the initial objects scope (hence 4*4).

Is it possible to refer to properties based on their scope? So in a base.GetTrueScale() call, any references within that function call will be on the base scope. Or am I completely missing something/trying to over simplify children?

Footnote

I've got a a bit of experience with procedural programming around data science, however I'm fairly inexperienced with object-oriented programming so forgive me if I'm ignorant to some core concepts. I’m happy to help clarify anything, thanks for taking the time to look over my first question! ^-^

(If anyone can think of a better title please let me know and I'll fix it up - was struggling to define the issue simply)

  • Thing is, each of the classes in your hierarchy don't have different `Scale` properties. It's the *same* `Scale`. Do your `Scale` properties really need to be writable? How would you set the value of `Parent.Scale`, given an instance of `Child`? – canton7 Apr 01 '22 at 07:53
  • You're also mis-using the term "Scope". I'm not sure what you're trying to refer to, but the scope of a variable is the region of code which can refer to it by name – canton7 Apr 01 '22 at 07:57
  • @canton7 Yeah in this case `Scale` is a simplification for a Hashset of objects, however I understand what you're getting at. The Hashset isn't writeable (so `Scale` shouldn't be in this case either that's my fault in the question). I wanted to simply the problem but in essence any nth-child has a HashSet that is in Union with all its hierarchy HashSets. The issue is the sense of maintainability that I want to be able to edit the values of a parent class if the features of the program change, and have child be aware. Sorry for any confusion! – BadCodeWithAngie Apr 01 '22 at 08:13
  • @canton7 Also thanks for highlighting my mis-use of "Scope", I was thinking in a runtime sense of the variables accessible to a method. I'm still learning alot and this is all new to me! ^-^ – BadCodeWithAngie Apr 01 '22 at 08:13
  • I guess my question still stands: in your real code, how are things added to a base class's `HashSet`? – canton7 Apr 01 '22 at 08:26
  • They're only added during development, not during runtime so they are immutable (if I'm using that term correctly aha). So if parent HashSet was {"valueOne"}, and the child's HashSet was "{"ValueTwo"} then the method should return {"ValueOne","ValueTwo"} – BadCodeWithAngie Apr 01 '22 at 08:34
  • Can you give us an actual example ([edit] something runnable into your question), rather than the over-simplified version you currently have? It's starting to sound like you actually just have a single `HashSet` which is added to by each sub-class, but I'd like to see something more concrete to be sure – canton7 Apr 01 '22 at 08:35
  • I edited above to I include an example above sorry, but yes you would be correct, its single `HashSet` that is added to by each class where modifications in hierarchy are shown in children. – BadCodeWithAngie Apr 01 '22 at 08:48
  • 1
    With this sort of hierarchy where each level maintains its own data, you probably don't want to use the *type system* to model the hierarchy but probably `Parent` and/or `Children` members of, potentially, a single type. Then each member of the hierarchy is an independent object. – Damien_The_Unbeliever Apr 01 '22 at 08:53
  • I don't see any recent edits in your question? Do you mean [something like this](https://sharplab.io/#v2:CYLg1APgAgDABFAjAFgNwFgBQUDMCBMcAglgN5ZyVwAOATgPYAuApgMYvBy3MCGw9AOwA2ATzgAJHgGcAFgGVmjADxIYAPjgzp8xXAC8cAcwDuACgCUGTFTgUquYhbuVy1m1S2yFjAHRFgwKYARABuPEIArswA8kZBls5wAL5YKZhYDlCEAEJwIMRkiQ7ZTm4uie6eOr7+gaHhUQAqxvTxVjZpSUA===)? – canton7 Apr 01 '22 at 08:57
  • @canton7 omg yes that would be correct. I feel so dumb I was overcomplicating it... In the `int` example I would just keep multiplying it in each constructor to keep track of the "true" value seperate as well! Will have to test later to see if I can implement it and ensure each constructor in the hierarchy is called to add to the `HashList`. – BadCodeWithAngie Apr 01 '22 at 09:05
  • 1
    One of a type's constructors will *always* be called. If you define multiple constructors, only one of them will be called, but you can chain them to ensure that a particular one is always called – canton7 Apr 01 '22 at 09:07

3 Answers3

2

I'd suggest using the type system to model this hierarchy is a mistake. You're wanting Child, Parent, GrandParent to be separate, independent things. That doesn't suggest the is-a relationship that you typically expect in the type system.

So instead have:

public class Thingy {
    public int Scale {get;set;}
    public Thingy Parent {get;set;}

    public int GetTrueScale()
    {
        var current = this;
        var accumulator = current.Scale;
        current = current.Parent;
        while(current!=null)
        {
            accumulator = accumulator * current.Scale;
            current = current.Parent;
        }
        return accumulator;
    }
}

And then create each of your objects:

var greatGrandParent = new Thingy {Scale = 1};
var grandParent = new Thingy {Scale = 2, Parent = greatGrandParent};
var parent = new Thingy {Scale = 8, Parent = grandParent};
var child = new Thingy { Scale = 4, Parent = parent};

And you can now call child.GetTrueScale() and all levels of the hierarchy are taken into consideration.

Adding Children sets to Thingy and other more interesting behaviours is left as a exercise.


Thingy itself could also be an IThingy interface, if the different levels truly do require separate types.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • @BadCodeWithAngie ^^ you may see this referred to as "composition over inheritence". – Fildor Apr 01 '22 at 09:04
  • Thank you heaps while @canton7 's answer helped me solve the issue at hand this is definitely worth considering in the perspective of the actual implementation. I'll have a go at this and see how it works. Thank you heaps! ^-^ – BadCodeWithAngie Apr 01 '22 at 10:24
  • I want to note for anyone else who stumbles that while accepted answer was the most straightforward solution to my problem, this answer was also extremely helpful! I tested this method out on my actual solution, and used Hashset.UnionWith() in the Thingy.GetTrueScale() implementation just fine! Great help, thanks @Damien_The_Unbeliever ^-^ – BadCodeWithAngie Apr 02 '22 at 00:27
2

From the discussion in the comments, it turns out that you don't need your Scale property to be settable (its value is fixed on construction), and you don't even need it to be virtual. You can just have a single property on the GreatGrandfather class, like so:

public class GreatGrandparent
{
    public int Scale { get; }
    public GreatGrandparent(int scale)
    {
         Scale = scale;   
    }
}

public class Grandparent : GreatGrandparent
{
    public Grandparent(int scale) : base(scale * 2) { }
}

public class Parent : Grandparent
{
    public Parent(int scale) : base(scale * 8) { }
}

public class Child : Parent
{
    public Child(int scale) : base(scale * 4) { }
}

In your actual code, you're dealing with a HashSet. You could write something like this, where each child adds new items to the GreatGrandparent's HashSet:

public class GreatGrandparent
{
    protected HashSet<string> hashSet = new();
    public GreatGrandparent()
    {
         hashSet.Add("itemOne"); 
    }
}

public class Grandparent : GreatGrandparent
{
    public Grandparent()
    {
        hashSet.Add("itemTwo");
    }
}

Or pass down the items to add in the constructor chain (more expensive, but maybe neater?):

public class GreatGrandparent
{
    protected HashSet<string> hashSet;
    public GreatGrandparent(IEnumerable<string> items)
    {
        hashSet = new(items);
        hashSet.Add("itemOne");
    }
}

public class Grandparent : GreatGrandparent
{
    public Grandparent(IEnumerable<string> items) : base(items.Concat(new[] { "itemTwo" })) { }
}
canton7
  • 37,633
  • 3
  • 64
  • 77
0

The type hierarchy will be called in the order from most base type -> most derived.

As you do not have overriden methods in Parent then your Scale is not multiplied. That it is a reason why you got 16. It is better to debug and see order of execution of your code.

You can add override GetTrueScale() method of class Parent to have desired value 64. The whole code will look like this:

public class GreatGrandparent
{
    public virtual int Scale { get; set; } = 1;

    public virtual int GetTrueScale()
    {
        Console.WriteLine("GreatGrandparent: " + Scale);
        return Scale;
    }
}

public class Grandparent : GreatGrandparent
{
    public override int Scale { get; set; } = 2;

    public override int GetTrueScale()
    {
        Console.WriteLine("Grandparent: " + Scale);
        return Scale * base.GetTrueScale();
    }
}

public class Parent : Grandparent
{
    public override int Scale { get; set; } = 8;

    public override int GetTrueScale()
    {
        Console.WriteLine("Grandparent: " + Scale);
        return Scale * base.GetTrueScale();
    }
}

and Child class:

public class Child : Parent
{
    public override int Scale { get; set; } = 4;
}
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • Isn't this just OP's "Messy" solution, which they already know about? – canton7 Apr 01 '22 at 08:32
  • Yeah I didn't mean discredit it as a solution by describing as "Messy" just to imply it *felt* tedious redefining the same functionality, and easily broken if another contributor inherited from my class and didn't correctly override the method with the same implementation. – BadCodeWithAngie Apr 01 '22 at 08:53
  • @canton7 yeah, it is. I just wanted to show why it happens. – StepUp Apr 01 '22 at 08:56
  • @BadCodeWithAngie if another developer breaks overriden methods, then developer breaks [Liskov substitution principle](https://stackoverflow.com/questions/56860/what-is-an-example-of-the-liskov-substitution-principle) of [SOLID principles](https://stackoverflow.com/questions/13692126/cant-seem-to-understand-solid-principles-and-design-patterns) – StepUp Apr 01 '22 at 09:03
  • Oh I've heard of the SOLID principles before, but haven't looked into them thank you for the link! Its probably worth me not worrying too much about people incorrectly using my code – BadCodeWithAngie Apr 01 '22 at 10:27
  • @StepUp Yeah that was my intention, it was just late at night for me! Thank you for your answer and a reminder! ^-^ – BadCodeWithAngie Apr 02 '22 at 00:22