2

If I have a 2-way relationship between 2 objects, such that A relates to B and B relates to A, how can I keep this consistent such that the 2 objects will always refer back to each other?

I'm struggling to put my very simple problem into words, so here's a very simple example. I start with Husband and Wife:

function Husband() { this.wife; }
function Wife() { this.husband; }

var harry = new Husband();
var wendy = new Wife();

harry.wife = wendy;
wendy.husband = harry;

Logically, if Harry's wife is Wendy, then Wendy's husband is Harry.

I need a way of keeping this relationship consistent. So I create a setter method on Husband and denote that the wife variable should be treated as private by prefixing with an underscore.

function Husband() {
    this._wife;
    this.setWife = function(wife) {
        this._wife = wife;
        wife.husband = this;
    }
}

Describing this relationship is now simple and encourages consistency:

harry.setWife(wendy);

Likewise, it would be nice to have the reverse option:

wendy.setHusband(harry);

To do this I create a setHusband method on Wife, and tweak Husband as needed.

function Husband() {
    this._wife;
    this.setWife = function(wife) {
        this._wife = wife;
        // wife.husband = this; // <-- husband is now _husband (private)...
        wife.setHusband(this);  // <-- switching to a public method
    }
}

function Wife() {
    this._husband;
    this.setHusband = function(husband) {
        this._husband = husband;
        husband._wife = this;  // <-- Oops! _wife is private!
        husband.setWife(this); // <-- Oops! this results in an infinite loop!
    }
}

At this point I run into a snag. My new setHusband method needs to be able to maintain consistency, but wife is now _wife (private), and calling setWife causes an infinite loop because they reciprocate each other.

I could create another set of methods like reallyJustSetHusband but that seems silly.

My conundrum isn't particular to JavaScript, but I've mentioned it in the question in case there's a particular approach needed.

What is the best way to achieve consistency between these 2 objects? Is there something I've overlooked?

Similar Pattern in the DOM

In the DOM, if you call parent.appendChild(child), then child.parentNode === parent. They are never inconsistent. If a parent has a child the child has the same parent. Other relationships such as nextSibling are also kept consistent.

Daniel
  • 63
  • 5
  • 3
    Note: `this._husband;` as a statement doesn’t do anything. – Ry- Mar 31 '17 at 03:16
  • Perhaps you could maintain a list of relationships. For example, `relationships = [[Wendy,Harry],...]`. Then deleting one of these arrays necessarily removes the relationship from either person. – jakeehoffmann Mar 31 '17 at 03:19
  • 1
    The `reallyJustSetHusband` method is probably the cleanest way to do it. The alternative is to make the properties public, so `setWife` can do `wife.husband = this` – Barmar Mar 31 '17 at 03:23
  • 1
    What do you actually intend to use this for? (It’s important.) – Ry- Mar 31 '17 at 03:32
  • @Ryan My real world project has parent/child relationships which need to maintain consistency, much like the DOM does, such that if you add a child to a parent, the parent of the child is also updated. If you remove a child from its parent, the parent's list of children is kept consistent. The same goes for properties such as next sibling, previous singling, etc. which must be kept consistent when a method like `insertBefore` is called, much like in the DOM. In the DOM, properties like parentNode are read-only, but they are still updated behind-the-scenes. A bit beyond my knowledge. – Daniel Mar 31 '17 at 04:08
  • 1
    @Daniel: In the DOM case, an easy and clean way is to always have the parent manage its children (including their `parent` properties). The methods to add and remove children go on the parent, and if you want to have convenience methods on the children to remove themselves, call the parent’s child removal. – Ry- Mar 31 '17 at 04:13
  • @Ryan: This sounds like a good approach, but with the DOM, the parentNode (or parentElement) property is read-only. How is it updated by the parent? – Daniel Apr 02 '17 at 21:10
  • 1
    @Daniel: You can have a `_parentNode` property updated by the parent that the `parentNode` getter returns. – Ry- Apr 02 '17 at 21:21
  • @Ryan: But isn't _parentNode supposed to be private? – Daniel Apr 02 '17 at 22:42
  • @Daniel: An underscore prefix is a convention indicating non-public. – Ry- Apr 02 '17 at 23:10

3 Answers3

3

A simple way is to just check for redundant values and abort early:

function Husband() {
    this._wife;
    this.setWife = function(wife) {
        if(this._wife == wife) return; // prevents recursion
        this._wife = wife;
        wife.setHusband(this);
    }
}

function Wife() {
    this._husband;
    this.setHusband = function(husband) {
        if(this._husband == husband) return; // prevents recursion
        this._husband = husband;
        husband.setWife(this); 
    }
}

you could also use an external state manager (redux, sql, etc) with update events, or have direct properties that don't need setters and be careful about keeping the data updated.

dandavis
  • 16,370
  • 5
  • 40
  • 36
1

Something like this is one way to do what you're asking:

function Husband() {
  this.marry = function(wife) {
    this.wife = wife;
    wife.husband = this;
  }

  this.say = function() {
    if (this.wife) {
      console.log('Hi! I\'m married to a wife!');
    } else {
      console.log('Hi! I\'m single, no wife.');
    }
  }

}

function Wife() {
  this.marry = function(husband) {
    this.husband = husband;
    husband.wife = this;
  }

  this.say = function() {
    if (this.husband) {
      console.log('Hi! I\'m married to a husband!');
    } else {
      console.log('Hi! I\'m single. No husband.');
    }
  }
}

var h = new Husband();
var w = new Wife();
h.marry(w);
w.say();
h.say();
mariocatch
  • 8,305
  • 8
  • 50
  • 71
1

._wife isn't necessarily private - a wife knows its husband and should be allowed to set his properties. Also you can easily break out of the infinite loop:

class Wife() {
  constructor() {
    this._husband=null;
  }
  get husband() { return this._husband }
  set husband(h) {
    if (this._husband) this._husband._wife = null;
    this._husband = h;
    if (h && h.wife != this) h.wife = this;
  }
}

If you don't want this insider knowledge, you need to use an intermediate class Marriage that manages a relationship (can be created, queried and broken).

class Marriage {
  constructor(h, w) {
    this.husband = h;
    this.wife = w;
  }
  divorce() {
    this.husband.marriage = null;
    this.wife.marriage = null;
  }
}
class Wife() {
  constructor() {
    this.marriage = null;
  }
  get husband() {
    return this.marriage && this.marriage.husband;
  }
  set husband(h) {
    if (this.marriage) this.marriage.divorce();
    if (h.marriage) h.marriage.divorce();
    this.marriage = h.marriage = new Marriage(h, this);
  }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375