Kwadrat dziedziczy po prostokącie - pytanie

Odpowiedz Nowy wątek
2020-04-08 08:58

Rejestracja: 2 miesiące temu

Ostatnio: 1 miesiąc temu

0

Powiedzcie,czy dobrze to rozumuje i czy kod jest poprawny

Skoro każdy kwadrat jest prostokątem,ale nie każdy prostokąt kwadratem to kwadrat jest szczególnym przypadkiem prostokąta. W związku z tym klasa kwadrat dziedziczy od klasy prostokąt.

Oto kod.Zmienne odpowiadające za pozycje oraz bok A są chronione,ponieważ cecha dotycząca pozycji jest wspólna tak samo bok jest cechą wspólną - w przypadku prostokąta boki można opisać dwoma zmiennymi (4 boki,ale 2 pary równych) a w przypadku kwadratu jedną zmienną (4 boki tej samej dlugosci),dlatego w klasie kwadratu nie trzeba definiować zmiennej odpowiedzialnej za bok.

#include <iostream>

class Rectangle
{
    float sideB;
protected:
    int posX,posY,sideA;
public:
    Rectangle(): posX(10),posY(10){}
    inline void setSides(int a,int b){sideA=a;sideB=b;}
    inline float getSquare()const{return sideA*sideB;}
    inline int getPosX()const{return posX;}
    inline int getPosY()const{return posY;}
};

class Square: public Rectangle
{
public:
    Square():Rectangle(){posX=20;posY=20;}
    inline void setSide(int a){sideA=a;}
    inline float getSquare()const{return sideA*sideA;}
    inline int getPosX()const{return posX;}
    inline int getPosY()const{return posY;}
};

int main(int argc, char  *argv[])
{
    Rectangle simpleRectangle;
    Square simpleSquare;

    float a,b;
    std::cout<<"Podaj bok a prostokata: ";
    std::cin >> a;
    std::cout<<"Podaj bok b prostokata: ";
    std::cin >> b;
    simpleRectangle.setSides(a,b);

    std::cout<<"Podaj bok kwadratu: ";
    std::cin >> a;
    simpleSquare.setSide(a);

    std::cout<<"Pole prostokata na pozycji ("<<simpleRectangle.getPosX()<<","<<simpleRectangle.getPosY()<<") wynosi: "<<simpleRectangle.getSquare()<<'\n';
    std::cout<<"Pole kwadratu na pozycji ("<<simpleSquare.getPosX()<<","<<simpleSquare.getPosY()<<") wynosi: "<<simpleSquare.getSquare()<<std::endl;

    return 0;
}

Zmartwiło mnie to,że klasa kwadratu nie ma zadnych zmiennych dlatego pytanie kieruje do Was czy ten kod jest poprawny.

Pozostało 580 znaków

2020-04-08 09:02
Moderator

Rejestracja: 12 lat temu

Ostatnio: 8 godzin temu

Lokalizacja: Wrocław

7

Możesz zrobić nielegalny kwadrat poprzez:

simpleSquare.setSides(a, a + 1);

... zatem nie jest to prawidłowy design.

Nie jesteś pierwszą, która wpadła na takie wykorzystanie dziedziczenia - ten problem ma nawet swoją nazwę: circle-ellipse problem :-)

Rozwiązaniem jest niewykorzystywanie na siłę dziedziczenia tam, gdzie ono nie pasuje - zwróć szczególną uwagę na sekcję Challenge the premise of the problem w zalinkowanym przeze mnie artykule.


edytowany 7x, ostatnio: Patryk27, 2020-04-08 09:23

Pozostało 580 znaków

2020-04-08 09:07

Rejestracja: 2 miesiące temu

Ostatnio: 1 miesiąc temu

0

Hm to jak rozumować dziedziczenie ?
Na takiej zasadzie: Zwierze -> Kot, Ksztalt->Prostokat, Liczby rzeczywiste -> Liczby naturalne

Pokaż pozostałe 5 komentarzy
A więzień nie powinien dziedziczyć po osobie, bo przecież więzień to mutant. - Pebal 2020-04-08 11:45
Nic takiego nie powiedziałem - https://pl.wikipedia.org/wiki/Sofizmat_rozszerzenia. Jeśli chcemy, aby ktokolwiek coś z tego wszystkiego wyniósł, trzymajmy się pewnych standardów rozmowy. - Patryk27 2020-04-08 11:45
@Patryk27 Przecież to jest analogiczna konstrukcja, przedstawiona na Wiki. - Pebal 2020-04-08 11:54
@Pebal: gdzie na Wikipedii jest napisane, aby więzień dziedziczył po mutancie? - Patryk27 2020-04-08 11:56
@Patryk27 Nigdzie nie jest to napisane i chyba nikt nie odebrał tego poważnie. Opisano problem, który moim zdaniem można rozwiązać w znacznie prostszy sposób, aniżeli usunięcie dziedziczenia klasy osoby przez klasę więźnia. To zrodziłoby znacznie więcej problemów. - Pebal 2020-04-08 12:59

