Dlaczego moja klasa jest gorsza niż hierarchia klas w książce (początkujący OOP)?

11

Czytam Obiekty PHP, Wzorce i Praktykę . Autor próbuje modelować lekcję na uczelni. Celem jest przedstawienie rodzaju lekcji (wykładu lub seminarium) oraz opłat za lekcję w zależności od tego, czy jest to lekcja godzinowa, czy stała. Tak więc wynik powinien być

Lesson charge 20. Charge type: hourly rate. Lesson type: seminar.
Lesson charge 30. Charge type: fixed rate. Lesson type: lecture.

gdy dane wejściowe są następujące:

$lessons[] = new Lesson('hourly rate', 4, 'seminar');
$lessons[] = new Lesson('fixed rate', null, 'lecture');

Ja to napisałem:

class Lesson {
    private $chargeType;
    private $duration;
    private $lessonType;

    public function __construct($chargeType, $duration, $lessonType) {
        $this->chargeType = $chargeType;
        $this->duration = $duration;
        $this->lessonType = $lessonType;
    }

    public function getChargeType() {
        return $this->getChargeType;
    }

    public function getLessonType() {
        return $this->getLessonType;
    }

    public function cost() {
        if($this->chargeType == 'fixed rate') {
            return "30";
        } else {
            return $this->duration * 5;
        }
    }
}

$lessons[] = new Lesson('hourly rate', 4, 'seminar');
$lessons[] = new Lesson('fixed rate', null, 'lecture');

foreach($lessons as $lesson) {
    print "Lesson charge {$lesson->cost()}.";
    print " Charge type: {$lesson->getChargeType()}.";
    print " Lesson type: {$lesson->getLessonType()}.";
    print "<br />";
}

Ale zgodnie z książką się mylę (jestem pewien, że też). Zamiast tego autor podał dużą hierarchię klas jako rozwiązanie. W poprzednim rozdziale autor podał następujące „cztery drogowskazy” jako czas, w którym powinienem rozważyć zmianę struktury mojej klasy:

Jedyny problem, jaki widzę, to stwierdzenia warunkowe, i to również w niejasny sposób - więc po co to refaktoryzować? Jak myślisz, jakie problemy mogą pojawić się w przyszłości, których nie przewidziałem?

Aktualizacja : zapomniałem wspomnieć - jest to struktura klas, którą autor przedstawił jako rozwiązanie - wzorzec strategii :

Wzór strategii

Aditya MP
źródło
29
Nie ucz się programowania obiektowego z książki PHP.
giorgiosironi
4
@giorgiosironi Zrezygnowałem z oceniania języków. Używam PHP na co dzień, więc jest to dla mnie najszybszy sposób na naukę nowych pojęć. Zawsze są ludzie, którzy nienawidzą języka (Java: slidesha.re/91C4pX ) - wprawdzie trudniej jest znaleźć hejtery Java niż disergery PHP, ale nadal. Mogą występować problemy z OO PHP, ale na razie nie mogę poświęcić czasu na naukę składni Java, „sposobu Java” i OOP. Poza tym programowanie na pulpicie jest mi obce. Jestem kompletną osobą internetową. Ale zanim przejdziesz do JSP, podobno musisz znać Java na pulpicie (bez cytowań, czy się mylę?)
Aditya MP
Zamiast ciągów używaj stałych dla „stałej stawki” / „stawki godzinowej” i „seminarium” / „wykładu”.
Tom Marthenal,

Odpowiedzi:

19

To zabawne, że książka nie podaje tego jasno, ale powodem, dla którego faworyzuje hierarchię klas, jeśli instrukcje w tej samej klasie jest prawdopodobnie zasada Otwarta / Zamknięta Ta powszechnie znana reguła projektowania oprogramowania stanowi, że klasa powinna być zamknięta dla modyfikacja, ale otwarta do rozszerzenia.

W twoim przykładzie dodanie nowego typu lekcji oznaczałoby zmianę kodu źródłowego klasy Lekcji, czyniąc ją delikatną i podatną na regresję. Jeśli miałeś klasę podstawową i jedną pochodną dla każdego rodzaju lekcji, zamiast tego musiałbyś po prostu dodać kolejną podklasę, która jest ogólnie uważana za czystszą.

Możesz umieścić tę zasadę w sekcji „Oświadczenia warunkowe”, jeśli chcesz, jednak uważam, że „drogowskaz” jest nieco niejasny. Jeśli stwierdzenia są na ogół tylko zapachem kodu, objawy. Mogą wynikać z wielu złych decyzji projektowych.

