Need ideas on restructuring my codebase

I’m currently building a game (an RPG) in javascript, and the codebase has grown rather big and currently feels like a bit of a spider’s nest. I’ve never built anything of this size before so I’ve never had to deal with this type of problem, and I have a constant feeling that I should do some re-arranging but I feel that my knowledge around OOP is lacking and I need help figuring out if there’s a better way to do things. I’m not even sure I can call it OOP, but it sure isn’t purely functional either.

The codebase is currently based around a single object, with lots of child objects as properties. I keep passing the parent down the chain because I need to call methods from it. A lot of child objects need to be able to access methods and values from siblings as well.

Here’s a mock example of what the structure looks like. I’ve simplified it a lot for the example’s sake:

class Game {
  constructor (content) {
    this.content = content
    this.movement = new Movement(this)
    this.character = new Character(this)
  }

  //... + A lot of helper methods on the game object
}

class Movement {
  constructor (game) {
    this._game = game
  }

  openChest = (chest) => {
    chest.content.forEach(itemId => this._game.character.pickupItem(itemId))
  }

  // ... + Bunch of methods and values here, a lot of them use this._game to call methods
}

class Character {
  constructor (game) {
    this._game = game
    this.inventory = []
  }

  pickupItem = (id) => {
    const props = _game.getEntity('items', id)
    this.inventory.push(new Item(props))
  }

  // ... + Bunch of methods and values here, a lot of them use this._game to call methods
}

class Item {
  constructor (props) {
    Object.assign(this, props)

    this.equipped = false
  }

  // ... + Bunch of methods and values here
}

As you can see, the game object is like an umbrella, and the children reference it and other siblings constantly. But it all feels messy, because it feels dirty to pass down the game object to every new object that is created that need it.

  • Is there a better way to do this?
  • How can I re-organize my code in a better way?
  • Should I be doing it like this, or should I separate concerns more and import already instantiated objects instead?

For example:

  • What if I have a use case where I need to use a method from the top level of the game object, but I need it in an item that is in the array in game.character.inventory. Is it “OK” to pass the game object to every item? Does it have performance implications?

PS. Not sure if it matters, but I’m using MobX quite heavily and game is a MobX store, which is how I got started on this path.

I personally prefer functional programming.

There’s several approaches you should consider, and you can do all or some of them:

  • Make the instance of Game a global. Seriously. It’s not the most ideal solution, but you’re making an app, not a reusable library. If you prefer, have a global game() helper that returns the game object, which makes it something you can abstract out for testing.

  • Be sure to use modules to separate concerns. Err on the side of more modules than fewer, since you can always create “super-modules” that export other modules.

  • Only pass the information that a function needs. If something only needs the inventory, for example, pass only the inventory and not a Game object.

  • Try to write as much in pure functions (functions that have no side effects and don’t rely on any) as possible. Then if you need to, create an impure layer that makes use of your pure functions, such as a function that passes random numbers (here the impure layer) to a function that then behaves deterministically when given a number (the pure layer). The fact that you’re using MobX means you shouldn’t even need to manage state in your impure layer, just things like randomness and I/O such as player input.

  • Write it in TypeScript. You’ll find the type information not only prevents bugs, you unlock other powerful libraries like tsyringe, which can help you with a global Game instance without actually making it a global.

One way is to extend the parent class, and the child class can have access to the methods of the parent class, the elements of the constructor to the parent class can be ‘transferred’ up to it using the super keyword

class Game {
  constructor (content) {
    this.content = content
  }

  gamePlay(){
    console.log(`Parent Class ${this.content}`)
  }
}

class Movement extends Game{
  constructor (content) {
    super(content)
  }

  openChest(){
    console.log(`Child Class ${this.content}`)
  }

}
const move= new Movement('Hello');

console.log(move.openChest()) // Child Class Hello
console.log(move.gamePlay()) // Parent Class Hello

This is all JavaScript syntactic sugar of course as it is analogous to using the prototype method of object constructors.


Thanks for the tips!

I’ve done about 50% of the things you mention, but it could be done better. I initially only passed the stuff I needed, but I ended up needing more as the children grew. Passing the whole game object at least made it so that I didn’t have to rewrite a bunch of code as soon as I needed to access something else.

I think making it global, or maybe splitting up the game object into several modules and instantiating them in the module export might be an option. That way I’ll be able to import them as needed. The only problem there is that a lot of the children can’t be instantiated until the game is started (which is not when the files are loaded).

Regarding typescript: Some day maybe, but I’ve avoided it because I don’t really like the idea. I prefer keeping it “vanilla” JS, albeit ES6.

Regarding pure functions and wrapping them: I’ve actually started doing this quite a lot, using static class methods, which has been a big improvement.

I appreciate your feedback!

Right, but in my case, most classes are only used to instantiate a single object, and they’re not really “children” as in a sub-class sense. They share no similarities with their parent. The reason I add them as properties to the parent object is that I need the child objects to be able to access their siblings.

A class that extends another class will inherit the parent’s methods, but not the data on a specific instantiated class, no?