抽象クラスにはどのコードを含める必要がありますか?


9

最近、抽象クラスの使用について困っています。

時々、抽象クラスが事前に作成され、派生クラスがどのように機能するかのテンプレートとして機能します。つまり、多かれ少なかれ、それらはいくつかの高レベルの機能を提供しますが、派生クラスによって実装される特定の詳細を除外します。抽象クラスは、いくつかの抽象メソッドを配置することにより、これらの詳細の必要性を定義します。そのような場合、抽象クラスは設計図、機能の高レベルの説明、またはそれと呼ぶもののように機能します。単独では使用できませんが、高レベルの実装から除外された詳細を定義するために専門化する必要があります。

場合によっては、いくつかの「派生」クラスの作成後に抽象クラスが作成されることがあります(親/抽象クラスがそこにないため、それらはまだ派生されていませんが、私が何を意味するか知っています)。これらの場合、抽象クラスは通常、現在の派生クラスに含まれるあらゆる種類の共通コードを配置できる場所として使用されます。

上記の観察をしたので、私はこれらの2つのケースのどちらがルールであるべきか疑問に思っています。派生クラスすべてに共通しているという理由だけで、抽象クラスに詳細を吹き込む必要がありますか?高レベルの機能の一部ではない一般的なコードがありますか?

たまたま派生クラスに共通しているからといって、抽象クラス自体には意味がないコードが存在する必要がありますか?

例を挙げましょう。抽象クラスAにはメソッドa()と抽象メソッドaq()があります。メソッドaq()は、派生クラスABとACの両方で、メソッドb()を使用します。b()をAに移動する必要がありますか?もしそうなら、誰かがAだけを見ている場合(ABとACがそこにないふりをしよう)、b()の存在はあまり意味がありません!これは悪いことですか?誰かが抽象クラスを見て、派生クラスにアクセスせずに何が起こっているのかを理解できる必要がありますか?

正直なところ、この質問をしている時点で、派生クラスを調べなくても意味のある抽象クラスを書くことは、クリーンなコードとアーキテクチャの問題だと信じがちです。anyあらゆる種類のコードのダンプのように機能する抽象クラスのアイデアが、すべての派生クラスで一般的に発生するのを嫌う

あなたはどう思いますか/練習しますか?


7
なぜ「ルール」が必要なのですか?
ロバート・ハーヴェイ

1
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture. - なぜ?アプリケーションの設計と開発の過程で、いくつかのクラスに共通の機能があり、自然に抽象クラスにリファクタリングできることを発見できませんか?派生クラスのコードを書く前に常にこれを予期できるほど透視力が必要ですか?私がこの慎重さを欠く場合、そのようなリファクタリングを実行することは禁止されていますか?コードを捨ててやり直す必要がありますか?
ロバートハーベイ

ごめんなさい、誤解されたら!私は(今のところ)自分が感じていることをより良い実践として述べようとしており、絶対的なルールがあるべきだという意味ではありません。さらに、抽象クラスに属するコードは、事前にのみ作成する必要があるという意味ではありません。私は、実際には、抽象クラスが高レベルのコード(派生コードのテンプレートとして機能する)と低レベルのコード(ユーザーが見ないでその使いやすさを理解できない)で終わる方法について説明しました派生クラス)。
Alexandros Gougousis 2017年

パブリックベースクラスの@RobertHarveyの場合、派生クラスを見ることが禁止されます。内部クラスの場合、違いはありません。
Frank Hileman、2017年

回答:


4

例を挙げましょう。抽象クラスAにはメソッドa()と抽象メソッドaq()があります。メソッドaq()は、派生クラスABとACの両方で、メソッドb()を使用します。b()をAに移動する必要がありますか?もしそうなら、誰かがAだけを見ている場合(ABとACがそこにないふりをしよう)、b()の存在はあまり意味がありません!これは悪いことですか?誰かが抽象クラスを見て、派生クラスにアクセスせずに何が起こっているのかを理解できる必要がありますか?

あなたの質問はどこに置くb()べきか、そしてある意味では質問はandのA直接のスーパークラスとして最良の選択であるかどうかです。 ABAC

