Co ważniejsze - zasady czy wygoda?

0

Mamy stworzyć np zarządzanie ludźmi, którzy będą się różnili ze względu na kontynenty. Zgodnie z poprawnymi zasadami projektowania powinno to wyglądać tak:

  • klasa abstrakcyjna Human ze wspólnymi właściwościami i metodami
  • klasy dziedziczące, czyli EuropeHuman, AfricaHuman, AsiaHuman itd

Ale zwykle korzysta się z jakiegoś frameworka i ORMa gdzie łatwo zrobić CRUDa i dochodzi kwestia wydajności bazy danych. Łatwiej żeby to wszystko było w jednej tabeli i miało tylko różny kontynent, bo dużo szybciej się coś wyszuka.

Teraz jak to napisałem to się zastanawiam czy są jakieś ORMy które potrafią tak specjalistycznie mapować obiekty?

I teraz co jest ważniejsze? Teoretycznie systemy powinno się projektować tak żeby łatwo się odnaleźć w kodzie, więc lepiej by to było zrobić z klasą abstrakcyjną i dziedziczącymi, ale ze względu na normalizację bazy danych to powinno być w jednej tabeli.

4

Mamy stworzyć np zarządzanie ludźmi, którzy będą się różnili ze względu na kontynenty. Zgodnie z poprawnymi zasadami projektowania powinno to wyglądać tak:

  • klasa abstrakcyjna Human ze wspólnymi właściwościami i metodami
  • klasy dziedziczące, czyli EuropeHuman, AfricaHuman, AsiaHuman itd

Czym się niby mają różnić by to uzasadniało tworzenie hierarchii klas?

0

Ok, to trochę zły przykład, ale załóżmy że czymś by się różniły jeszcze.

0

Podaj konkretny przykład obrazujący problem.

0

Nie mam takiego, bo dopiero się uczę i się nad tym zastanawiam. Czytam o zasadach dobrego programowania i tam są np przykłady markami samochodów.
Jest samochód który dzieli się na osobowe i ciężarowe. Osobowe dzielą się na Sedan i Kombi itd. Sedan na Audi i BMW itd. I mam np wypożyczalnie samochodów, więc np Audi A4 powinno być w klasie Audi, która dziedziczy z Sedan, a Sedan dziedziczy z osobowe itd. No ale jakbym chciał do tego użyć jakiegoś ORM to miałbym bardzo dużo powtórzonych pól w tabelach, więc najlepiej gdyby wszystkie auta były w jednej tabeli...

4

Osobowe dzielą się na Sedan i Kombi itd. Sedan na Audi i BMW itd.

No i już tutaj cały misterny plan się sypie bo zarówno Audi jak i BMW mogą być sedanami albo kombi.

Zobacz sobie np na serwisy z ogłoszeniami kupna i sprzedaży. Są tam np formularze do wystawiania oferty sprzedaży samochodu. Czy masz multum różnych formularzy dla każdego rodzaju samochodu czy może tylko jeden?

Wiem, że uogólnianie zasad dotyczących używania dziedziczenia może być naiwne, ale spróbuję. Jako praktyczną zasadę możesz zastosować to:

  • dziedziczenie stosujemy jeśli podklasy mają różną logikę i/ lub różny zestaw pól
  • jeśli podklasy różnią się tylko i wyłącznie stałymi to znaczy, że niepotrzebnie zastosowaliśmy dziedziczenie i można te stałe po prostu wstawić w pola klasy, a następnie robić wiele instancji tej klasy z różnymi zestawami stałych

Sprecyzuję teraz drugi punkt. Przykład złego stosowania dziedziczenia:

interface Animal {
  void makeSound();
}

class Dog implements Animal {
  void makeSound() {
    System.out.println("woof");
  }
}

class Cat implements Animal {
  void makeSound() {
    System.out.println("meow");
  }
}

Jak widać podklasy różnią się tylko stałymi. W przypadku Dog mamy stałą "woof" a w przypadku Cat mamy stałą "meow". Można więc tą stałą wstawić jako pole klasy i uniknąć niepotrzebnego dziedziczenia:

class Animal {
  String sound;
  public Animal(String sound) {
    this.sound = sound;
  }
  void makeSound() {
    System.out.println(sound);
  }
}

Animal dog = new Animal("woof");
Animal cat = new Animal("meow");
0

Dzięki, chyba za bardzo sugerowałem się przykładami, bo ten z autami i państwami rzeczywiście był nieudany.

