Need advice from advanced C# coders out there -- OO design practice

Hello everyone!

Being a knowledgeable programmer (not all-knowing though, don’t get me wrong), I usually don’t have a question, at least not one I can’t Google out in minutes or seconds. (I’ve been programming for decades is all I’m saying so we can go as deep as you’d like in trying to help me.)

However, this time I figured out I should ask for some advice or at least for opinions on one of the code design problems I’ve been having for ages, that don’t seem to be googleable (or acquired from my professional background). So I’m basically curious whether this is just an ugly symptom of the OO form factor in general, or if I’m being silly and there is a decent enough workaround that I’m somehow unable to see.

To understand my problem, let me describe the situation a little more generally. It’s a common pattern, I’m sure plenty of you had this going on before, and numerous times. Namely, you have a very simple hierarchy involving some parent object (I’m talking about dry OO here, not gameobjects) that can own strictly-typed compatible child objects. You feed the parent with some general properties, and instruct it to instantiate its children according to some pattern or rules, and that’s it!

If it helps, this could be as simple as a tilemap, as an example, with children being individual tiles, where each tile has some sort of a unique identifier (in this case it’s their coordinates), and you obviously set the global grid size in the parent, and let it predictably populate the model etc.

Now imagine you’d like to be able to interrogate the individual tiles, and ask them about their size or some other general property, right? It’s a silly example, but bear with me. Because such data is collective and immutable anyway, it doesn’t need to be mindlessly repeated per instance, so a tile would have to be able to internally communicate with its parent to get the result. I hope this paints a clear picture of the set up.

Therefore, if we follow this line of thoughts, the children are dumb, at least when it comes to any information that is clearly above them, while their parent acts as some sort of a representative. It’s their guardian, right? lol And okay, if it’s critical enough and there aren’t that many children, one could obviously pass the reference to their parent during instantiation, thus enabling this internal protocol to take place. A child would do public Vector2 Size => _myParent.Size; and we’re done.

But now we come to the actual problem. What if I didn’t want to bloat the child with extraneous 4 (or in fact 8 bytes which is a lot) per instance, which is the weight of this completely redundant pointer sitting dead in memory. What if we had hundreds of thousands of children, but only a couple of data points that should be communicated through this, and likely not that frequently, as it’s there mostly for the sake of having a complete and well-intended API?

Yes, I hear you, they could or should be referring to their parent through a static pointer, but that’s not a particularly robust solution because what if we had several different parents with different global parameters and different sets of children? I want to instantiate the entire model several times, so the question is how do you tie up individual tiles with their corresponding parents without storing that reference per instance.

I hope my explanation is sufficiently clear, and that you can see there is a hard trade off about this because you have to pick whether a child knows any extended stuff, and if it does, it becomes much more memory-heavy in bulk, which makes sense really only if it’s supposed to share loads of data, not just couple of things at most. And if it doesn’t know stuff, it either a) has to ask its parent about it, which might be a problematic design (i.e. identifying a child could be an algorithmic problem, and not just O(1), say the parent was keeping track of them in a list, and it’s not just size, but something more exotic instead, a neighbor lookup or something along these lines), or b) simply lacks any extended information, has a really light memory footprint and a straightforward interface, but the design requires you to always interrogate the parent instead, which is sometimes a clear case of a misplaced responsibility.

Let me elaborate this example further: Why would a grid have to provide an answer about some particular tile’s area? Well, because the tile doesn’t know its own size, that’s why, so it cannot extrapolate anything. Dumb as a brick. And if we wanted to move this responsibility to the tile instead, we’d have to repeat a pointer (or the actual information) in each tile instance, needlessly bloating the entire collection from the original, say, 14 Mbytes to a whopping 30. Multiply this by several model instances, and you get hundreds of Mbytes just needlessly wasted due to poor design.

Finally, I was thinking about using some sort of mediation, where a tile itself would be a key that could be used to resolve who its parent was by some 3rd-party mediator, and in this case the access to this mediator could be static. For some models this is really neat, especially if there is some piece of information that could be used to cleanly transform the child’s identity into a pointer to its parent, but for the simpler ones all of this is just an overdesign and an implementation liability.

Please bear in mind that a) this question is not really about tiles, the actual designs I regularly stumble upon are typically more developed and convoluted where this matter of placing API where it belongs isn’t just my OCD but there are actually deeper reasons for separating concerns, and b) I don’t really have a technical problem with any of this, it’s not that I’m stuck or that I cannot compromise with it, I’m mostly philosophically interested in achieving a better design, in hearing out different solutions and listening to potential workarounds for this particular case.

Especially if they are simple to implement, easy to maintain, and fulfill this basic requirement of not wasting memory just because we can.