3つの選択肢があるようです。

  1. 休暇b()の両方でABAC
  2. 中間クラスを作成ABAC-Parentから継承していることAと、その紹介はb()、その後のための即時のスーパークラスとして使用されているABAC
  3. 入れb()A(別の将来のクラスがいるかどうかわからないADでしょうb()か)

  1. されていないから被るDRY
  2. YAGNIに苦しむ。
  3. それで、これは残ります。

(3)をAD必要としない別のクラスがb()それ自体を提示するまでは、正しい選択のように見えます。

ADプレゼントのようなときに、(2)のアプローチにリファクタリングできます—結局それはソフトウェアです!


7
4番目のオプションがあります。b()どのクラスにも入れないでください。すべてのデータを引数として取り、両方ABを持つ自由な関数にして、それをAC呼び出します。つまり、を追加するときに、クラスを移動したり、クラスを作成したりする必要はありませんAD
user1118321 2017年

1
@ user1118321、すぐに使える優れた素晴らしい思考。b()インスタンスの長期間有効な状態が必要ない場合に特に意味があります。
Erik Eidt 2017年

(3)で私を困らせているのは、抽象クラスがもはや自己完結型の動作を記述していないことです。それはほんの少しのコードであり、私はこのクラスとすべての派生クラスの間を行き来しないと、抽象クラスコードを理解できません。
Alexandros Gougousis 2017年

抽象的acではbなく呼び出すベース/デフォルトを作成できますacか?
Erik Eidt、2017年

3
私にとって最も重要な質問は、ABとACの両方が同じメソッドb()を使用する理由です。偶然の一致であれば、ABとACに同様の実装を2つ残しておきます。しかし、それはおそらくABとACに共通の抽象概念が存在するためです。私にとって、DRYはそれ自体はそれほど価値のあるものではありませんが、いくつかの有用な抽象化を見逃したことを示唆しています。
ラルフクレバーホフ2017年

2

抽象クラスは、便利であるため、抽象クラスに投入されるさまざまな関数やデータのダンプ場となることを意図していません。

最も信頼性が高く拡張可能なオブジェクト指向のアプローチを提供していると思われる経験則の1つは、「継承よりも構成を優先する」です。抽象クラスは、おそらくコードを含まないインターフェース仕様と考えるのが一番でしょう。

抽象クラスに付随する一種のライブラリメソッドであるメソッドがある場合、抽象クラスから派生したクラスが通常必要とする機能または動作を表現するための最も可能性の高いメソッドであるメソッドを作成すると、このメソッドが利用可能な場合、抽象クラスと他のクラスの間にあるクラス。この新しいクラスは、このメソッドを提供することにより、特定の実装の代替またはパスを定義する抽象クラスの特定のインスタンス化を提供します。

抽象クラスのアイデアは、抽象クラスを実際に実装する派生クラスがサービスまたは動作まで提供することになっているものの抽象モデルを持つことです。継承は使いすぎやすいため、最も有用なクラスは、ミックスインパターンを使用したさまざまなクラスで構成されていることがよくあります

ただし、常に変化の問題があり、何が変化し、どのように変化し、なぜ変化するのかがわかります。

継承はソースの脆弱で壊れやすい本体につながる可能性があります(継承も参照してください:すでに使用を停止してください!)。

壊れやすい基本クラスの問題は、オブジェクト指向プログラミングシステムの基本的なアーキテクチャの問題であり、基本クラス(スーパークラス)は「壊れやすい」と見なされます。 。プログラマは、基本クラスのメソッドを個別に調べるだけでは、基本クラスの変更が安全かどうかを判断できません。


OOPの概念を誤用したり、使いすぎたり、乱用したりすると、問題が発生する可能性があります。さらに、b()がライブラリメソッド(他の依存関係のない純粋な関数など)であると仮定すると、質問の範囲が絞り込まれます。これは避けましょう。
Alexandros Gougousis 2017年

@AlexandrosGougousis私はそれb()が純粋な関数であるとは想定していません。Functoid、テンプレート、またはその他の可能性があります。「ライブラリメソッド」という語句は、純粋な関数、COMオブジェクト、またはソリューションに提供する機能に使用されるものなど、何らかのコンポーネントを意味するものとして使用しています。
Richard Chambers

すみません、よくわからなかったら!純粋な関数については、多くの例の1つとして説明しました(多くの例を挙げました)。
Alexandros Gougousis 2017年

1