guillaume31
źródło
3
O wiele lepszym pomysłem byłoby użycie rozszerzenia funkcjonalnego niż dziedziczenia.
DeadMG,
6
Istnieją z pewnością inne / lepsze podejścia do problemu. Jest to jednak wyraźnie książka o programowaniu obiektowym i otwartego / zamkniętego zasadzie tylko uderzyło mnie jak na klasycznej drodze zbliża się ten rodzaj konstrukcji dylemat w OO.
guillaume31
Co powiesz na najlepszy sposób podejścia do problemu ? Nauczanie gorszego rozwiązania nie ma sensu tylko dlatego, że jest to OO.
DeadMG
16

Myślę, że celem tej książki jest unikanie takich rzeczy jak:

public function cost() {
    if($this->chargeType == 'fixed rate') {
        return "30";
    } else {
        return $this->duration * 5;
    }
}

Moja interpretacja książki jest taka, że ​​powinieneś mieć trzy klasy:

  • Streszczenie Lekcja
  • HourlyRateLesson
  • FixedRateLesson

Następnie należy ponownie zaimplementować costfunkcję w obu podklasach.

Moja osobista opinia

dla takich prostych przypadków: nie. To dziwne, że twoja książka preferuje głębszą hierarchię klas, aby uniknąć prostych instrukcji warunkowych. Co więcej, przy specyfikacji wejściowej Lessonbędzie musiała być fabryka z wysyłką, która jest instrukcją warunkową. Dzięki rozwiązaniu książkowemu będziesz miał:

  • 3 klasy zamiast 1
  • 1 poziom dziedziczenia zamiast 0
  • ta sama liczba instrukcji warunkowych

To większa złożoność bez dodatkowych korzyści.

Simon Bergot
źródło
8
W tym przypadku tak, to zbyt skomplikowane. Ale w większym systemie, będzie dodanie większej ilości lekcji, jak SemesterLesson, MasterOneWeekLessonitp abstrakcyjna klasa korzeń, albo jeszcze lepiej interfejs, jest zdecydowanie do zrobienia potem. Ale kiedy masz tylko dwie sprawy, autor może według własnego uznania.
Michael K,
5
Zgadzam się z tobą ogólnie. Jednak przykłady edukacyjne są często wymyślone i nadbudowane w celu zilustrowania pewnej kwestii. Przez chwilę ignorujesz inne uwagi, aby nie pomylić omawianego problemu, i przedstawisz je później.
Karl Bielefeldt,
+1 Niezły gruntowny podział. Komentarz „Większa złożoność bez dodatkowych korzyści” doskonale ilustruje koncepcję „kosztów alternatywnych” w programowaniu. Teoria! = Praktyczność.
Evan Plaice,
7

Przykład w książce jest dość dziwny. Z twojego pytania oryginalne oświadczenie brzmi:

Celem jest przedstawienie typu lekcji (wykładu lub seminarium) oraz opłat za lekcję w zależności od tego, czy jest to lekcja godzinowa, czy stała.

co w kontekście rozdziału o OOP oznaczałoby, że autor prawdopodobnie chce mieć następującą strukturę:

+ abstract Lesson
    - Lecture inherits Lesson
    - Seminar inherits Lesson

+ abstract Charge
    - HourlyCharge inherits Charge
    - FixedCharge inherits Charge

Ale potem pojawia się cytowany przez Ciebie wkład:

$lessons[] = new Lesson('hourly rate', 4, 'seminar');
$lessons[] = new Lesson('fixed rate', null, 'lecture');

tzn. coś, co nazywa się kodem typu ciągłego i które, szczerze mówiąc, w tym kontekście, gdy czytelnik ma się uczyć OOP, jest okropne.

Oznacza to również, że jeśli mam rację co do intencji autora książki (tj. Tworzenia tych klas abstrakcyjnych i dziedziczonych, które wymieniłem powyżej), doprowadzi to do zduplikowania i dość nieczytelnego i brzydkiego kodu:

const string $HourlyRate = 'hourly rate';
const string $FixedRate = 'fixed rate';

// [...]

Charge $charge;
switch ($chargeType)
{
    case $HourlyRate:
        $charge = new HourlyCharge();
        break;

    case $FixedRate:
        $charge = new FixedCharge();
        break;

    default:
        throw new ParserException('The value of chargeType is unexpected.');
}

// Imagine the similar code for lecture/seminar, and the similar code for both on output.

