Dwukierunkowe relacje między klasami

Dwukierunkowe relacje między klasami
W8
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 242
0

Pytanie nie tylko w kontekście Pythona ale ogólnie programowania w jakimkolwiek języku, np. Go, C++, C#, Java.

Powiedzmy że mam taką strukturę

Kopiuj
class Interface:
    def __init__(self, name):
        self.name = name
        self.profiles = {} # profiles interface belong to

class Profile:
    def __init__(self, name):
        self.name = name
        self.interfaces = {} # interfaces belonging to profile

Czy takie dwukierunkowe relacje zaimplementowane bezpośrednio w klasach powinny być sygnałem ostrzegawczym? Jeżeli tak, czy zamiast tego dobrym pomysłem jest wydzielenie w takich sytuacjach osobnej klasy o nazwie np. NetworkMapping lub innej, która będzie jasno informowała o swoim przeznaczeniu:

Kopiuj
from bidict import bidict

class NetworkMapping:
    def __init__(self):
        self.map = bidict()
YA
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 2396
1

Samo w sobie niekoniecznie jest czymś złym, np.

Kopiuj
class Node {
    List<Node> children;
    Node parent;
    ...
}



class Department {
    List<Employee> employees;
    ...
}

class Employee {
    Department department;
    ...
}

Natomiast skutki na poziomie implementacji mogą być niepożądane. Czy takie zależności dwukierunkowe utrudniają Ci życie? np. testowanie, sposób inicjalizacji? Może logika się rozlewa po obydwu konceptach (masz jakąś odpowiedzialność, ale jest ziamplementowna trochę w Profile, trochę w Interface)?

obscurity
  • Rejestracja: dni
  • Ostatnio: dni
3

To całkiem normalne w przypadku list i struktur drzewiastych że dzieci znają rodzica a rodzic zna dzieci, nie ma w tym nic złego ani nadzwyczajnego.

No właśnie, czy zależności takie jak np. tu w Department, Employee mogą powodować jakieś niepożądane efekty? Co może się wydarzyć w programie?

Takie zależności powodują problemy przy serializacji, standardowo będziesz mieć nieskończoną pętlę i dostaniesz stack overflow lub wykorzystasz całą dostępną pamięć, lub (najczęściej jeśli używasz gotowych rozwiązań) serializator rzuci błędem.
Przy serializowaniu trzeba stosować jakieś specjalne mechanizmy żeby pamiętać referencje do instancji lub wprowadzić limity głębokości, albo po prostu oznaczyć "parent" jako nieserializowalny i uzupełnić przy deserializacji.
Natomiast zazwyczaj nie ma z tym problemu bo nikt takich rzeczy nie serializuje bezpośrednio, prościej jest mapować obiekty do prostszych form przeznaczonych do tego celu.

Wibowit
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: XML Hills
1
whiteman808 napisał(a):

Pytanie nie tylko w kontekście Pythona ale ogólnie programowania w jakimkolwiek języku, np. Go, C++, C#, Java.

Powiedzmy że mam taką strukturę

Kopiuj
class Interface:
    def __init__(self, name):
        self.name = name
        self.profiles = {} # profiles interface belong to

class Profile:
    def __init__(self, name):
        self.name = name
        self.interfaces = {} # interfaces belonging to profile

Czy takie dwukierunkowe relacje zaimplementowane bezpośrednio w klasach powinny być sygnałem ostrzegawczym? Jeżeli tak, czy zamiast tego dobrym pomysłem jest wydzielenie w takich sytuacjach osobnej klasy o nazwie np. NetworkMapping lub innej, która będzie jasno informowała o swoim przeznaczeniu:

Kopiuj
from bidict import bidict

class NetworkMapping:
    def __init__(self):
        self.map = bidict()

moim zdaniem, jeśli takie dwukierunkowe powiązania nie upraszczają znacząco programu, to lepiej ich unikać. mając dwukierunkowe powiązania można czasem zapomnieć o aktualizacji przeciwnego kierunku powiązania i wykryć to dużo później niż przy jednokierunkowych powiązaniach. gdybym miał już wybierać z powyższych, to pewnie wybrałbym rozwiązanie drugie, tzn. osobną klasę z powiązaniami (przy czym opieram się tu na domysłach, a gdybym znał więcej szczegółów to być może decyzja byłaby inna). mając powiązania w obie strony w jednej klasie od razu widać gdzie i jak trzeba aktualizować powiązania w obu kierunkach. nie wiem czy akurat bidict rozwiązuje sprawę, bo chyba to nie jest multimapa, tylko zwykła mapa, ale za to dwukierunkowa.

yarel napisał(a):