Pozostało 580 znaków

2020-04-08 09:22

Rejestracja: 2 miesiące temu

Ostatnio: 1 miesiąc temu

0

Dziękuje za wyjaśnienie!

Pozostało 580 znaków

2020-04-08 09:34

Rejestracja: 12 lat temu

Ostatnio: 10 godzin temu

5

to jest naruszenie Liskov substitution principle
Polecam obejrzeć, bo tu jest dokładnie wytłumaczone co jest nie tak, z Kwadratem dziedziczącym po Prostokącie:


Jeśli chcesz pomocy, NIE pisz na priva, ale zadaj dobre pytanie na forum.
edytowany 3x, ostatnio: MarekR22, 2020-04-08 18:14
Pokaż pozostałe 13 komentarzy
@Pebal: ja tak robię i u mnie się to sprawdza nie jest żadnym argumentem; każdy z nas mógłby powiedzieć dokładnie to samo: a ja tak _nie robię_ i u mnie _to_ się świetnie sprawdza. Podsyłamy Ci filmiki, linki oraz wytykamy dziury związane z takim sposobem dziedziczenia, na co Twoją linią obrony jest otwórz sobie program graficzny i narysuj kwadrat czy nie zajmujcie się bzdurnymi teoriami. To nie jest rozmowa - mam wrażenie, że Ty chcesz powiedzieć swoje i "mieć to z głowy", a nie faktycznie podebatować nad skądinąd ciekawym problemem :-) A skoro tak, no to EOT. - Patryk27 2020-04-08 12:07
@Patryk27 Kolejny. A jakieś konkrety? Nie podsyłaj linków tylko argumentuj własnym doświadczeniem. To forum a nie wykop. - Pebal 2020-04-08 12:11
@Pebal jak napisałem EOT (end of topic), znaczy, że dla mnie to jest "flame war", na który nie mam ochoty, więc nie wciągaj mnie już do dyskusji. Proszę nie przywoływać mnie do tego wątku za pomocą "@". - MarekR22 2020-04-08 12:12
@MarekR22: Liskov to nazwisko, nie ma potrzeby pisać całości wielką literą. - enedil 2020-04-08 12:18
w dodatku nazwisko kobiety, więc nie odmieniamy na "zasadę Liskova". - Azarien 2020-04-08 13:41

Pozostało 580 znaków

2020-04-08 09:42
Moderator

Rejestracja: 16 lat temu

Ostatnio: 2 godziny temu

9
  1. W ogóle należy dwa razy sie zastanowic zanim zrobi się jakiekolwiek dziedziczenie ;) W większośći przypadków kompozycja jest lepszym rozwiązaniem
  2. Bardziej ogólnie: należy patrzeć na relacje między obiektami na podstawie przejawianego ZACHOWANIA (z tym tez wiąże się wspomniana wyżej zasada LSP). Niby każdy kwadrat jest prostokątem, ale czy kwadrat przedstawia takie samo zachowanie jak prostokąt? Jeśli masz metodę która pozwala ustawić długość dwóch boków, to siłą rzeczy takie zachowanie nie jest wspólne dla kwadrata i prostokąta. Wiec czy kwadrat może dziedziczyć po prostokącie? Tak, ale tylko jeśli interfejs prostokąta zawiera jedynie metody, które są poprawnym zachowaniem także dla kwadratów.

Masz problem? Pisz na forum, nie do mnie. Nie masz problemów? Kup komputer...
W kwestii pkt.2 mam pytanie. Czy mogę dać komuś 10zł nie wiedząc, że ta osoba zachowa tylko 5zł? Czy jednak powinienem o tym wiedzieć i taką osobę trzymać w specjalnej grupie i traktować inaczej? - Pebal 2020-04-08 13:08

Pozostało 580 znaków

2020-04-08 09:52

Rejestracja: 5 lat temu

Ostatnio: 11 godzin temu

2

Dziedziczenie przydaje się w sytuacji, kiedy mamy do czynienia z funkcją ( tutaj calculateArea ) posiadającą różną implementację dla poszczególnych obiektów ( Rectangle , Circle ), które można związać ze sobą za pomocą dziedziczenia interfejsu Shape.

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

struct Point
{
    double x {0};
    double y {0};
};

class Shape
{
public:
    virtual double calculateArea() const = 0;
    virtual ~Shape(){};
};

class Rectangle : public Shape
{
public:
    Rectangle( Point position_ , Point sides_ ): position {position_} , sides {sides_} {}
    double calculateArea() const override { return sides.x*sides.y; };

private:
    Point position;
    Point sides;
};