I am well aware that 30 Mbytes of extra memory is nothing for todays computers, so please don’t offer “RAM is cheap and not a problem nowadays” as an answer or excuse, because this is not a question I’m asking and will likely only clutter the thread. And besides we all have StackOverflow for such answers anyway :slight_smile:

So, that aside, what are your thoughts on this?! How do you normally approach this situation? (And thanks for reading and any replies.)

1 Like

As you said, you can use a static dictionary, use the tiles as keys and the parent as value.
Obviously the dictionary will grow quite large and it won’t be any better.

You can also provide the tiles a unique Id corresponding to the parent.
Hopefully you know from design you won’t have more than 256 parents at the same time.
Then you have a static array of 256 pointers to the parents.
That way it will only cost you 1 octet per tile along with 256 * 8 octets for the static array (it will also be a lot faster to access the parent than a huge dictionary).

You could also create a base tile type, and then a specific tile class for each parent.
That way each tile would know its parent as it knows its own type.
Of course it’s not easy to do that at runtime, it requires to generate code at runtime but it can be done (except if you’re on mobile…).

Personally, I’d use the specific tile class for each parent.
I would probably have a pool of them (the runtime-generated classes I mean, but I’d also have another pool for its objects anyway), that way I would reduce the code-generation at runtime to the bare minimum.
But that’s because I’m not working on mobile, and I really like code-generation at runtime, so I’m -a bit- biased…

In typical StackOverflow fashion, I’ll respond by questioning about your approach.
Are you certain that you can not split up your data into different fundamental types? I’m mostly thinking of a Minecraft style voxel world analogy:

Like a “dumb”, “static” and “instance-context” split between types; where each builds upon the one before it. Where callbacks could be implemented on different levels:

  • no callbacks ← say dirt; just some flags/settings on how things generally work
  • IOnCreated1 (World world, int3 xyz); ← support sampling neighbours for cellular automate style things; water, redstone
  • IOnCreated3 (World world, int3 xyz, ref object instanceContext); ← support storing some context for later callbacks to re-use; chests

and the class responsible for executing the callbacks would manage where the context is stored (probably some dictionaries). Most data would be “dumb” and storable using the flyweight pattern (possibly multiple levels for spatial compression).

It’s hard to answer stuff like this in a vacuum because just small details in how you set up the scene or your individual assets can dramatically impact how you should organize the code that controls them.

Because Unity organizes assets naturally into “collections,” such as scenes and prefabs and chunks of hierarchy, coding interrelations between those chunks (and indeed intra-relations within each chunk) can be varied, and should be varied according to the problem you’re trying to solve.

For instance, if you expect the player to shoot at a tile and destroy only parts of it, that’s gonna have a lot of implications as to how you structure scripts that control and interrelate those parts, as compared to tiles that are never going to be user-changed.

At the end of the day, try to solve the problems you have in front of you the best you can, and when you think you might have to refactor, then by all means refactor. Remember it is SOFT-ware, which means it can be changed. Keep a firm grasp on the context your code will be used in, and try to make your life as simple and easy as possible to produce content, because when you (or your art staff) is busily producing content, that’s where you want things to work really optimally.

Yep, such a solution cannot be as raw as this, there has to be a design piece in each child instance, some kind of a bitwise signature or something else embedded into the actual child identity, otherwise having each child map to a unique key, and then repeating this as many times as there are children is nonsensical.

Exactly, it’s a neat idea, and some kind of a signature would have to be slipped per instance, and some kind of limitation would probably be part of the design, but that’s a well-placed compromise, and then this model would work just fine.

But still, I’m not terribly happy with it, it is simple enough on paper, but doesn’t look that way when it’s actually implemented. It’s an unconventional approach so it has to be well-documented to remain maintainable; also any bugs in this system would be very annoying, y’know, children cross-referencing someone else’s parents? Yuck. And also could lead to things not being properly garbage collected.

That said, it is an ok thing to build as a specific solution to some of the problems in the general problem space. It could also be generalized to an extent, but doesn’t solve the need for having a pick-up-and-go solution that is easy to read into, and easy to slap on a much simpler model, without any forethought.

Both of these approaches are very similar, so let me cover both of these with a resounding YES!
This is actually a schoolbook yes, both ways are the proper way to do this in the OO paradigm. It’s the best and the most general way of thinking about this, and a way to go. Sadly, the designs I regularly come up with are more data-oriented (because games) and sometimes have to go against what’s recommended by OO.

