3

I would like to know the best way to handle a Bi-Directional association in O.O.P. I have found multiple solutions on Google and SO but each and every one of them seems to have a drawback. The language is irrelevant but let's use PHP to illustrate what i mean:

Let's say i have a simple States..[1..n]..City association:

public class State {
    public $cities;
    public function add_city($city) {}
}
public class City {
    public $state;
    public function set_state($state) {}
}

IMPLEMENTATION #1:

public class State {
    public $cities;
    public function add_city($city) {
        $this->cities[] = $city;
        $city->state = $this;
    }
}
public class City {
    public $state;
    public function set_state($state) {
        $this->state = $state;
        $state->cities[] = $state;
    }
}

The two problems with this implementation are:

  • "$state" and "$cities" must be public (so anyone can add a city without using the public function add_city...). There is no "friend class" concept in most languages.
  • the public function could have to do some operation before adding

IMPLEMENTATION #2:

public class State {
    public $cities;
    public function add_city($city) {
        $this->cities[] = $city;
        if ($city->state != $this) {
            $city->set_state($this);
        }
    }
}
public class City {
    public $state;
    public function set_state($state) {
        $this->state = $state;
        if (!in_array($this, $state->cities)) {
            $state->add_city($this);
        }
    }
}

A little bit better than #1 but the "set_state" function must call "in_array" which in must language is O(n) (turning a fast O(1) operation into a O(n) one.)

IMPLEMENTATION #3:

public class State {
    public $cities;
    public function add_city($city, $call_the_other_function = true) {
        $this->cities[] = $city;
        if ($call_the_other_function) {
            $city->set_state($this, false);
        }
    }
}
public class City {
    public $state;
    public function set_state($state, $call_the_other_function = true) {
        $this->state = $state;
        if ($call_the_other_function) {
            $state->add_city($this, false);
        }
    }
}

Implementation #3 is very effective but is kind of "ugly" (for lack of a better term) because of the extra optional parameter

Anyway, if anyone has any idea what the "Right Way"(tm) is, i would like to know.

EDIT: If that's possible i would like a solution:

  • Without using another class
  • Without knowing the order in which the object are created (i.e. not a "constructor" solution)
d08z
  • 73
  • 6
  • 2
    "Right Way(tm)" questions are off-topic for Stackoverflow, as they tend to generate opinion-based answers. Please review http://stackoverflow.com/help/on-topic to see what you should and shouldn't ask about. – Tim Lewis Oct 15 '15 at 15:19
  • Forgive me, but I'm confused about your first implementation. Why do those variables need to be public? The "friend" class could just use the public method on the other class, could it not? – Jacob Oct 15 '15 at 15:21
  • In addition to what @TimLewis said, perhaps your question would be better suited to [Programmers StackExchange](http://programmers.stackexchange.com/) :) – Jacob Oct 15 '15 at 15:23
  • Yes, but then an "outsider" could directly access the "add" function from the collection i.e. $my_state->get_cities()->add() (bypassing $my_state->add_city() "additional code" ...) – d08z Oct 15 '15 at 15:24
  • I believe there is no right way or perfect code. Either something works for you or doesn't. If it doesn't, it's eligible to refactor, end of story. – rr- Oct 15 '15 at 15:45
  • @JacobWalker when referring other sites, it is often helpful to point that [cross-posting is frowned upon](http://meta.stackexchange.com/tags/cross-posting/info) – gnat Oct 15 '15 at 20:42
  • @gnat Thanks, I'll bear that in mind :) – Jacob Oct 16 '15 at 12:20

2 Answers2

2

I'd try using a constructor, in this way, when you instatiate a city you can directly pass its state.

public class State {
    private $cities;
    public function add_city($city) {
        $this->cities[] = $city;
    }
}

public class City {
    private $state;
    function __construct($state) {
        $state->add_city($this)
        $this->state=$state
    }
}
NicolaSysnet
  • 486
  • 2
  • 10
  • what if the state does not exists yet (will be created and linked later) when the city is created? – d08z Oct 15 '15 at 15:47
  • 1
    You need to specify your use cases, then the code will follow. – NicolaSysnet Oct 15 '15 at 16:01
  • There is a dependency between cities and states: a city belongs to exactly one state, a state contains zero or more cities. This dependency requires a city to be created after its owning state. The answer expresses this statement in code; it also provides the correct encapsulation of the properties. – axiac Oct 15 '15 at 16:03
  • As @axiac points out, it is not just a matter of use cases, but also a matter of class diagrams :D – NicolaSysnet Oct 15 '15 at 16:07
1

In all of your proposals City knows about methods in State or vice versa. What if you introduced a third class responsible for linking cities to states, such as LocationService with one method such as linkCityToState? With this, you could later extend it with linkCityToCountry, or some advanced logic such as getPostalCodeFromApi.

If you worry about performance, turn your list into hashset, which will reduce lookups complexity to mere O(log n).

Also in your implementation #1 you have city->cities...?

In any case, I would never for solution #3 unless I was programming compression algorithms, drivers, massive database queries, etc.

rr-
  • 14,303
  • 6
  • 45
  • 67
  • My mistake; i should have added "without using any additional classes" to my question. – d08z Oct 15 '15 at 15:26
  • Welp, cyclic references are usually solved with introduction of third class. – rr- Oct 15 '15 at 15:27
  • Honest question: why would you never use solution #3? – d08z Oct 15 '15 at 15:30
  • You browse random code. You see a method: `add_city()`. It accepts `City`. Perfect. But then it has a `bool` argument = WTF1. Then you see its name: `call_the_other_function` = WTF2... This breaks several patterns - KISS, SRP and is an example of premature optimization. If you really need performance that much, don't use PHP. – rr- Oct 15 '15 at 15:36
  • but that is why the argument is optional and is true by default so you dont have to include it on the original call... anyway thanks for everything – d08z Oct 15 '15 at 15:41