あなたの質問は抽象クラスへのどちらかまたはアプローチを示唆しています。しかし、私はあなたがそれらをあなたのツールボックスの単なる別のツールと考えるべきだと思います。そして、問題は次のようになります。どのジョブ/問題に対して、抽象クラスが適切なツールですか?

優れた使用例の1つは、テンプレートメソッドパターンを実装することです。すべての不変ロジックを抽象クラスに入れ、バリアントロジックをそのサブクラスに入れます。共有ロジック自体は不完全で機能しないことに注意してください。ほとんどの場合、それはアルゴリズムの実装に関するものであり、いくつかのステップは常に同じですが、少なくとも1つのステップは異なります。この1つのステップを、抽象クラス内の関数の1つから呼び出される抽象メソッドとして配置します。

時々、抽象クラスが事前に作成され、派生クラスがどのように機能するかのテンプレートとして機能します。つまり、多かれ少なかれ、それらはいくつかの高レベルの機能を提供しますが、派生クラスによって実装される特定の詳細を除外します。抽象クラスは、いくつかの抽象メソッドを配置することにより、これらの詳細の必要性を定義します。そのような場合、抽象クラスは設計図、機能の高レベルの説明、またはそれと呼ぶもののように機能します。単独では使用できませんが、高レベルの実装から除外された詳細を定義するために専門化する必要があります。

最初の例は基本的にテンプレートメソッドパターンの説明だと思います(私が間違っている場合は訂正してください)。これは、抽象クラスの完全に有効なユースケースと見なします。

場合によっては、いくつかの「派生」クラスの作成後に抽象クラスが作成されることがあります(親/抽象クラスがそこにないため、それらはまだ派生されていませんが、私が何を意味するか知っています)。これらの場合、抽象クラスは通常、現在の派生クラスに含まれるあらゆる種類の共通コードを配置できる場所として使用されます。

2番目の例では、抽象クラスの使用は最適な選択ではないと思います。共有ロジックと重複コードを処理するための優れた方法があるからです。抽象クラスA、派生クラスがBありC、両方の派生クラスがメソッドの形でいくつかのロジックを共有しているとしs()ます。重複を取り除くための正しいアプローチを決定するには、メソッドs()がパブリックインターフェイスの一部であるかどうかを知ることが重要です。

そうでない場合(methodを使用した具体的な例のようにb())、ケースは非常に単純です。それから別のクラスを作成し、操作の実行に必要なコンテキストを抽出するだけです。これは、継承を介した構成の典型的な例です。コンテキストがほとんどまたはまったく必要ない場合は、いくつかのコメントで提案されているように、単純なヘルパー関数ですでに十分な場合があります。

s()がパブリックインターフェースの一部である場合は、少しトリッキーになります。仮定の下s()では何の関係もありませんBし、Cに関連しているA、あなたは置くべきではないs()内部A。それでそれをどこに置くのですか?をI定義する別のインターフェースを宣言することについて、私は主張しますs()。その後、再び、あなたが実行するためのロジックが含まれている別のクラスを作成する必要がありますs()し、両方BCそれに依存します。

最後に、ここにSOに関する興味深い質問の優れた回答へのリンクがあります。これは、いつインターフェイスにアクセスするか、いつ抽象クラスにアクセスするかを決定するのに役立つ場合があります。

優れた抽象クラスは、その機能や状態を共有できるため、書き換えが必要なコードの量を削減します。


s()がパブリックインターフェースの一部であることは、物事をよりトリッキーにすることに同意します。一般に、同じクラスで他のパブリックメソッドを呼び出すパブリックメソッドを定義することは避けます。
Alexandros Gougousis 2017年

1

論理的にも技術的にもオブジェクト指向のポイントが欠けているように思えます。2つのシナリオについて説明します。基本クラスの型の一般的な動作のグループ化と多態性です。これらは両方とも、抽象クラスの正当なアプリケーションです。ただし、クラスを抽象化する必要があるかどうかは、技術的な可能性ではなく、分析モデルに依存します。

実世界に具体化されていないタイプを認識する必要がありますが、存在する特定のタイプの基礎を築きます。例:動物。動物のようなものはありません。それはいつも犬か猫か何かですが、本物の動物は存在しません。しかし動物はそれらすべてを組み立てます。