These are the things I regularly refrain from doing (because of being in control, of unwanted indirection, gc, unintentional heap allocation, and such):

  1. boxing/unboxing, in any way or form, thus I can’t store my types as objects,
  2. using reference types when a value type would suffice (especially once we got the ‘in’ keyword),
  3. frequent or small-fry malloc, I would rather pool an array or reuse a private static,
  4. abusing interfaces (because of hitting (1) behind the scenes in many cases).

And here I’m describing the ways I’m normally treating my critical paths, not regular high level code, and any data-oriented approach to children hierarchies is surely part of a critical path. As Zuntatos intuitively guessed, it smells like voxels, right? Yes, I had a similar problem with voxels a while ago, but that one I managed to solve satisfactorily. ( @Zuntatos Yes it was similar to what you’ve described, I like your solution a lot, flywheel is fine when suitable).

The idea of having specialized subclass is all fine and dandy. I have nothing against that, however, here is one example where doing this wouldn’t make the problem go away, in fact the general design would become much worse.

  1. you have a HexGrid class that does what it says on the tin, you pass in some general information (like obviously pointy-top vs flat-top style, tile radius, offset, whathaveyou) and voila;

  2. it stores nothing and keeps track of nothing but basic properties; it’s just a glorified calculator for converting arbitrary coordinates into abstract hex tiles, named Hex; it can also produce a valid geometry for any such produced Hex, and it can do other nifty transformation on the fly, like computing the actual equilateral triangles of the hex in question, all kinds of helpers really. – (in principle this particular example violates the guideline of having a 1->N hierarchy stored in the memory, but still represents one of the many variants of the same basic system, where we have at least a psychological connection between the elements and the parent structure they all belong to)

  3. Hex objects are very simple immutable value types exposing two variables x, y, representing two coordinates in the cubic format, which are used mostly for passing as arguments, but can be stored just like Vectors, yet hold no other data; BUT, and this is where things get interesting, once we are past the threshold from the plain euclidean to cubic-hex realm (thanks to HexGrid) we can do things inside this class, because x, y is all we need to infer all of the neighbors and all of the useful properties of the grid; because we don’t necessarily care about any payload data (that’s deferred to somewhere else), here we can do all kinds of work like finding a neighbor that many spaces away in that direction, or taking a distance and making a ring, rotating a bunch of hexes, subtracting two hex sets, and so on, just about anything that’s not euclidean, but lives in the hex space.

  4. Hex objects do not know their globally-set style, radius, and offset, but I can compromise with this – however, now I need something that is actually presentable for the user, because cubic formats tend to be not as palatable;

  5. Enter HexOffset, another immutable value type that can be explicitly cast from Hex and back, exposing two variables p, q – that provides an opaque conversion between the cubic-hex system and the usual row-column (aka ‘hex offset’) system that is human-readable (and a part of every game using hex grids);

Here we have a dumb thing, that’s practically a glaring hole in an otherwise perfect design. Imagine a typical pointy-top hex grid, think Civilization. Now every odd row is horizontally shifted so that it densely packs between the even rows, but we have to be deterministic about this if we are to have a finite grid, so imagine a global property that declares whether this shift goes to the left or to the right of the zeroth hex, because both options are equally valid.

This property is normally called ‘odd shifting’. But HexGrid doesn’t care about this, nor does Hex, the math works the same, the only thing where this matters is the actual humanized notation we get from HexOffset. So whenever you cast Hex into HexOffset, you need to specify, every time, for every hex, that you want this ‘odd shifting’ on or off. Which is just silly, because you want this option either on or off, for all of the hexes belonging to a single grid, but of course, there is no relationship between HexOffset and HexGrid, because there is no relationship between Hex and HexGrid to begin with.

And there is no way I can sort out this minor inconvenience without toppling the entire design on its head. Which annoys me to no end. And don’t get me wrong: I usually don’t mind rewriting the whole thing if I can see a clean path ahead. I can see none in this case.

I have another example that is slightly more complicated, but I don’t want to drown everybody with my actual projects.
Click this only if you can spare more time.

