1

What is the recommended way to reference other classes within a container class while keeping everything decoupled?

In the below example, I'd like to automatically add to the SceneGraph the constructed Path.

class SceneGraph {
  constructor() {
    this.children = []
  }

  addChild(child) {
    this.children.push(child)
  }
}

class Path {
  constructor(opts) {
    this.color = opts.color

    // @NOTE: Coupling here
    sceneGraph.addChild(this)
  }
}

// User space

const sceneGraph = new SceneGraph()
const path = new Path({ color: 'red' })

"Automatically" here meaning that I'd like to avoid doing something like this:

const path = new Path({ color: 'red' })
sceneGraph.addChild(path)

... which looks intuitive enough, but it soon starts getting clunky as more and more methods are added to the Path that should also affect the SceneGraph - i.e path.remove().

The example above looks problematic to me, since the SceneGraph is inherently coupled with the Path.

Is there a pattern available that solves this problem?

2

You could create a PathFactory which does nothing but create Paths. PathFactory would be created with an instance of SceneGraph, so that PathFactory can "automatically" link the two without SceneGraph directly having a reference to Path.

class PathFactory {
  constructor(sceneGraph) {
    this.sceneGraph = sceneGraph;
  }

  createPath(options) {
    let path = new Path(options);
    this.sceneGraph.addChild(path);
    return path;
  }
}

const sceneGraph = new SceneGraph();
const pathFactory = new PathFactory(sceneGraph);

const path = pathFactory.createPath({color: 'red' });

Granted, this is displacing the coupling between SceneGraph and Path with an intermediary PathFactory, however it obtains what you wish to achieve. Automatic adding without a direct relationship between SceneGraph and Path.

  • I've thought of this; It breaks down when more Path methods are added that affect the SceneGraph, i.e path.remove(). Apologies - I've edited the question to include this while you were writing up the answer. – Nik Kyriakides Feb 7 '18 at 12:03
  • @NicholasKyriakides To SceneGraph, path here is just a child. There is no coupling if you have a SceneGraph.removeChild function. – Neil Feb 7 '18 at 12:05
  • I was thinking path.remove() would be more intuitive; I'm trying to avoid add/remove operations on the SceneGraph altogether. To be honest, I'm not yet sure if the API I want is a sensible approach to begin with. – Nik Kyriakides Feb 7 '18 at 12:07
  • @NicholasKyriakides Hmm, you've already got addChild. It seems sensible even expected to have a removeChild. If that's not what you want, then you may have to rethink a bit what it is you want to do. – Neil Feb 7 '18 at 12:11
  • 1
    @NicholasKyriakides If the actual work is done in SceneGraph, then either Path directly calls SceneGraph or you add some sort of intermediary such as PathFactory in the way. By nature they are tightly coupled, but that may not even be a bad thing. I would just avoid depending on a global variable reference. – Neil Feb 7 '18 at 12:54
2

The Path and SceneGraph are already coupled as you except each Path to be added to the SceneGraph upon instantiation.

But there is a clear hierarchy here. The SceneGraph holds a list of Path children. In these cases, I find it cleanest when the parent container is responsible for the creation of the children. Then:

class SceneGraph {
  constructor() {
    this.children = []
  }

  newChild(opts) {
    var child = new Path(opts)
    this.children.push(child)
    return child;
  }
}

class Path {
  constructor(opts) {
    this.color = opts.color
  }
}

There are substantial restrictions with this solution. There is now a hard dependency from SceneGraph to Path. It is not possible to inject alternative Path implementations. If more flexibility is needed, then a separate factory as suggested by Neil's answer might be a better approach.

1

If you want to support the nice syntax of path.remove() etc., I think you'll have to have the path have a link to it's container, ScenePath. This introduces a troublesome cyclic dependency. Whether this is worth the convenient API is a good question. (I'm much more tolerant of circular dependencies than most) Example code:

class Path {
  constructor(sceneGraph, options) {
    this.variousStuff = options.variousStuff;
    this.sceneGraph = sceneGraph;  // introduces circular dependency
    sceneGraph.addChild(this);
  }

  remove() {  // note - I'd name this "delete", not "remove"
    this.sceneGraph.remove(this);
    this.cleanupAnythingThatNeedsCleanup();
  }

  clone() {    // and might name this "deepClone" if that fits better
     let cloned = this.clone();  // or deepClone()
     this.sceneGraph.addChild(cloned);
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.