Jeśli chodzi o „drogowskazy”:

  • Powielanie kodu

    Twój kod nie ma duplikatu. Kod, którego autor oczekuje od ciebie, pisze.

  • Klasa, która za dużo wiedziała o swoim kontekście

    Twoja klasa po prostu przekazuje ciągi od wejścia do wyjścia i nic nie wie. Z drugiej strony, oczekiwany kod może być krytykowany za zbyt dużą wiedzę.

  • The Jack of All Trades - Klasy, które próbują robić wiele rzeczy

    Ponownie, tylko przekazujecie sznurki, nic więcej.

  • Instrukcje warunkowe

    W twoim kodzie jest jedna instrukcja warunkowa. Jest czytelny i łatwy do zrozumienia.


Być może autor spodziewał się, że napiszesz kod o wiele bardziej przebudowany niż ten z sześcioma klasami powyżej. Na przykład możesz użyć wzoru fabrycznego do lekcji i opłat itp.

+ abstract Lesson
    - Lecture inherits Lesson
    - Seminar inherits Lesson

+ abstract Charge
    - HourlyCharge inherits Charge
    - FixedCharge inherits Charge

+ LessonFactory

+ ChargeFactory

+ Parser // Uses factories to transform the input into `Lesson`.

W takim przypadku skończysz z dziewięcioma klasami, które będą doskonałym przykładem nadmiaru architektury .

Zamiast tego otrzymałeś łatwy do zrozumienia, czysty kod, który jest dziesięć razy krótszy.

Arseni Mourzenko
źródło
Wow, twoje domysły są precyzyjne! Spójrz na obraz, który przesłałem - autor polecił dokładnie te same rzeczy.
Aditya, poseł
I byłoby tak kochać, aby zaakceptować tę odpowiedź, ale również jak na @ ian31 odpowiedzi :( Och, co mam robić!
Aditya MP
5

Przede wszystkim możesz zmienić „magiczne liczby”, takie jak 30 i 5 w funkcji cost (), na bardziej znaczące zmienne :)

I szczerze mówiąc, uważam, że nie powinieneś się tym martwić. Kiedy będziesz musiał zmienić klasę, zmienisz ją. Najpierw spróbuj stworzyć wartość, a następnie przejdź do refaktoryzacji.

A dlaczego myślisz, że się mylisz? Czy ta klasa nie jest dla ciebie „wystarczająco dobra”? Dlaczego martwisz się o jakąś książkę;)

Michał Franc
źródło
1
Dzięki za odpowiedź! Rzeczywiście, refaktoryzacja nastąpi później. Jednak napisałem ten kod do nauki, próbując rozwiązać problem przedstawiony w książce na swój własny sposób i zobaczyć, jak się mylę ...
Aditya MP
2
Ale dowiesz się o tym później. Kiedy ta klasa będzie używana w innych częściach systemu i będziesz musiał dodać coś nowego. Nie ma rozwiązania „srebrnej kuli” :) Coś może być dobrego lub złego, zależy to od systemu, wymagań itp. Podczas nauki OOP musisz dużo zawieść: P Potem z czasem zauważysz kilka typowych problemów i wzorców. Ten przykład jest zbyt prosty, aby dać jakąś radę. Ohh, naprawdę polecam ten post od Joela :) Architekci astronauci przejmują joelonsoftware.com/items/2008/05/01.html
Michał Franc
@adityamenon Twoja odpowiedź brzmi: „refaktoryzacja przyjdzie później”. Dodaj inne klasy tylko wtedy, gdy są potrzebne, aby uprościć kod - zwykle wtedy pojawia się trzeci podobny przypadek użycia. Zobacz odpowiedź Simona.
Izkata,
3

Absolutnie nie ma potrzeby implementowania żadnych dodatkowych klas. Masz trochę MAGIC_NUMBERproblemu w swojej cost()funkcji, ale to wszystko. Wszystko inne jest nadmierną inżynierią. Niestety często zdarza się, że udzielane są bardzo złe rady - na przykład Circle dziedziczy Kształt. Zdecydowanie nie jest efektywne w żaden sposób wyprowadzanie klasy dla prostego dziedziczenia jednej funkcji. Zamiast tego możesz zastosować funkcjonalne podejście, aby je dostosować.

DeadMG
źródło
1
To jest interesujące. Czy możesz wyjaśnić, jak byś to zrobił na przykładzie?
guillaume31
+1, zgadzam się, nie ma powodu, aby nadmiernie konstruować / komplikować tak małą funkcjonalność.
GrandmasterB,
@ ian31: Co konkretnie zrobić?
DeadMG
@DeadMG „zastosuj funkcjonalne podejście”, „zastosuj funkcjonalne rozszerzenie zamiast dziedziczenia”
guillaume31,