It was a rather big library API tailored specifically for manual and procedural mesh modelling. Again there were reasons to have elements as uniform and as simple as possible, and I’ve done some beautiful custom hash generators guaranteed to be unique in O(1), and utilized a dictionary to map potentially millions of faces. Etcetera the library is incredibly complex and fast, considering its purpose, where we have the nodes (my own vertex abstracts), faces, and edges belong to a mesh (teh parent), simultaneously, and then having multiple meshes as container-models for all this data, Blender-like. Obviously these are three separate collections internally, where I use a custom-designed memory-efficient ordered Hash Lists (a weird thing to have, C# normally has no orderable hash sets for a good reason) to keep an arbitrary combination of these elements to provide a unified selection etcetera. And I’ve managed to solve the parent/child domain problem by implementing extensions, which have ways to internally resolve the faces’ owners thanks to dictionaries already in place. On the other hand, edges are value types and follow a different set of rules. And this really works. But still I’m not liking this face->mesh solution as it is dirty in places, overly expansive or impossible to cheaply implement in other projects or areas, and contains highly sensitive code, some of which took me weeks to design and implement, or just to debug. I have definitely learned a lot about C#, perhaps the most in the last 15 years, and some of the core classes are literal poetry.

Frankly I’ve grown to hate it even though it’s really a piece of work to be proud of. But there it is again, the problem, as if the language itself is incapable of letting you specify a clear demarcation of context while also having only essential data neatly packed in memory. It seems you can’t have a cake and eat it too. I’ve rewrote the core classes six times in an attempt to find a better perspective, refactored the whole thing in a more practical way that doesn’t demolish the basic principles, nor any extended APIs that sat on top of the engine (I’ve made a bash-like language on top of it, but that’s a different topic), but it didn’t get me where I wanted to be.

Examples such as this one really made me think hard about this, and ultimately I’m willing to openly admit that I can’t find a satisfying solution to the problem.

On the ground level, this is what I would like very much as a language feature, from all my adventures so far.
Pay special attention to [SharedRef] attribute (that sadly cannot/doesn’t exist).

public class LegoModel {

  List<LegoBrick> _legos = new List<LegoBrick>();
  public int KitId { get; private set; }

  public LegoModel(int kitId) {
    KitId = kitId;
  }

  public void AddBrick(int brickId, Vector3 position = Vector3.zero, Quaternion rotation = Quaternion.identity) {
    var brick = BrickFactory.Create(KitId, brickId, position, rotation);
    _legos.Add(brick);
  }

}

public struct LegoBrick {

  [SharedRef] private LegoModel _model;
  public LegoModel Model => _model;

  // legit payload
  public Vector3 Position { get; private set; }
  public Quaternion Rotation { get; private set; }

  internal LegoBrick(LegoModel model, Vector3 position, Quaternion rotation) {
    _model = model;
    Position = position;
    Rotation = rotation;
  }

}

And well, this is the first time that I’ve actually spelled out clearly what I need.
And maybe I can still figure this out… hmm

I don’t know, perhaps a well-deliberated combination of your proposals might do the trick.
I’ll think about this a bit more tomorrow, it’s getting late where I am.

Thanks for the comments!
I recognize all of the techniques you’ve mentioned and this really helps me narrow down potential solutions.
If I end up doing something of value and ease of use, I’ll definitely share it.

Thanks for the broad answer, but I intentionally asked a broad question fully expecting I won’t get a one-size-fits-all answer. It’s more a need to have an open discussion on various approaches, because the setup IS, in my mind at least, pretty basic, yet one of the most complicated things I’ve come across, and not because it is complicated, but likely because of the particular constraints I’ve set in place, that are non-negotiable.

:slight_smile: (just so you don’t miss it, I’ve answered to your post midway above, this I’m just highlighting on a separate note)

Given the lego example, is there a situation where you would find yourself accessing bricks without knowing the model ID they’re in somewhere up the callstack? Most of the time you would store bricks inside a model or in some system instance that is model-local, so accessing them provides the model ID automatically.

Most of the time it’s a trade-off like:
Say you want to have physics between multiple LegoModel instances and having them support falling apart etc;
Option A) If you want to use a global acceleration structure, you’ll need a LegoWorldPhysics instance where you put in LegoBrickPhyics, where each will have to store some identifying info (pointer, model + brick ID); otherwise you do not know how to map these copies in the acceleration structure to the originals to write back changes. You can’t get the context from the LegoWorldPhysics doing it’s thing, since it has no knowledge of LegoModels.
Option B) Brute force it the mostly O(n^2) style - in this case you do not need to store a pointer/ID since you know it from context, working directly on the LegoModels. (you could still do some broadphase checks on model bounds of course)

For your Lego thingy, you can use something like that:

public struct LegoBrick1
{
    public LegoModel Model { get { return _model; } }
    static public LegoModel _model;
  
    public LegoBrick Brick { get { return _brick; } }
    private LegoBrick _brick;
}

public struct LegoBrick2
{
    public LegoModel Model { get { return _model; } }
    static public LegoModel _model;
  
    public LegoBrick Brick { get { return _brick; } }
    private LegoBrick _brick;
}

Here your ‘SharedRef’ attribute is just a static field.

You have 2 types of bricks, each type having access to its own model.
I haven’t tried it in a real test-program, but I suspect you’ll need some reflection tricks here and there to make it working.
Having an interface to get the brick and model would probably help a lot but the problem is that casting a struct to an interface will generate garbage because of boxing.
You’ll probably need some sort of factory (probably with a pool) to create the bricks.

