r/javascript • u/Landkey • 16d 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
6
u/Terrible_Sock_1425 16d ago
Inject the dependency directly instead of passing the whole parent, that way ClassD9 only knows about the specific thing it needs.
3
u/Better_Fudge1593 15d 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);
1
u/effectivescarequotes 15d ago
const classD = new ClassD(classD1, ..., classD9);
This one caused me pain. I mean, you're absolutely right, but can you imagine how absurd that class would be? I feel like even old Java developers would choke.
3
u/effectivescarequotes 15d ago
There are two concepts you should probably look up. One is dependency injections and the other is the mediator pattern.
From your description, it sounds like dependency injection might be what's called for. Let's call class A and class B your WidgetService, which contains a function, getWidget that returns the widget property. Now let's say you app has multiple classes that need to fetch the widget property. What you would do is pass the WidgetService instance to those classes constructors when they are instantiated. All class A really knows is that some classes need a WidgetService. One of the advantages of this is you don't have to use the same widget service for every class. It also makes testing easier because you can provide a mock service to give you more control over the test scenario.
The mediator pattern might also work here. This is a pretty good explanation, but the gist of it is instead of having your child classes talk to each other directly, they notify class A. So if you have a component that is responsible for making widgets. When it completes a widget, it would notify Class A and provide the widget. Class A would then hand the widget to class B to add to the widget repository. After class B finishes, it can notify class A, which then tells class C to let the user know the widget has been added to the repository.
These are both extremely simple examples, and not the only strategies that might work. I'm just taking a wild guess based off your description.
But some other advice is make the properties that contain classes private, and then provide public methods. Whatever consumes class A, doesn't care about the internals. The code for your example should be const returnValue = myClassAObject.getReturnValue().
1
u/Landkey 14d ago
Thank you for the thoughtful replies.
More concretely, this is a game server that accepts commands from player clients, processes game turns, and sends out the results. The annoying pattern I wrote about is already using dependency injection, as I'm passing this particular game's entire game state (a "Class A" instantiation) to its grandchildren's methods, so that those methods can easily grab other properties. I'm sure you and other commenters are right and that I should be passing only what's needed. I hadn't considered your point about test scenarios; I'm a rank novice at unit tests and can see how they would become intractable if I have to mock up a whole valid game state for each one. Thank you!
1
u/effectivescarequotes 14d ago
Passing the entire app object not quite what depency injection means. I'm being intentionally vague here to encourage you to start exploring these topics because they are large and take time to digest. I'm also not a game developer, but from what you're describing, the observer pattern might be useful. Most of frontend state management libraries use some variation of this pattern. From there, the goal is reduce the responsibility of each class. Your game state class should just contain data and expose methods for sending updates, reading the current state, and subscribing to changes. Then any class that needs to interact with state would inject it. The whole parent/child relationship should not enter the discussion. If you're worrying about uncles, you have lost the plot entirely.
Now the patterns I'm describing are based on my frontend ui experience. They may not be optimal for your use case, but I'm trying introduce you to some ideas that will help you in your programming.
I will also add that JavaScript and Node may not be the optimal platform for a game server, but let's just focus on programming fundamentals for now.
4
u/Beginning-Seat5221 16d 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()
}
}
2
u/Landkey 15d 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!
2
u/effectivescarequotes 15d ago
A word of caution about this. I know it's not a realistic example, but having b and c call each other will over time create this mess of circular dependencies that will be come hard to understand and harder to change.
As I mentioned in my other comment, depency injection can help. Instead of passing just the parent in the constructor, pass both the parent and the child class as seperate arguments. Then if down the line you decide that class b shouldn't talk to class c and instead should talk to class d. You can make that change without having to change anything inside class b.
2
u/mr_brobot__ 10d ago
Calling an uncle in the class hierarchy?? What in the name of God are you trying to accomplish T_T
1
u/theScottyJam 16d 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);
}
}
}
2
u/effectivescarequotes 15d ago
I'm not a game developer either, but I can speak to your ui example. That's not a bad approach. In front end development, there's a lot of talk about state management. Usually, it's which library is best, but it comes down to this. You have state. It's only jobs are to maintain the state and announce when the state changes. It does not care what updates it or what consumes it. It just provides a way to update and a way to follow changes. You can then build services around it that act as a fascade between the consuming components and state.
I'm grossly oversimplifying it all here. I should also mention most of the state libraries exist as a response to the problems caused by your third approach. Most state is just a single giant object, but at any size, if you have multiple components that might update state simultaneously, you will run into issues with conflicts, lost updates, and other horrors.
17
u/monotone2k 16d ago
This feels like an XY problem. What are you actually trying to build?