A co w sytuacji gdy w Twoim przykładzie chcemy dodać np typ budy? Pies może nie mieć budy, albo może mieć drewnianą albo metalową, ale już kot nie powinien mieć budy. W bazie to może być null, ale jak użyjemy ORMa to to dla kota też się nam zmapuje buda, a czytałem że w OOP nie powinno być pól które nigdy nie będą użyte.
I tak samo np w przypadku sklepu. Mamy tabelę product i np dla samochodów będą całkiem inne pola, a dla telewizorów całkiem inne. I tutaj już jest duży problem z używaniem ORMów.

1

Pomijając kwestię zapisu do bazy danych to jeśli chcemy dodać informację o budzie dla psa to mamy następujące rozwiązania:

  1. Dziedziczenie, więc tutaj powstaje nam klasa Dog, ale klasy Cat dalej nie trzeba. Mamy więc
class Animal {
  String sound;
  public Animal(String sound) {
    this.sound = sound;
  }
  void makeSound() {
    System.out.println(sound);
  }
}

class Dog extends Animal {
  Kennel kennel;
  public Dog(String sound, Kennel kennel) {
    super(sound);
    this.kennel = kennel;
  }
  Kennel getKennel() { // dałem gettera bo i tak wszędzie się używa getterów
    return kennel;
  }
}

Animal dog = new Dog("woof", new Kennel());
Animal cat = new Animal("meow");
  1. Kompozycja. Tutaj nie musimy zmieniać klasy Animal ani rozszerzać jej. Tworzymy za to np klasę AnimalWithDen. Możemy mieć więc taką hierarchię:
class Animal {
  String sound;
  void makeSound();
}

class Den
class Kennel extends Den // przy założeniu, że Kennel ma coś więcej niż Den

class AnimalWithDen {
  Animal animal;
  Den den;
  // alternatywnie
  Optional<Den> denOpt;
}

Kompozycja pozwala nieraz uniknąć niepotrzebnie skomplikowanej struktury dziedziczenia aczkolwiek nie są to rozwiązania które zawsze można stosować zamiennie. W zależności od przypadku raz lepiej będzie się sprawdzać kompozycja a raz dziedziczenie.

Wracając do sprawy ORMa to ORMy są właśnie po to, by rozwiązywać problem dziedziczenia przy mapowaniu obiektów na tabele w bazie danych. Opcji jest wiele. Można poinstruować ORMa by np:

  1. Stworzył tabelę z kolumnami które mogą być nullowe. Wtedy jeśli mamy taką hierarchię dziedziczenia:
class Zwierzę {
  String dźwięk;
}
class Kot extends Zwierzę {
  String ulubionyKocyk;
}
class Pies extends Zwierzę {
  String ulubionaBuda;
}

to ORM stworzy tabelę z polami np: primaryId, nazwaKlasy, zwierzę_dźwięk, kot_ulubionyKocyk, pies_ulubionaBuda. W zależności od zawartości pola 'nazwaKlasy' to jedno z pól 'kot_ulubionyKocyk" lub 'pies_ulubionaBuda' będzie nullem. ORM sam to sobie ogarnie przy wyciąganiu obiektu z bazy.
2) Stworzył dodatkowe tabele z atrybutami dodanymi przez podklasy. Dla przykładu z poprzedniego punktu (Zwierzę, Kot, Pies) ORM stworzy 3 tabele: TabelaZwierzę, TabelaKot, TabelaPies. TabelaKot będzie zawierać dwa pola: id oraz ulubionyKocyk. TabelaPies też będzie mieć dwa pola: id oraz ulubionaBuda. TabelaZwierzę będzie natomiast zawierać: id, nazwaKlasy, dźwięk, id_podtypu. Następnie przy wyciąganiu np kota ORM zrobi zapytanie typu:
SELECT z.id, z.dźwięk, k.ulubionyKocyk FROM TabelaZwierzę z, TabelaKot k WHERE z.nazwaKlasy == 'Kot' AND z.id_podtypu == k.id
3) ORM może stosować też inne metody ale to już zależy od fantazji twórców.

PS. Nie jestem zwolennikiem używania ORMów. Otypowane (na etapie kompilacji kodu Javowego itp) zapytania SQL można osiągnąć na inne, mniej magiczne sposoby.

0

Wielkie dzieki :)

0

Nie zgodzę się z takim przedstawieniem klasy Animal. To zależy jeszcze od zastosowania tak naprawdę. Ale na ogół taka klasa powinna być abstrakcyjna. Chociażby z prostego powodu. Powiedz mi jak wygląda zwierzę. Ile ma kończyn? Co je? Itd.