Honestly, it’s a bit like trying to have the features of a class in a struct… not sure it’s really good.
I’ve seen that there’s a new thing, ‘ref struct’, maybe it can help.
I haven’t yet read these articles, but they may be provide some interesting things about structs:

https://www.devsanon.com/c/c-7-2-introducing-ref-struct/

That’s where I see that we usually know more about classes than structs.

There’s a lot of text here, but as far as I can understand it, a very short version of the problem is:

  • There’s parents and children, where there’s a 1 parent ↔ many children relationship
  • To get some information about a specific child, you need the parent
  • There’s so many children that having a pointer to the parent in the child costs too much memory.

In that case the obvious solution is that the things that need to work with the children also get a reference to the parent. If the children are just data, then the functionality can’t live on them, so put it in static methods or in the parent itself.

Or am I missing something? I’m pretty sure that with the design you’re talking about, passing only the children somewhere is probably not the right thing to do.

1 Like

Exactly, that was a solution for my example #2 (hidden behind the spoiler button). I had an overarching model for the contained parent-child relationship, and thus children had something to call AND multiple models weren’t prohibited. This was great but a) this really only works with reference types, and b) just ended being relatively cumbersome.

Why? Well function calls aren’t that happy performance wise, passing such things through getters tends to be cached better, but you have no control, whether inline or not, I’m guessing calls are expensive because of having to build a context frame on the stack, regardless of how simple its implementation was. So I had to build a caching logic and directly-accessed internal dictionaries, and the thing bloated real fast.

With that said, there is again a clear case of trade off between performance and memory consumption. From that angle, Gladyon’s static approach is the fastest possible design. You know, the issue is that the deeper you go with this, the more true engineering obstacles you stumble upon. It’s truly like the rocket science.

“Why don’t we just send a rocket into space?”
“Yeah let’s”

Here I’m referring to @Baste as well
I’m not necessarily saying that this simple thing cannot be done, but that it cannot be done to satisfy the 3 conditions: 1) ease of use/implementation, 2) maintenance (it has to ‘look’ relatively benign, not like rocket science), 3) struct support (where this matters the most, I don’t mind having boilerplate patterns in my high level logic, but doing this for structs is like having wrenches in a gearbox).

I completely agree with you, and yes you got it well, sorry about the amount of text.
Regarding the bolded part, yes, even I would fall in a trap of sensing a code smell if I read this somewhere.
I get what you mean.

Look at the HexGrid example in my second post, if you have time – maybe that one can be solved differently, but still illustrates the point of having access to parent. It’s not always about having functionality living in data objects, I appreciate what you’re saying, sometimes it has to do with (opaquely) sharing properties that belong to the parent only because of how these things psychologically belong to a same structure, yet traditional OO implementation prohibits this simple connection to take place.

To solve this minor inconvenience (the lack of knowing what the general setting was in the output object) I cannot simply do one minor thing to improve it, but I need to step back and think of another design (after two of them already in case of HexGrid), which is what really bothers me. I would really like to come up with a breeze solution that compromises reasonably and let’s me move on with life. Currently that compromise is really a hard line between being extremely memory-intensive or being too cumbersome to work with. And so you end up with a ‘reasonable’ compromise of having a solution riddled with small inconveniences, needlessly deferring some responsibilities to client code (like keeping track of global settings and having to reinsert them throughout data production, yuck). That’s like having a big fly in the eye for any software engineer.

Sure, you can loosen up the constraints and be like “well it’s good enough” but then 1) I’m admitting that OO has severe limitations and that I’m ok with subpar solutions that produce annoying API inconveniences, 2) I learn nothing. Hence this discussion. Thanks for dropping by btw!

The true issue that I can readily identify is that data objects shouldn’t really be separate entities (it’s the parent that should act as a collection internally, without having to fight these thorny vines and arbitrary chasms between two worlds), but there is no other way to manage data records flexibly in C#. I truly need only records on a clean slate of memory block, and that appears structural in design, and would have to be implemented low-level in an ‘unsafe’ code because of pointers. Btw I’ve done this as well (for the voxel thing I mentioned earlier), and it works, and is possibly the fastest and the best suited solution, but again violates the principles of keeping it simple and approachable. Also ‘unsafe’.

Maybe I’m just a C programmer in a high-level disguise, I don’t know. But oh boy C# can be annoying at times.

All of this I’m actually trying to build since last night. We had exactly the same basic idea. I’m yet to see what the problems are. Structs are complicated because of no inheritance, and yes casting bothers me, but the good thing is that you need reflection only once per each separate model, after that you should be able to type check with ‘is’ and that shouldn’t produce garbage.

True. Painful but true.