class Circle : public Shape
{
public:
    Circle( Point position_ , double radius_ ): position {position_} , radius {radius_} {}
    double calculateArea() const override { return 3.14159*radius*radius; };

private:
    Point position;
    double radius;
};

int main()
{
    vector<unique_ptr<Shape>> shapes;

    shapes.emplace_back(make_unique<Rectangle>(Point{0,0},Point{10,20}));
    shapes.emplace_back(make_unique<Circle>(Point{0,0},5));

    for( const auto& shape : shapes )
    {
        cout << "Area of shape = " << shape->calculateArea() << "\n";
    }

    return 0;
}
edytowany 2x, ostatnio: TomaszLiMoon, 2020-04-08 10:54

Pozostało 580 znaków

2020-04-08 10:34

Rejestracja: 7 lat temu

Ostatnio: 12 godzin temu

0

Jak dla mnie kwadrat jest prostokątem i dziedziczenie jest tutaj ok. Trzeba je tylko poprawnie zaimplementować:

class Rectangle
{
public:
    Rectangle(): posX(10),posY(10){}
    virtual ~Rectangle(){}
    virtual void setSides(int a,int b){sideA=a;sideB=b;}
    inline float getSquare()const{return sideA*sideB;}
    inline int getPosX()const{return posX;}
    inline int getPosY()const{return posY;}
protected:
    int posX,posY,sideA,sideB;
};

class Square: public Rectangle
{
public:
    Square(){posX=20;posY=20;}
    inline void setSide(int a){sideA=sideB=a;}
protected:
    void setSides(int a,int b) override {sideA=sideB=std::min(a,b);}
};

Analogiczne rozwiązanie dla "Challenge the premise of the problem" z Wiki:

class Person
{
    virtual void walkNorth(int meters) {...}
    virtual void walkEast(int meters) {...}
};

class Prisoner: Person
{
    void walkNorth(int meters) override {...}
    void walkEast(int meters) override {...}
};
Pokaż pozostałe 19 komentarzy
@xxx_xx_x, Nie byłoby postępu, gdyby nie łamało się zasad. Ale tu akurat nie ma żadnego łamania zasad, bo nawet Wiki mówi, że można tak dziedziczyć. Wiąże się to z pewnymi problemami, ale brak dziedziczenia generuje inne problemy. Nie istnieje jedna, złota metoda. - Pebal 2020-04-08 15:22
@Pebal: wybacz, ale wiki to dla mnie żadne źródło.I masz łamanie zasada Liskov - xxx_xx_x 2020-04-08 15:28
@xxx_xx_x, Wiesz jak działa std::variant i jak on się ma do zasady Liskov? - Pebal 2020-04-08 16:01
void setSides(int a,int b) override {sideA=sideB=std::min(a,b);} czyli ustawiam boki kwadratu na 2 i 3, a kończę z bokami długości 2. Co to ma wspólnego z poprawną implementacją? Chyba jedyne w miarę sensowne rozwiązanie to rzucanie wyjątku, jeśli boki różnią się od siebie o więcej niż eps albo mają niedodatnią wartość. - ŁF 2020-04-08 22:15
@ŁF Miałeś kiedyś do czynienia z GUI i layoutami? Tam takich zachowań jest na pęczki, są one poprawne i oczekiwane. - Pebal 2020-04-08 22:19

Pozostało 580 znaków

2020-04-08 10:40

Rejestracja: 5 lat temu

Ostatnio: 4 minuty temu

3
Pebal napisał(a):

Jak dla mnie kwadrat jest prostokątem i dziedziczenie jest tutaj ok. Trzeba je tylko poprawnie zaimplementować:

A dlaczego uważasz, że "poprawne" jest "min", a nie "max"?

Nie ma to większego znaczenia czy będzie min(a, b), max(a, b), a czy b. Z doświadczenia jednak wiem, że min jest najbardziej przydatne, gdyż kwadrat jest wpisany w teoretyczny prostokąt. - Pebal 2020-04-08 10:50
Chcesz przez to powiedzieć, że zachowanie nie ma większego znaczenia i brak jednoznaczności nie jest problemem? - yarel 2020-04-08 10:59
Jak już wcześniej napisałem, kwadrat wprowadza pewne ograniczenia na prostokąt. To Ty decydujesz jakie te ograniczenia są. Dlaczego min? Weź dowolny program graficzny i spróbuj narysować kwadrat narzędziem do rysowania prostokątów (trzymając SHIFT, lub inny klawisz to tego dedykowany). - Pebal 2020-04-08 11:24

Pozostało 580 znaków

2020-04-08 11:29

Rejestracja: 1 rok temu

Ostatnio: 1 dzień temu

10

