私のクラスが本のクラスの階層よりも悪いのはなぜですか(初心者OOP)?


11

PHPオブジェクト、パターン、およびプラクティスを読んでいます。著者は大学での授業をモデル化しようとしています。目標は、レッスンタイプ(講義またはセミナー)と、レッスンが時間制レッスンか固定価格レッスンかに応じて、レッスンの料金を出力することです。したがって、出力は次のようになります

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

入力が次の場合:

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

私はこれを書いた:

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 />";
}

しかし、本によると、私は間違っています(私もかなり確信しています)。代わりに、著者は解決策としてクラスの大きな階層を与えました。前の章で、著者はクラス構造の変更を検討すべき時期として次の「4つの道標」を述べました。

私が見ることができる唯一の問題は条件文であり、それもあいまいな方法で-それでなぜこれをリファクタリングするのですか?私が予見していなかった将来、どんな問題が起こると思いますか?

更新:言及するのを忘れました-これは著者が解決策として提供したクラス構造です- 戦略パターン

戦略パターン


29
PHPの本からオブジェクト指向プログラミングを学ぶな。
-giorgiosironi

4
@giorgiosironi言語の評価をあきらめました。私は日常的にPHPを使用しているため、PHPで新しい概念を学ぶのが最速です。言語が嫌いな人は常にいます(Java:slidesha.re/91C4pX)-確かに、PHPの嫌いな人よりもJavaの嫌いな人を見つけるのは難しいですが、それでもです。PHPのオブジェクト指向には問題があるかもしれませんが、現時点では、Java構文、「Javaの方法」、および OOP を学ぶ時間をspareしみません。その上、デスクトッププログラミングは私にとって異質です。私は完全なウェブパーソンです。しかし、JSPに到達する前に、デスクトップJavaを知る必要があると言われています(引用なし、間違っていますか?)
Aditya MP

文字列の代わりに、「固定レート」/「毎時レート」および「セミナー」/「講義」に定数を使用します。
トムマーセナル

回答:


19

この本が明確に述べていないのは面白いですが、同じクラス内のステートメントがif / closed原則よりもクラス階層を好む理由は、おそらくオープン/クローズの原則ですこの広く知られているソフトウェア設計ルールは、クラスを変更が拡張用に開いています。

あなたの例では、新しいレッスンタイプを追加することは、レッスンクラスのソースコードを変更することを意味し、脆弱で回帰しやすくなります。基本クラスと各レッスンタイプの派生物が1つある場合は、代わりに別のサブクラスを追加する必要がありますが、これは一般にクリーナーと見なされます。

そのルールを「条件文」セクションの下に置くことができますが、私はその「道標」は少しあいまいであると考えています。ステートメントが一般的に単なるコードのにおいである場合、症状。それらは、さまざまな悪い設計上の決定から生じる可能性があります。


3
継承ではなく、機能拡張を使用することをお勧めします。
DeadMG

6
問題に対する他の/より良いアプローチは確かにあります。しかし、これは明らかにオブジェクト指向プログラミングに関する本であり、オープン/クローズの原則は、オブジェクト指向におけるそのような設計のジレンマにアプローチする古典的な方法として私を驚かせました。
-guillaume31

問題にアプローチする最良の方法はどうですか?オブジェクト指向だからといって、劣ったソリューションを教える意味はありません。
-DeadMG

16

この本が教えることを目指しているのは、次のようなことを避けることだと思います。

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

この本の解釈では、3つのクラスが必要です。

  • 抽象レッスン
  • HourlyRateLesson
  • FixedRateLesson

その後cost、両方のサブクラスで関数を再実装する必要があります。

私の個人的な意見

そのような単純な場合:しないでください。あなたの本が単純な条件文を避けるためにより深いクラス階層を好むのは奇妙です。さらに、入力仕様Lessonでは、条件付きステートメントであるディスパッチを持つファクトリーである必要があります。したがって、本ソリューションでは次のことができます。

  • 1ではなく3つのクラス
  • 0ではなく1つの継承レベル
  • 同数の条件ステートメント

それは追加の利点を持たないより複雑なものです。


8
ではこのような場合、はい、それはあまりにも複雑です。しかし、より大規模なシステムでSemesterLessonMasterOneWeekLesson、などのレッスンを追加することになります。抽象ルートクラス、またはさらに優れたインターフェイスが間違いなくその方法です。ただし、ケースが2つしかない場合は、作成者の裁量に任されます。
マイケルK

5
私は一般的にあなたに同意します。ただし、教育的な例は、ポイントを説明するために、しばしば考案され、過剰に設計されています。手元の問題を混乱させないために、他の考慮事項をしばらく無視し、それらの考慮事項を後で紹介します。
カールビーレフェルト