From my preliminary glance, ref struct doesn’t do anything useful for me, in general. (‘in’ does, whoa, that’s just great for immutable structs in function calls.) But now I’m second-guessing that opinion, I think I’ll give it another round of research. They didn’t implement that for nothing or so I’m hoping.

Comments are great, thanks guys for your time, I really appreciate it.

Okay. So I’ve figured out something thanks to your input, and this is what I ended up with as an early proof of concept.
Gladyon was right in that it’s probably hard to escape casting. I managed to overcome this in a very surprising way, that none of us actually thought about doing.

It all started with that remark of mine, that the whole thing strongly resembles a collection, and ultimately I’ve managed to end up with a generic class that wraps up the core logic and lets the client define the child objects however it sees fit.

It is the genericness that enables having (virtually) unlimited parent/child models, and it’s the child’s type that is used to figure out the actual correspondence. However this does come with a few problems. Typically, generic classes are really clumsy when it comes to instantiating any generic types within them because they require the types to include a parameterless ctor yadda yadda. Structs come like that out of box, but for the classes you have to implement an explicit parameterless ctor, and then it becomes slightly messy from that point on.

I have originally circumvented this by using an interface which would guarantee the type has an instantiator method with a proper signature, and this was also cool in that it also established a contract that the child object is compatible with the primary class, and so the client code would have to also implement this interface.

However, introduction of an interface also introduced a need for casting back and forth in few places, but I could live with in until it turned out that I was unable to actually store the concrete instances, requiring me to cast on access, and not just on instantiation. Nope!

So I’ve moved onto another design which doesn’t constrain the generic type in any way, and instead uses duck typing to resolve child compatibility on the fly. Which turned out to be very nimble, and seriously awesome. No casting involved! Both structs and classes are covered, and treated absolutely the same from the client code perspective.

There is just one pitfall, that I can live with: The core class cannot be inherited (I’ve tried to do it, but let’s just say it’s very complicated because it’s generic and, in fact, not worth doing. Though, tbh it was really painful only because I had a generic constraint, I will probably try this again now that it’s much cleaner.). So it’s sealed. Fine.

Enough talking.

Here’s a simple value type, doing nothing new in particular, but just to show that it doesn’t break the usual way of doing things in any way.

public struct SimpleValueChild {

  // payload
  public int Identity { get; private set; }

  // ctor
  public SimpleValueChild(int index) {
    Identity = index;
  }

  // creation method
  SimpleValueChild Make(int index) => new SimpleValueChild(index);

  // debug
  public override string ToString() => $"[Id = {Identity}]";

}

Notice the function Make(int) – similar to how Next() and Current() work in enumerators, I basically look for this method via reflection and it has to have this exact signature and be privately declared. This is done just once per model. It turned out very cute and unobtrusive, if I may say so myself.

Here’s a properly extended value type, that actually uses the shared reference to get to the parent properties. (Don’t mind the idiotic names.)

public struct SmartValueChild {

  // payload
  public int Identity { get; private set; }

  // parent resolver
  Parent<SmartValueChild> myParent => Parent<SmartValueChild>.SharedRef;

  // parent knowledge
  public int GlobalArea { get {
    var parent = myParent;
    return (parent.Width * parent.Height);
  } }

  public bool WellIntended => myParent.WellIntentions;

  // ctor
  public SmartValueChild(int index) {
    Identity = index;
  }

  // creation method
  SmartValueChild Make(int index) => new SmartValueChild(index);

  // debug
  public override string ToString() => $"[Id = {Identity}, GlobalArea = {GlobalArea}, WellIntended = {WellIntended}]";

}

Because the implementation is generic, we can resolve the parent right through the static field! It simply can’t be any faster than that. I’m still caching that reference to not do it twice in that getter which is probably not really required.

Finally, here’s an ordinary reference type, for comparison. Basically there are no differences in the way it works which is very useful, as there are no weird mnemonics to keep in mind and no dev overhead in implementing this when needed.

public class RefTypeChild {
  // payload
  public int Identity { get; private set; }

  // parent resolver
  Parent<RefTypeChild> myParent => Parent<RefTypeChild>.SharedRef;

  // parent knowledge
  public int GlobalArea { get {
    var parent = myParent;
    return (parent.Width * parent.Height);
  } }

  // ctor
  public RefTypeChild(int index) {
    Identity = index;
  }

  // creation method
  RefTypeChild Make(int index) => new RefTypeChild(index);

  // debug
  public override string ToString() => $"[Id = {Identity}, GlobalArea = {GlobalArea}]";

}

And the core class.

using System;
using System.Reflection;
using System.Collections.Generic;

namespace ParentChildCoupling {