Z drugiej strony jeśli chodzi o wyszukiwanie samochodu, to faktycznie można by mieć jedynie klasę Car z odpowiednimi atrybutami: kolor karoserii, przebieg, model, typ nadwozia, paliwo... I to wszystko będzie leżało w jednej tabeli.

Czyli całe obiektowe podejście zależy jednak od problemu. Bo jeśli robisz grę, to powinieneś mieć klasę Animal jako abstrakcyjną, klasę Car jako abstrakcyjną itd. Ale jeśli robisz np. proste wyszukiwanie jak w serwisie ogłoszeniowym, to wystarczą tylko konkretne klasy Animal i Car.

Chociaż pewnie masz jeszcze jedno pytanie, którego nie potrafiłeś zadać. (Pomijam sensowność tego rozwiązania.) Co w takim przypadku, gdy mamy klasę Human i dziedziczące po niej EuropeHuman, AfricaHuman, AsiaHuman... I teraz jest sytuacja, że dla EuropeHuman ważne jest to, ile zarabia. Dla AfricaHuman ważne jest to, jakiego ma długiego. Dla AsiaHuman natomiast współczynnik skośności oczu. I teraz AsiaHuman nie będzie miał informacji o długości penisa, a EruopeHuman nie będzie miał informacji o skośności oczu.

Teoretycznie więc każda z tych klas powinna mieć swoją własną tabelę. W praktyce to jednak nieco bez sensu. I tu dochodzimy do zderzenia modelu obiektowego z relacyjnym. To są dwa zupełnie różne modele. Ciężko jest przejść z jednego do drugiego. Dlatego tutaj trzeba iść na jakiś kompromis. I tak będzie dopóki ktoś nie opracuje czegoś w stylu modelowej bazy danych :)

2

klasa abstrakcyjna Human ze wspólnymi właściwościami i metodami
klasy dziedziczące, czyli EuropeHuman, AfricaHuman, AsiaHuman itd

To byłby rasizm w kodzie :) Jeśli tworzysz osobną klasę dla człowieka każdej rasy.

Poza tym - gdzie byś umieścił np. Elona Muska? Z pochodzenia AfricaHuman, z rasy EuropeHuman, ale obecnie AmericaHuman.

A gdzie był umieścił polonię amerykańską? EuropeHuman czy AmericaHuman? A jak ktoś emigruje do USA to co, zmieniasz mu klasę?

To jest tak bardzo nierealistyczne, że nawet nie mogłoby to służyć jako przykład do nauki (już przykłady w książkach o zwierzątkach są bardziej realistyczne)

Myślę, że relacja między człowiekiem a kontynentem jest raczej relatywna(jak w bazach danych), a nie klasowa (prędzej kompozycja byłaby lepsza).

2

@Juhas:
Tworzenie hierarchii klas gdzie każda ma tę samą logikę i zestaw pól i klasy różnią się tylko stałymi (np użytymi w metodach, jak w przykładzie ze zwierzętami wydającymi odgłosy) jest nadużyciem mechanizmu dziedziczenia. Dziedziczenie nie jest po to, by program spełniał jakieś ideologiczne założenia tylko jest narzędziem do rozwiązywania konkretnych problemów.

Jeśli chcesz odróżnić rodzaj zwierzęcia to zamiast tworzyć wiele podklas klasy Animal zrób dodatkowe pole w klasie Animal, np jakiegoś enuma z wartością typu JestemKoniem, JestemKotem itd

Hierarchia klas ma też tę wadę że jest sztywna. Jeśli np na siłę będziesz chciał robić osobną klasę dla każdego gatunku zwierzęcia to musiałbyś nie tylko stworzyć miliony klas, ale także codziennie aktualizować ich zbiór, bo codziennie ludzkość odkrywa jakieś nieznane wcześniej gatunki zwierząt. Jak widać jest to wprost nie do ogarnięcia. Oczywiście jeśli zamkniesz się w swoim świecie, np jakiejś gry to możesz sobie ograniczyć uniwersum kreatur i wtedy nagle robienie przekombinowanej hierarchii klas stanie się możliwe, ale dalej nierozsądne.

W komercyjnych projektach działanie wbrew brzytwie Ockhama, czyli generowanie niepotrzebnego kodu się mści, bo trzeba go potem utrzymywać, a to kosztuje. Przed podejmowaniem decyzji (np wprowadzaniem ekstra abstrakcji) trzeba sobie zrobić rachunek kosztów i zysków. Im większe masz doświadczenie komercyjne tym lepiej będziesz w stanie te koszty i zyski oszacować.