Samo w sobie niekoniecznie jest czymś złym, np.

Kopiuj
class Node {
    List<Node> children;
    Node parent;
    ...
}
...

...

obscurity napisał(a):

To całkiem normalne w przypadku list i struktur drzewiastych że dzieci znają rodzica a rodzic zna dzieci, nie ma w tym nic złego ani nadzwyczajnego.
...

zależność między profile, a interface nie wygląda na strukturę drzewiastą, tylko na pełnoprawny graf z pętlami. to jedno, a drugie to jest to, że (wzbogacone lub nie) drzewo jest reprezentowane przez jeden typ lub zestaw ściśle powiązanych typów (a'la tree, node, leaf, etc). ścisła zależność między typami tree, node i leaf to sprawa naturalna i nieproblematyczna, ale jeśli wprowadzilibyśmy równie silne sprzężenie między typami profile i interface to już jest całkowicie inna sprawa. tworzenie grafu zależności chyba nie jest główną powinnością typów profile i interface.

podsumowując:
w zależności od sytuacji takie dwukierunkowe powiązania mogą mieć mocno odmienny rachunek zalet i wad, więc trzeba się wstrzymać od ogólnikowych zasad typu 'dwukierunkowe zależności są ogólnie dobre' (albo analogicznie 'kiepskie' zamiast 'dobre'), a zamiast tego podejść do sprawy indywidualnie (jak zwykle zresztą - jeśli znamy daną sprawę od podszewki to kierujmy się indywidualną znajomością sprawy, a nie ogólnymi regułami kciuka, które bazują na dużo mniejszym wycinku wiedzy).

p.s.
jeśli chodzi o pythona, to ten ma gc oparte o zliczanie wskaźników, więc do obsługi pętli w grafie obiektów musi stosować dodatkowe mechanizmy, a więc nie radzi sobie z nimi tak dobrze jak z resztą, tzn. nie odśmieca ich od razu. to jest oparte o mojej wiedzy o pythonie sprzed wielu lat, więc może się zmieniło, ale nie sądzę. tak czy siak, możliwe że nie jest to duży problem.

SL
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 1132
0

Lepiej nie. W takim wypadku zawsze jest ryzyko złamania enkapsulacji, bo w razie co to musisz edytować obie instancje co może być kruche

Najlepiej oczywiście mieć niemutowalność tylko w tym wypadku musisz utworzyć instancje obu klas jednocześnie co samo w sobie wskazuje na to, że może lepiej jest zrobić z tego jedną dużą klasę

LukeJL
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 8528
0

Nie jest to zły pomysł i ma swoje zastosowanie (choćby przykład z drzewkiem, gdzie rodzic trzyma dzieci, a każde dziecko ma referencję do rodzica), ale może być problematyczny w implementacji (szczególnie w Rust, gdzie trzeba by jakichś sztuczek użyć, żeby to w ogóle skompilować). Albo w JS jak dasz JSON.stringify na takim nieskończonym grafie, to ci rzuci wyjątek.

Ale możesz trzymać jakieś id zamiast bezpośrednio referencji.

Kopiuj
nodes = {
   0: {
       "links": [1, 2],
   },
   1: {
       "links": [0],
   },
   2: {
       "links": [0],
   }
}

Co rozwiązuje pewne problemy - na poziomie języka programowania przestaje być to nieskończony graf, a będą drzewka (więc łatwiej też będzie to zserializować). Nie trzeba również troszczyć się o referencje. Więc nawet w Rust się to skompiluje.

Jednak wtedy musisz sam zarządzać tymi id/indeksami, gdzieś trzymać te referencje do obiektów więc przenosisz odpowiedzialność z języka programowania na własny kod. No i też jak usuwasz obiekt, to przy wersji z referencjami, musiałbyś usuwać istniejące referencje do obiektu, to teraz też masz "referencje" w postaci ideka/indeksu i też musisz usunąć. Więc koncepcyjnie jest to samo, bo na poziomie koncepcyjnym dalej masz taką samą strukturę, tylko że udajesz, że nie masz, żeby uniknąć pewnych problemów, za to wprowadzając konieczność samodzielnego tym zarządzania.

Ale nie mówię, co jest lepsze, co gorsze.

No i w sumie... podejście z id jednak nawet koncepcyjnie się trochę różni. Bo ponieważ nie robisz bezpośrednio referencji, tylko jakieś id (albo nazwę, guid itp.), to masz więcej swobody, możesz np. dynamicznie ładować coś wtedy, kiedy jest potrzebne. I ogólnie traktować relacje między obiektami bardziej koncepcyjnie, a nie na poziomie tylko referencji do realnych utworzonych już obiektów.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.