  public class ChildInUseException : ArgumentException {

    public ChildInUseException() : base("Child type already in use.") {}
    public ChildInUseException(string message) : base(message) {}

  }

  sealed public class Parent<T> {

    static Parent<T> _self;
    static public Parent<T> SharedRef => _self;

    // global properties
    public int Width { get; private set; }
    public int Height { get; private set; }
    public bool WellIntentions { get; set; }

    // factory delegate
    delegate T ChildMaker(int index);

    // actual data collection
    List<T> _collection;

    // constructor
    public Parent(int children, int width, int height) {
      if(_self != null) throw new ChildInUseException();
      _self = this;

      Width = width;
      Height = height;

      _collection = new List<T>(children);
      ChildMaker makefunc = getMakeFunc(typeof(T));
      instantiateChildren(children, makefunc);
    }

    // basic accessor
    public T this[int index] => _collection[index];

    // duck typing
    ChildMaker getMakeFunc(Type type) {
      var info = type.GetMethod(
        "Make", BindingFlags.NonPublic | BindingFlags.Instance,
        null, CallingConventions.Any, new Type[] { typeof(int) }, null
      );

      if(info == null)
        throw new NotSupportedException("Child type doesn't contain Make method.");
      
      if(info.ReturnType != type)
        throw new NotSupportedException("Make method return type doesn't match the object type.");

      return (ChildMaker)Delegate.CreateDelegate(typeof(ChildMaker), null, info);
    }

    // instantiaton
    void instantiateChildren(int children, ChildMaker makefunc) {
      for(int i = 0; i < children; i++) _collection.Add(makefunc(i));
    }

  }

}

Here’s a very simple test, I’m yet to benchmark this when I flesh it out a bit more.

using UnityEngine;
using ParentChildCoupling;

[ExecuteInEditMode]
public class ParentChildTest : MonoBehaviour {

  Parent<SimpleValueChild> _hier1;
  Parent<SmartValueChild> _hier2;
  Parent<RefTypeChild> _hier3;

  void OnEnable() {
    _hier1 = new Parent<SimpleValueChild>(100, 30, 10);
    _hier2 = new Parent<SmartValueChild>(100, 10, 20);
    _hier2.WellIntentions = true;
    _hier3 = new Parent<RefTypeChild>(50, 20, 20);
    Debug.Log(_hier1[15]);
    Debug.Log(_hier2[15]);
    Debug.Log(_hier3[15]);
  }

}

It’s still just a proof of concept but I am very happy with it. Your brainstorm really helped, even though this particular approach wasn’t on the list. What I’ll probably do is to move out all properties and data setters into custom property classes or something similar, and leave only the core motor behind the scenes.

Tell me what you think and thanks a bunch! (I’ve finally nailed this down yay)

edit:
oh, and the results are

[Id = 15]
[Id = 15, GlobalArea = 200, WellIntended = True]
[Id = 15, GlobalArea = 400]

It seems a bit strange to have to have one type for each unique hierarchy. From your hex example earlier, now you have to declare a child type every time you need a hex grid that can be alive at the same time as the old one.

Or in other words; you’ve made the parent a singleton.

So that seems strange - the code is written to be super-generic, so it seems like it wants to be a library-like thing that supports a bunch of different cases. Otherwise it wouldn’t be Parent, it’d be MyCivCloneHexTile. And in the super-generic, making the assumption “there’s only ever one hierarchy of a certain type” seems to be overly bold.

If you’re making a “build lego” game, then you don’t want to have bricks belong to a specific model. The power of legos is that the bricks are reusable. Didn’t you see the lego movie??? :wink:

Also this solution ends up with the child not being able to access any data that’s in the parent. In order to put data in the parent, you have to derive it; ParentWithData : Parent. But ChildType doesn’t know that it’s parent is ParentWithData, it only knows that it’s Parent, so you’re going to have to cast.

I don’t think you’re there yet. There’s always going to be child-parent relationships in a code base, but I don’t think there’s a one-size-fits-all abstraction to wrap them with.

No no, of course, you’re right about the most of the glaring issues with it right now.
Yes the parent is a singleton, but generic singletons offer at least some flexibility, they add a dimension to something that is traditionally only punctual. I don’t intend to use this for hundreds of different models, and very likely the base child will be hidden under the hood anyway, therefore it’s just a trick to be able to reuse the static field without having to introduce a hash map.

But this is only really a proof of concept, and besides it’s main purpose isn’t to be a high level collection to be used off the shelf for some game out there, but to sort out some general pathological issues I’ve been discovering in very specific use cases over the years. This is more akin to how enumerators work, therefore almost too low level for general usage, than it is to typical game APIs.