0
Wibowit napisał(a):

@Juhas:
Tworzenie hierarchii klas gdzie każda ma tę samą logikę i zestaw pól i klasy różnią się tylko stałymi (np użytymi w metodach, jak w przykładzie ze zwierzętami wydającymi odgłosy) jest nadużyciem mechanizmu dziedziczenia. Dziedziczenie nie jest po to, by program spełniał jakieś ideologiczne założenia tylko jest narzędziem do rozwiązywania konkretnych problemów.

Przecież napisałem, że są sytuacje, w których to podejście jest ok. Ale są też sytuacje, gdzie to się nie sprawdzi. Bardzo prosty przykład. Odejdźmy od zwierząt. Weźmy się za figury geometryczne. Masz program, który rysuje kółko i kwadrat. I teraz możesz zrobić klasę Shape, która będzie miała odpowiedni enum i w metodzie Draw będzie posługiwała się ifem, żeby narysować albo kółko, albo kwadrat. I teraz dochodzi do tego jeszcze trójkąt. I tu już musisz zmienić klasę Shape i odchodzisz od SOLID.

Pewnie, nie ma co nadużywać dziedziczenia. Ale pisałem o tym, że to wszystko zależy od projektu i założeń.

Hierarchia klas ma też tę wadę że jest sztywna. Jeśli np na siłę będziesz chciał robić osobną klasę dla każdego gatunku zwierzęcia to musiałbyś nie tylko stworzyć miliony klas, ale także codziennie aktualizować ich zbiór, bo codziennie ludzkość odkrywa jakieś nieznane wcześniej gatunki zwierząt.

No i dobrze. W innym wypadku musiałbyś codziennie dopisywać jakieś ify do istniejącej już klasy i bardzo szybko kod stałby się nie do utrzymania. Zwierzęta mają swój wygląd (ilość kończyn, postawa itd), odgłosy i inne unikalne cechy. I teraz możesz albo tak:

Animal cat = new Animal("meow", Legs4, Eyes2); //dla ułatwienia podaję enumy, mogłyby to być zwykłe liczby
Animal spider = new Animal("I will kill you", Legs8, Eyes8);
Animal monkey = ...

Albo możesz mieć też tak:

Cat cat = new Cat();
Spider spider = new Spider();
Monkey monkey = new Monkey();

I są projekty, gdzie inaczej się nie da. Bo za chwile okaże się, że trzeba gdzieś coś rozgraniczyć. Np. nagle trzeba narysować wygląd zwierzęcia. I owszem, można dodać nowy parametr do konstruktora, w którym przekażemy odpowiedni obrazek, ale to już się nieco kopie z ideą programowania obiektowego.

W komercyjnych projektach działanie wbrew brzytwie Ockhama, czyli generowanie niepotrzebnego kodu się mści, bo trzeba go potem utrzymywać, a to kosztuje. Przed podejmowaniem decyzji (np wprowadzaniem ekstra abstrakcji) trzeba sobie zrobić rachunek kosztów i zysków. Im większe masz doświadczenie komercyjne tym lepiej będziesz w stanie te koszty i zyski oszacować.

Niepotrzebna abstrakcja to może być wciskanie wszędzie na siłę interfejsów. A nie dziedziczenie. Chociaż oczywiście nie zawsze dziedziczenie ma sens.

1

Przeczytaj jeszcze raz co napisałem:

Tworzenie hierarchii klas gdzie każda ma tę samą logikę i zestaw pól i klasy różnią się tylko stałymi (np użytymi w metodach, jak w przykładzie ze zwierzętami wydającymi odgłosy) jest nadużyciem mechanizmu dziedziczenia.

Nie postulowałem zastępowania dziedziczenia ifologią. W zasadzie moje wywody tutaj można by streścić w jednym zdaniu (oczywiście o ile mi coś nie umknęło, bo brzmi dość ryzykownie): jeśli da się uniknąć dziedziczenia bez dorzucania ifów to znaczy, że dziedziczenie jest niepotrzebne.

1
Animal cat = new Animal("meow", Legs4, Eyes2); //dla ułatwienia podaję enumy, mogłyby to być zwykłe liczby
Animal spider = new Animal("I will kill you", Legs8, Eyes8);

przecież możesz zastosować fabrykę i zrobić cat = createCat() zamiast klasy, i fabryka by ustawiała domyślne parametry.

