In Martin Fowler's, UML Distilled, in the "Bidirectional Association" section, he says:
Implementing a bidirectional association in a programming language is often a little tricky because you have to be sure that both properties are kept synchronized. Using C#, I use code along these lines to implement a bidirectional association:
Code from the book
class Car...
public Person Owner {
get {return _owner;}
set {
if (_owner != null) _owner.friendCars().Remove(this);
_owner = value;
if (_owner != null) _owner.friendCars().Add(this);
}
}
private Person _owner;
...
class Person ...
public IList Cars {
get {return ArrayList.ReadOnly(_cars);}
}
public void AddCar(Car arg) {
arg.Owner = this;
}
private IList _cars = new ArrayList();
internal IList friendCars() {
//should only be used by Car.Owner
return _cars;
}
....
Question 1:
I tried implementing this in python (get_cars()
in Person
&& get_owner_v2()
in Car), I want to know if my code can be used to describe a 'bidirectional association' or not, and if not, how should it be modified to do so?
Note: the first version (inspecting the caller's class/object) was working fine until I started creating car object independently and assigning them to owner on two steps (last 4 print statement proves that). The second version uses the licence numeber lno
to figure out the owner. Not sure if I did it correctly but it does the work, based on my understanding.
My Implementation:
#trying the inverse-bidirectional association
class Person:
cars_and_lnos = []
def __init__(self,name):
self.__cars = []
self.__cars.append("Dummy")
self.__name = name
def add_car(self, *args, obj = None):
# import inspect
if not obj:
car_object = Car(*args)
else:
car_object = obj
Person.cars_and_lnos.append((car_object,self.__name))
self.__cars.append(car_object)
def __repr__(self):
return f"{self.__name}"
def get_cars(self):
return self.__cars
class Car:
car_count = 0
def __init__(self, lno, price ,year, make, model):
import inspect
self.__lno = lno
self.__price = price
self.__year = year
self.__make = make
self.__model = model
Car.car_count += 1
self.__car_id = Car.car_count
if "self" in inspect.getargvalues(inspect.stack()[1][0]).args :
self.__owned_by = f"Car (ID:{self.__car_id}) is Owned By: {inspect.stack()[1][0].f_locals['self']}, which is an instance of Class: {inspect.stack()[1][0].f_locals['self'].__class__.__name__}"
else:
self.__owned_by = "This car is not owned by anyone."
def __repr__(self):
return f"Car ID: {self.__car_id}."
def get_specs(self):
print(f"+{'-'*30}+")
print(f"""
Liscense No.: {self.__lno}
Price: {self.__price}
Year: {self.__year}
Make: {self.__make}
Model: {self.__model}
""")
@property
def get_lno(self):
return self.__lno
def get_owner_v1(self):
# import inspect
return self.__owned_by
def get_owner_v2(self):
if Person.cars_and_lnos:
for tup in Person.cars_and_lnos:
if self.__lno == tup[0].get_lno:
return f"Car (ID: {self.__car_id}) is owned by: {tup[1]}, he is a: {Person.__name__} Class."
return "[0] This car is not owned by anyone."
else:
return "[1] This car is not owned by anyone."
owner1 = Person("William")
owner1.add_car("4567781",10000,2012,"Toyota","Corrolla")
owner1.add_car("2137813",8000,2010,"Porshe","GT3")
owner1.get_cars()[1].get_owner_v1()
print(f"{owner1} owns {len(owner1.get_cars())-1} Car(s).")
print(owner1.get_cars()[1].get_owner_v1())
print("=====================================================")
owner2 = Person("Defoe")
owner2.add_car("8729120",10000,2012,"Dodge","Challenger")
print(f"{owner2} owns {len(owner2.get_cars())-1} Car(s).")
print(owner2.get_cars()[1].get_owner_v1())
print("=====================================================")
car1 = Car("7839291",10000,2012,"Chevrolet","Camaro")
car2 = Car("6271531",10000,2012,"Ford","Mustang")
print(car2.get_owner_v1())
print("=====================================================")
owner3 = Person("Lyan")
owner3.add_car("656721",9000,2013,"Toyota", "Camry")
owner3.add_car("652901",9000,2013,"Nissan", "Sunny")
owner3.add_car("870251",9000,2013,"BMW", "6 Series")
print(owner3.get_cars()[1].get_owner_v2())
print(owner2.get_cars()[1].get_owner_v2())
print(owner1.get_cars()[1].get_owner_v2())
print("=====================================================")
car3 = Car("5424201",10000,2012,"Volks","Eos")
print(car3.get_owner_v1())
print(car3.get_owner_v2())
owner4 = Person("Daphne")
owner4.add_car(obj=car3)
print(car3.get_owner_v1())
print(car3.get_owner_v2())
Question 2:
In this section he says the that these two notations are likely the same:
Is the following notation (without any arrows specified) can also be considered bidirectional relationship?
EDIT:
I understand that a logical refinement to the class diagram would be * to * (many to many), but I'm only concerned about how correct my implementation to the description/design is and how can it be improved, and where the no-arrows association line fit in the picture.
The question 2 is already perfectly answered. So I'll just provide some complementary info on navigability before focusing on question 1.
The navigability is something which is defined in very broad terms in the UML specs:
Navigability means that instances participating in links at runtime (instances of an Association) can be accessed efficiently from instances at the other ends of the Association. The precise mechanism by which such efficient access is achieved is implementation specific. If an end is not navigable, access from the other ends may or may not be possible, and if it is, it might not be efficient.
The navigability expresses some promises or constraints about the implementation. In an early stage it is therefore common not to specify the navigability. In later stages, you'll explicitly show the navigation path that must be supported. But it's rarely done systematically, and moreover, it is rare to see the non-navigability (an X instead of an arrow head).
Conclusion: when you see no arrow head and no X, you cannot draw any conclusion, unless your team has defined some conventions that are more precise on this situation.
An association can be implemented in many ways. A typical implementation is to keep in the object of class A one or several references to associated objects of class B. By definition, this ensures navigability A ---> B
since there is an efficient access. But what about the way back ?
If you do nothing else, the object B can not find easily the object A it is associated with. The only way, would be to iterate through all the A objects to find its reference (possible but not efficient) if the reference is not private (which would make the search impossible). So you'd have a non navigable way back A x--> B
To make it bidirectionally navigable, you would keep in B objects a reference to the associated A object(s). This makes the navigability bidirectional: A <--> B
.
The main challenge here, is that you have to keep the references synchronized in both objects.
Person
A keeps a list of owned Car
s C1, C2 and C3, reciprocally each Car
C1, C2, C3 would keep the reference to their owner A.Design issue: each class should be well encapsulated and use the public access of the other class. So Owner.AddCar(Car)
and Car.SetOwner(Owner)
should be exposed in public interface. But if somewhere you'd invoke B.AddCar(C4)
, the AddCar()
method would want to invoke C4.SetOwner(B)
to ensure consitency. But SetOwner()
method could also be invoked from outside and woultd therefore want to also keep synchronisation by invoking B.AddCar(C4)
and then ... you'll end with a stack overflow of two methods calling each other for ever to maintain synchronisation.
Fowler's code solved this issue, by giving Car
a privileged access to the car list of the Person
via the friendCars()
. This solves the trick but requires Car
to know the internals of Person
and create an unnecessary coupling.
I'm not a specilist of Python, but what I understand, is that your code is different:
cars_and_lnos
that holds tuples of all the cars and their owner.__owned_by
is only updated at construction of the car and not afterwards. So it might work when the car is constructed in the adding process based on *arg
, but fail when an existing car is added to an owner.get_owner_v1()
fails because it is based on __owned_by
. but get_owner_v2()
works well because it iterates (very inefficiently) on cars_and_lnos
which is up-to-date.In short, the v1 fails because your code produces inconsistent objects.
This being said, your approach is more promising than Fowler's one:
cars_and_lnos
into a separate class CarRegistration
, and rewrite Car
and Person
to interact with CarRegistration
to update ownerships based on a well designed api.CarRegistration
to replace cars_and_lnos
with 2 dictionaries for optimized search.