I’m a programmer first and foremost, so my need for this is a bit more technical, and believe me it doesn’t still solve all of it, I’m yet to figure out a more robust version of it. For example it doesn’t solve my HexGrid problem above, at least not in this incarnation. But it’s a worthy proof of concept, so I’ll get there if I push a bit more.

The idea, at this moment at least, is not for this to become a general purpose class or even a first-degree collection. More of a simple but sturdy industrial-size tie-clip to hold up the damn OO curtain while I’m trying to do something much more serious in the foreground. Some sort of under-the-hood memory management logistics pattern, let’s say. The result of which, or at least I’m hopeful it’ll turn out like that, would be to let me do a better placement and structuring of the general API of some my solutions, while at the same I don’t need to compromise long-term maintainability of the whole thing, and can keep certain elements logically separated and without sacrificing speed or wasting memory. That sort of thing.

If I do it, it’s a win-win-win for me. I have a backlog of things waiting to be refactored if I manage to pull this off.

Nah that was just a quick example to illustrate the point. It has nothing to do with high level objects such as legos. I’d do that completely differently. Now I realize it was a silly example, but it got the point across very quickly, I had to write some code to keep my blabbering short.

This I didn’t understand. I thought it was obvious from the code. A child is able to access anything public from its corresponding parent. Derivation is impossible because it’s sealed anyway.

Btw I tried doing a dictionary, it’s actually necessary if I want it to not be sealed (because different derivations are their own separate parent/child models, so the static field isn’t enough any more), and it works. I just decided to strip the codebase down to a bare minimum when I changed the design from constrained to unconstrained generics until I’m able to get a clear picture of what’s going on and where I’m heading. We’ll see. I’ve been trying to figure this one out for quite some time, well, mostly because it wasn’t immediately clear and I had better things to do, but still, it wasn’t easy either. I expect I’ll need several more iterations, maybe even another redesign.

I guess what you’re doing here is to use the children’s specific type as a tag in order to be able to look up data using that tag.

That’s starting to sound data oriented! The “trick” you’re using to prevent the tag from taking up space is to go “hey, a type is already a tag, and the type has to bee there, so I’ll just repurpose it as a tag”.

For anyone reading the code, I think a Dictionary<Type, Data> is a more readable way of doing that then the parent singleton type. The singleton is the same thing, but the dictionary is very explicit about what’s happening being “look up data using the type as a key”.

I still think the right way to go about this is probably to refactor the code so you always have the relevant parent at hand when you’re messing with the children, rather than looking it up all the time. That severely reduces the amount of magic going on, and I don’t think it’s going to lead to very much bloat. But the tag solution is valid as well.

Regarding this, well, I think you’re considering generics to be something more than they really are. The whole thing is a legit class name, only modular (and nestable) from the compiler standpoint. I see no need for MyCivCloneHexTile. If you imagine a game like Civ, then you truly really need just one HexGrid model, thus Parent would suffice.

I don’t know how you normally work with these things, but flat hierarchies such as this are just memory records, similar to how arrays operate. In this case however, when I say model, that means I don’t intend to flush it or dispose it anytime soon (which is important because you want to allocate once, and reuse that memory block indefinitely), but most often than not, it’s a central data distributor, redistributing global data to local units of topology, and a central data model, serving other parts of the code with the up-to-date values including topological relationships. It could be tiles, that’s like super common in games, so I take that as an example, but it could be something else that takes a single block of memory and is enumerated as records.

But the need for two separate models might arise if you had a game with entire Civ-like planets, for example, and while the user is on some planet X, you’d like to be able to show him a map of a different planet without committing to a full thing in memory. So you’d use a surrogate model, for example Parent. That kind of thing.

Obviously the naming is super early, it wouldn’t be really called like that either. But I hope you’re seeing that you’re definitely not supposed to have a vast amount of such models. On the contrary. But I don’t like to be artificially limited as well, so that’s the explanation why generics are, well, okay.

What you’re saying does make sense. I’ll look into it.

Although ‘having’ the relevant parent reference still requires me to store it somewhere, right?
The static solved not having to call a hash map lookup. So it’s very cheap, almost as if it was stored locally, if it weren’t for the reference to the Parent class itself (which also comes with the call, so it’s unavoidable).

If I store it, I’d lose any benefit from doing this. It’s 8 bytes dangling on each child instance for no reason. That’s more than the actual payload in this demo, which is only 4 bytes. The only thing I can think of would be to derive the child classes from a base one that hides these innards from view. But ok, I’ll think about this as well. Thanks.

I’m still learning what the hell I’ve made. This is some sort of inverse aggregation / forward propagation pattern. Well I’m not that much into patterns. If someone can come up with some sort of a name that nails it down, please let me know.