次に、レベルについて話します。継承に関しては、レベルは契約の一部ではありません。また、一般的なデータや一般的な動作を認識していません。これは、おそらく役に立たない技術的なアプローチです。ステレオタイプを認識してから、基本クラスを挿入する必要があります。そのようなステレオタイプがない場合は、複数のクラスによって実装されるいくつかのインターフェースを使用する方がよい場合があります。

抽象クラスの名前とそのメンバーは、意味をなす必要があります。技術的にも論理的にも、派生クラスから独立している必要があります。アニマルのように、抽象メソッドEat(多態になる)とブールの抽象プロパティIsPetとIsLifestockを持つことができます。これは、猫や犬、豚について知らなくても意味があります。依存関係(技術的または論理的)は、子孫からベースへの一方向にのみ進む必要があります。基本クラス自体も、降順クラスの知識を持っていてはなりません。


あなたが言及するステレオタイプは、私がより高いレベルの機能性について話すとき、または他の誰かが階層の上位のクラスによって記述されている一般化された概念について話すときとほぼ同じです(下位のクラスによって記述されるより専門的な概念に対して)階層)。
Alexandros Gougousis 2017年

「基本クラス自体も、降順クラスの知識を持っていてはなりません。」私はこれに完全に同意します。親クラス(抽象かどうかにかかわらず)には、子クラスの実装を調べて理解する必要がない自己完結型のビジネスロジックが含まれている必要があります。
Alexandros Gougousis 2017年

そのサブタイプを知っている基本クラスにはもう1つあります。新しいサブタイプが開発されるときはいつでも、基本クラスを変更する必要があります。これは、後方にあり、開閉の原則に違反しています。私は最近、既存のコードでこれに遭遇しました。基本クラスには、アプリケーションの構成に基づいてサブタイプのインスタンスを作成する静的メソッドがありました。その意図はファクトリーパターンでした。この方法でも、SRPに違反します。「当たり前」、「言語が自動的にそれを処理してくれる」と思っていたが、人々は想像以上にクリエイティブであることがわかった。
Martin Maat 2017年

0

最初のケース、あなたの質問から:

そのような場合、抽象クラスは設計図、機能の高レベルの説明、またはそれと呼ぶもののように機能します。

2番目のケース:

また、抽象クラスは通常、現在の派生クラスに含まれるあらゆる種類の一般的なコードを配置できる場所として使用されます。

次に、あなたの質問

私はこれらの2つのケースのどちらがルールであるべきか疑問に思っています。...たまたま派生クラスに共通しているからといって、抽象クラス自体には意味がないコードが存在する必要がありますか?

IMOには2つの異なるシナリオがあるため、適用する必要のあるデザインを決定するための単一のルールを描くことはできません。

最初のケースでは、他の回答ですでに言及されているテンプレートメソッドパターンのようなものがあります。

2番目のケースでは、abs()メソッドを受け取る抽象クラスAの例を示しました。IMO b()メソッドは、他のすべての派生クラスに対して意味がある場合にのみ移動する必要があります。つまり、2か所で使用されているために移動する場合、おそらくこれは適切な選択ではありません。明日、b()がまったく意味を持たない新しい具象Derivedクラスが存在する可能性があるためです。また、あなたが言ったように、Aクラスを個別に見て、b()もそのコンテキストで意味をなさない場合、おそらくこれもデザインの良い選択ではないというヒントです。


-1

私は少し前にまったく同じ質問をしました!

私がこの問題に出会ったときはいつでも、私が見つけていない隠された概念があることに気づきました。そして、おそらくこの概念は、何らかの値オブジェクトで表現されるべきです。あなたは非常に抽象的な例を持っているので、私はあなたのコードを使用して私が何を意味するのかを示すことができないと思います。しかし、ここに私自身の実例があります。外部リソースにリクエストを送信してレスポンスを解析する方法を表す2つのクラスがありました。リクエストの作成方法とレスポンスの解析方法には類似点がありました。つまり、私の階層は次のようになりました。

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

そのようなコードは、私が単に継承を誤用していることを示しています。それがリファクタリングできる方法です:

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

それ以来、私は継承を非常に慎重に扱います。

それ以来、私は継承について別の結論に達しました。内部構造に基づく継承には強く反対します。それはカプセル化を壊し、それは壊れやすく、結局のところ手続き型です。ドメインを適切分解すると、継承が頻繁に発生しなくなります。


@Downvoter、私に好意をして、この答えのどこが悪いのかコメントしてください。
Vadim Samokhin 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.