@Pebal:
Problemem nie jest wybór pomiędzy min i max. Problemem jest to, że Twój Kwadrat w modyfikuje "kontrakt" zdefiniowany przez Prostokąt.

Co się stanie w takiej sytuacji ?

void test_area(Rectangle & rect)
{
        // Interfejs klasy prostokąt mówi, że możemy dla każdego prostokąta ustawić jego wymiary
        rect.setSides(10, 20);

        // hmm, troszkę nieszczęśliwa nazwa dla funkcji zwracającej pole, ale NVM
        // wiemy, że dla prostokąta którego wymiary sobie skonfigurowaliśmy chwilkę wcześniej pole powinno być równe 200
        assert (rect.getSquare() == 200);
}

int main()
{
        Square s;
        // Skoro każdy kwadrat jest prostokątem, to możemy kwadrat przekazać do funkcji test_area()
        test_area(s);
}
Pokaż pozostałe 4 komentarze
Dlaczego uważasz, że każdą funkcję należy zdefiniować na nowo? Mam funkcję rysującą prostokąt, czy muszę ją modyfikować dla kwadratu? - Pebal 2020-04-08 12:35
@xxx_xx_x To samo co dla więźnia oznacza metoda goToPub, bez argumentów. - Pebal 2020-04-08 12:45
Funkcję rysującą kwadrat powinieneś włączyć do innego interface np RectangularShape i tam ją dodać. A tak w zasadzie to nie powinieneś mieszać obiektu z częścią renderującą. - xxx_xx_x 2020-04-08 12:57
Prisoner i Person mają stan wspólny i wspólne zachowania wiec w ich przypadku dziedziczenie ma jakiś sens. GoToPub będzie dla obu bezargumentowa, dla kwadratu masz metodę dwuargumentową gdzie oba boki muszą mieć taką samą długość i w żaden sposób nie wymusisz tego jeśli ktos zrobi cast na rectangle, naprawde nie widzisz różnicy? - xxx_xx_x 2020-04-08 13:00
@xxx_xx_x, Problem z więźniem jest analogiczny. Masz metodę walkNorth, która dla więźnia może wpisać 0. Zaś z rzutowaniem nie będzie problemu, bo metoda setSides jest wirtualna i zrobi to co ma zrobić. Zresztą, może zrobić cokolwiek, bo do tego właśnie służą metody. - Pebal 2020-04-08 13:58

Pozostało 580 znaków

2020-04-08 13:18

Rejestracja: 1 rok temu

Ostatnio: 1 dzień temu

6

@Pebal: Za długie mi wyszło na komentarz

Jakiego rysowania? W kodzie który pokazałeś nie było nic o rysowaniu, było za to liczenie pola. Skąd mogę wiedzieć, czy Twoja funkcja rysująca będzie wymagać zmodyfikowania?
Jeśli nie dotyka tych elementów klasy dla których zmieniłeś kontrakt - to pewnie nie wymaga.
Ale ja tego nie wiem. - i to na tym polega głównie problem. Definiując klasę kwadrat w taki sposób jak to zrobiłeś świadomie złamałeś kontrakt klasy prostokąt.

Robisz to w swoim kodzie który ma kilkaset linijek - ok, to nie (MÓJ) problem. Ale być może Rectangle to klasa którą dostałeś z jakiejś zewnętrznej biblioteki, być może jest używana w projekcie który ma już kilkaset tysięcy linii kodu ? Co wtedy ? Czy mogę wszystkich tych funkcji bezpiecznie używać z Twoim Kwadratem ?

Reguła susbstytucji Liskov mówi (w uproszczeniu), że cały ten kod powinien działać poprawnie jeśli dasz mu obiekt typu pochodnego. W przypadku Twojego kwadratu takiej gwarancji nie ma.

@xxx_xx_x:
To w zasadzie dokładnie taka sama sytuacja.

Jeśli z jakichś powodów dla klasy Person zdefiniowano metodę goToPub() i w kontrakcie ustalono że ta metoda zawsze działa poprawnie, to definiując klasę Prisoner jako klasę pochodną od Person i redefiniując jej tą metodę w poniższy sposób też zmieniasz kontrakt ;)

void Prisoner::goToPub() override
{
  throw PermissionDeniedException();
}
Z tym prisoner i tak i nie bo możesz założyć że metoda może sie nie powieść / zgłosić błąd. Np brak ścieżki lub brak pozwolenia na wyjście z celi ;) - xxx_xx_x 2020-04-08 13:26
Takiego rysowania, jakie sobie wymyślę. Mam Ci podać inny przykład? A z tą metodą goToPub to bym tak nie przesadzał, bo więźniowie mogą mieć swój "pub". - Pebal 2020-04-08 14:02

Pozostało 580 znaków

Odpowiedz

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

Robot: Bingbot