web-dev-qa-db-ja.com

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

私は 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つの道標」を述べました。

私が見ることができる唯一の問題は条件付きステートメントであり、それも漠然としています-なぜこれをリファクタリングするのですか?私が予見していなかった将来、どのような問題が発生する可能性があると思いますか?

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

The strategy pattern

11
Aditya M P

この本では明確に述べられていないのはおかしいですが、同じクラス内のステートメントがif Open/Closed主義)であるかどうかよりもクラス階層を支持する理由はおそらく---(Open/Closed主義 この広く知られているソフトウェア設計ルールはクラスは変更に対して閉じている必要がありますが、拡張のために開く必要があります。

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

このルールは、「条件付きステートメント」セクションに含めることができますが、「道標」は少し曖昧だと思います。文が一般的に単なるコードの匂い、症状である場合。それらは、さまざまな設計上の誤った決定から生じる可能性があります。

19
guillaume31

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

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

本の私の解釈は、あなたは3つのクラスを持つべきであるということです:

  • アブストラクトレッスン
  • HourlyRateLesson
  • FixedRateLesson

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

私の個人的な意見

そのような単純なケースでは:しないでください。単純な条件文を避けるために、本がより深いクラス階層を優先しているのは奇妙です。さらに、入力仕様では、Lessonは条件付きステートメントであるディスパッチを備えたファクトリでなければなりません。したがって、本のソリューションでは次のようになります。

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

これは、追加の利点がなく、より複雑です。

16
Simon Bergot

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

目標は、レッスンの種類(講義またはセミナー)と、時間単位のレッスンか固定料金のレッスンかによって、レッスンの料金を出力することです。

これは、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');

つまり、stringly typedコードと呼ばれるものであり、正直なところ、このコンテキストでは、読者がOOPを学習することを意図している場合、それは恐ろしいものです。

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

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.

「道標」について:

  • コードの重複

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

  • 彼のコンテキストについて知りすぎたクラス

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

  • Jack of All Trades-多くのことをしようとするクラス

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

  • 条件付きステートメント

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


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

+ 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つのクラスが作成されます。これはover-architectureの完璧な例になります。

代わりに、理解が容易でクリーンなコードになり、10分の1になりました。

7

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

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

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

5
Michal Franc

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

3
DeadMG