I są projekty, gdzie inaczej się nie da. Bo za chwile okaże się, że trzeba gdzieś coś rozgraniczyć.
Np. nagle trzeba narysować wygląd zwierzęcia. I owszem, można dodać nowy parametr do konstruktora,
w którym przekażemy odpowiedni obrazek, ale to już się nieco kopie z ideą programowania obiektowego.

Zależy co robisz. Jeśli robisz prostą małą gierkę, gdzie każdy kot wygląda identycznie, robi tak samo miał, ma taki sam kolor, to pewnie, możesz nie podawać żadnych parametrów, tylko pisać new Cat. Ale jeśli masz cokolwiek większego to i tak będziesz musiał podać te parametry, np. załóżmy, że robisz bardziej realistyczną symulację i do każdego tworzona kota będziesz podawał losowy kolor, losową wielkość, losową masę w kilogramach, losowe umiejętności (np. umiejętność dobrego łapania myszy), losowe choroby, losową lokację itp.

Więc i tak nie będzie to new Cat a new Cat(color, size, mass, skills, diseases, location)

oczywiście podawanie zbyt wielu parametrów do konstruktora jest brzydkie, więc pewnie będziesz to robił inaczej jakoś - może będziesz ustawiał później kolejne właściwości po kolei, może załadujesz hurtem parametry z pliku tekstowego itp. W każdym razie odpada argument o tym, że new Cat jest ładniejsze, bo jest ładniejsze dopóki nie robisz nic złożonego.

Swoją drogą wydaje mi się, że wpisywanie danych domyślnych do klas jest nadużyciem mechanizmu klas i takie rzeczy ładniej byłoby rozwiązać przez jakieś fabryki, mixiny czy coś takiego (w końcu klasy mają niby zachowanie dziedziczyć, a nie po prostu ustawiać parametry?). Nie wiem jak w Javie, ale w JS by się to łatwo rozwiązało w ten sposób np.

function createCat(params) {
    const defaultParams = {legs: 4, eyes: 2, voice: 'meow'}; 
    // tworzy nowy obiekt i wrzuca do niego najpierw `defaultParams`, 
    // a potem nadpisuje za pomoca `params` tymi parametrami, ktore są tam obecne
    return Object.assign({}, defaultParams, params); 
}
const cat = createCat({voice: 'jestem kotem, który gada'})
0
Zimny Terrorysta napisał(a):

Mamy stworzyć np zarządzanie ludźmi, którzy będą się różnili ze względu na kontynenty. Zgodnie z poprawnymi zasadami projektowania powinno to wyglądać tak:

  • klasa abstrakcyjna Human ze wspólnymi właściwościami i metodami
  • klasy dziedziczące, czyli EuropeHuman, AfricaHuman, AsiaHuman itd

Najpierw musisz wiedzieć co chcesz stworzyć. Bez jasnego celu każdy model będzie dobry/zły. Umowa, Aneks, Faktura - to będzie coś innego z perspektywy systemu zarządzania dokumentami, a coś innego z perspektywy systemu rozliczeniowego.

W kontekście zarządzania ludźmi, wiesz jak działa zarządzanie zasobami ludzkimi? Bez tej wiedzy będziesz tworzył jakieś oderwane od rzeczywistości modele.

Patrząc ogólnie, to możesz mieć różnice w strukturze i/lub zachowaniu. Modelujesz te różnice z jednego powodu, jest to ważne w kontekście rozwiązania problemu.

Co odzwierciedla to dziedziczenie w Twoim przykładzie? Gdzie ta różnica dla HRów między Europejczykiem, a Amerykaninem?

1
Zimny Terrorysta napisał(a):

Mamy stworzyć np zarządzanie ludźmi, którzy będą się różnili ze względu na kontynenty. Zgodnie z poprawnymi zasadami projektowania powinno to wyglądać tak:

  • klasa abstrakcyjna Human ze wspólnymi właściwościami i metodami
  • klasy dziedziczące, czyli EuropeHuman, AfricaHuman, AsiaHuman itd

Ale zwykle korzysta się z jakiegoś frameworka i ORMa gdzie łatwo zrobić CRUDa i dochodzi kwestia wydajności bazy danych. Łatwiej żeby to wszystko było w jednej tabeli i miało tylko różny kontynent, bo dużo szybciej się coś wyszuka.

I zwykle korzysta się bez sensu. Pomijając wszystkie inne już podane sensowne argumenty - najprawdopodobniej nie potrzebujesz bazy danych SQL.
Jakbyś potrzebował to wiedziałbyś co zrobić :-)

#DROPDATABASE

1 użytkowników online, w tym zalogowanych: 0, gości: 1