+1すばらしい完全な内訳。「それは複雑さを増し、メリットはありません」というコメントは、プログラミングにおける「機会費用」の概念を完全に示しています。理論!=実用性。
エヴァンプレイス

7

本の例はかなり奇妙です。あなたの質問から、元のステートメントは次のとおりです。

目標は、レッスンタイプ(講義またはセミナー)と、レッスンが時間制レッスンか固定価格レッスンかに応じて、レッスンの料金を出力することです。

これは、OOPに関する章の文脈では、著者がおそらく次の構造を持つことを望んでいることを意味します。

+ abstract Lesson
    - Lecture inherits Lesson
    - Seminar inherits Lesson

+ abstract Charge
    - HourlyCharge inherits Charge
    - FixedCharge inherits Charge

しかし、それから引用した入力が来ます:

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

つまり、文字列型コードと呼ばれるものであり、正直なところ、読者がOOPを学習しようとしているときにこのコンテキストでは恐ろしいものです。

これはまた、本の著者の意図が正しい場合(つまり、上記の抽象クラスと継承クラスを作成する場合)、これが重複してかなり読みにくくandいコードにつながることを意味します。

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.

「標識」について:

  • コードの複製

    コードに重複はありません。著者があなたが書くことを期待しているコードはそうします。

  • 彼の文脈について多くを知っていたクラス

    クラスは、入力から出力に文字列を渡すだけで、何も知りません。一方、予想されるコードは、あまりにも多くのことを知っていると批判される可能性があります。

  • すべての取引のジャック-多くのことをしようとするクラス

    繰り返しますが、単に文字列を渡すだけで、それ以上のものはありません。

  • 条件文

    コードには1つの条件ステートメントがあります。読みやすく、理解しやすいです。


実際、著者は、上記の6つのクラスを持つコードよりもはるかに多くのリファクタリングされたコードを書くことを期待していたのかもしれません。たとえば、レッスンや料金などに工場パターンを使用できます。

+ 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`.

この場合、最終的に9つのクラスになります。これは、オーバーアーキテクチャの完璧な例です。

代わりに、わかりやすくクリーンなコードになりました。これは10分の1です。


うわー、あなたの推測は正確です!私がアップロードした画像を見てください-著者はまったく同じことを推奨しました。
アディティアMP

Iしまうので、この答えを受け入れるのが大好き、私も@ ian31の答え:(ああ、私は何をしますかのように!
アーディティヤMP

5

まず、cost()関数の30や5などの「マジックナンバー」をより意味のある変数に変更できます:)

正直に言うと、これについて心配するべきではないと思います。クラスを変更する必要がある場合は、変更します。最初に値を作成してから、リファクタリングに移行してください。

そして、なぜあなたはあなたが間違っていると思っているのですか?このクラスはあなたにとって「十分」ではありませんか?なぜあなたは本を心配しているのですか;)


1
回答ありがとうございます!実際、リファクタリングは後で行われます。しかし、学習のためにこのコードを書いて、本に示されている問題を自分のやり方で解決しようとし、どうやってうまくいかないかを見てみました...
Aditya MP

2
しかし、これは後でわかります。このクラスがシステムの他の部分で使用され、新しいものを追加する必要がある場合。「銀の弾丸」の解決策はありません:)システムや要件などに応じて、良いことも悪いこともあります。OOPを学習していると、多くの失敗が必要になります。この例はあまりにも単純なので、アドバイスをすることはできません。おお私は本当に宇宙飛行士が引き継ぐジョエル:)アーキテクチャからこのポストをお勧めしますjoelonsoftware.com/items/2008/05/01.html
ミハル・フラン

@adityamenonそれからあなたの答えは「リファクタリングは後で来る」です。他のクラスを追加するのは、コードを簡素化するために必要になったときだけです。通常は、3番目の同様のユースケースが登場します。サイモンの答えをご覧ください。
イズカタ

3

追加のクラスを実装する必要はまったくありません。機能に少しMAGIC_NUMBER問題がありますcost()が、それだけです。それ以外は大規模なオーバーエンジニアリングです。ただし、残念なことに、非常に悪いアドバイスが与えられるのは一般的です。たとえば、CircleはShapeを継承します。1つの関数を単純に継承するためのクラスを派生させることは、決して効率的ではありません。代わりに機能的なアプローチを使用してカスタマイズできます。


1
これは面白い。例を使ってそれをどのように行うか説明できますか?
-guillaume31

+1、私は同意します、そのようなわずかな機能を過剰に設計/複雑化する理由はありません。
GrandmasterB

@ ian31:具体的にはどうしますか?
-DeadMG

@DeadMG「機能的アプローチを使用」、「継承ではなく機能的拡張を使用」
-guillaume31
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.