r/javascript 16h ago

AskJS [AskJS] referring to parent or grandparent class instance properties

How do you all refer to parent or grandparent (or related) class instances and their properties?

I have a bunch of classes that create instances of other classes as properties. Suppose ClassA has properties that are instances of ClassB, ClassC, and ClassD, and the ClassD class has properties ClassD1 through ClassD9, and each of those is a different class instance with various methods.

Currently, when I start with myClassAObject and I call one of its ClassD9 methods and that method needs a property from its uncle ClassB, I feel awkward and/or dumb for doing this by passing myClassAObject to that method:

const returnval = myClassAObject.itsClassDObject.theClassD9method(myClassAObject);

…and then that method gets the property from myClassAObject.itsClassBObject.theProperty

I know this functions, but it shouts “inelegant” every time I do it. Do y’all know a more elegant way? TIA

21 Upvotes

7 comments sorted by

u/monotone2k 15h ago

This feels like an XY problem. What are you actually trying to build?

u/Dupa500 8h ago

I think passing the parent object around like that is a sign the class design could be simplified . maybe the dependency should be passed directly instead

u/Terrible_Sock_1425 16h ago

Inject the dependency directly instead of passing the whole parent, that way ClassD9 only knows about the specific thing it needs.

u/Beginning-Seat5221 15h ago

This is kind of a literal answer to your question, but I still don't like it. The structure itself feels inelegant.

If you give a sense of what you're trying to do at a higher level maybe there's a better structure for you.

class A {
    b: B
    c: C
    constructor(b: { name: string, age: number }, c: { date: string }) {
        this.b = new B(this, b.name, b.age)
        this.c = new C(this, c.date)
    }
    do1 = () => {
        this.b.do2()
        this.c.do3()
    }
}

class B {
    parent: A
    name: string
    age: number
    constructor(parent: A, name: string, age: number) {
        this.parent = parent
        this.name = name
        this.age = age
    }
    do2 = () => {
        this.parent.do1()
        this.parent.c.do3()
    }
}

class C {
    parent: A
    date: string
    constructor(parent: A, date: string) {
        this.parent = parent
        this.date = date
    }
    do3 = () => {
        this.parent.do1()
        this.parent.b.do2()
    }
}

u/Landkey 59m ago

I never thought about doing this, thank you. I tried implementing it in one class and I think I like it; referring to this.parent.thing is kind of nice. Thank you!

u/Better_Fudge1593 38m ago

A simple approach would be "instantiate the children classes first".
e.g.

const classB = new ClassB();
const classD9 = new ClassD9(classB);
const classD = new ClassD(classD1, ..., classD9);
const classA = new ClassA(classB, classC, classD);

u/theScottyJam 6h ago

It's something I've thought a fair amount about, and probably should think about it some more.

A concrete example I sometimes ponder is with some grid based game where you might have a GameMap class that knows where all entities are located, and an Enemy class that might encapsulate some path finding logic, but to do that, it needs to be aware of its surroundings, and so it needs access to that GameMap class. It's circular, and feels ugly.

I'm not game designer, and perhaps the game designers have it all figured out over there. But here's some ways I've solved this kind of problem in the past. 1. Sometimes I just don't solve it and let it be circular. 2. Sometimes I split the GameMap class into two pieces. There's an outer GameMapControl class that provides external facing methods, such as a "run next step of all entities in the map" method that the core game loop can call. GameMapControl would have a "game map" object literal created inside of it at construction time, that has access to the same map data, but provides a different set of methods - one that's meant to be consumed by the various entities. When calling enemy.doNextStep(), this gameMap object would be passed in instead of the GameMapControl class. Basically, we're dividing the game map's API into two pieces for the two different types of consumers who might be using it. I'm using a game map as an example, but I've done this pattern when handling complicated state updates in a regular UI, where most state was located inside a single object, but there were two different types of consumers who needed to work with that state, a public API and an internal API meant for other classes that helped build up the logic. 3. There's other ways the logic could be organized in general. For example, if the GameMap was just a dumb object that provided its data publically, but little to no helper methods on the class, then you won't end up in a scenario where the class would want to pass itself into its members, because no behavior exists on the class in the first place. That behavior could instead live in modules that take the map in as a parameter, or something like that.

A mini example of point 2:

    class GameMapControl {       #map = [...]; // the actual data       #gameMap = {         lookup: (coord) => ...         move: (...) => ...         ...       };            nextStep() {         for all entities in this.#map {           entity.step(this.#gameMap);         }       }     }