クラスの本当の責任は何ですか?


42

私は、OOPの名詞に基づいた動詞を使用することが合法かどうか疑問に思っています。
私はこの素晴らしい記事に出くわしましたが、それが主張する点にはまだ賛成しません。

この問題をもう少し説明するために、この記事では、たとえばFileWriterクラスが存在するべきではないと述べていますが、書き込みはアクションなので、クラスのメソッドである必要がありFileます。RubyプログラマーはFileWriterクラスの使用に反対する可能性が高い(RubyはメソッドFile.openにファイルへのアクセスを使用する)のに対し、Javaプログラマーはそうではないため、多くの場合言語に依存していることに気付くでしょう。

私個人の(そしてもちろん、非常に謙虚な)視点は、そうすることは単一責任の原則を破ることだということです。PHPでプログラミングしたとき(PHPは明らかにOOPに最適な言語だからです)、この種のフレームワークをよく使用します。

<?php

// This is just an example that I just made on the fly, may contain errors

class User extends Record {

    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

}

class UserDataHandler extends DataHandler /* knows the pdo object */ {

    public function find($id) {
         $query = $this->db->prepare('SELECT ' . $this->getFields . ' FROM users WHERE id = :id');
         $query->bindParam(':id', $id, PDO::PARAM_INT);
         $query->setFetchMode( PDO::FETCH_CLASS, 'user');
         $query->execute();
         return $query->fetch( PDO::FETCH_CLASS );
    }


}

?>

私の理解では、接尾辞DataHandler は関連するものを追加しません。しかし、重要な点は、単一の責任原則により、データを含むモデルとして使用されるオブジェクト(レコードと呼ばれることもあります)は、SQLクエリとデータベースアクセスを実行する責任も負わないことです。これにより、Ruby on Railsでインスタンスに使用されるActionRecordパターンが何らかの理由で無効になります。

先日、このC#コード(この投稿で使用した4番目のオブジェクト言語)に出会いました。

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

また、Encodingor Charsetクラスが実際に文字列をエンコードすることはあまり意味がないと言わざるを得ません。それは単にエンコーディングが実際に何であるかの表現でなければなりません。

したがって、私はそう思う傾向があります:

  • Fileファイルを開いたり、読み込んだり、保存したりすることはクラスの責任ではありません。
  • Xmlシリアル化することはクラスの責任ではありません。
  • Userデータベースを照会することはクラスの責任ではありません。

私たちはこれらのアイデアを外挿する場合は、なぜでしょうObject持っているtoStringクラスは?文字列に変換するのは車や犬の責任ではありません。

実用的な観点から、toString厳密なSOLIDフォームに従うことの美しさのためのメソッドを取り除くことは理解できません。

また、これに対する正確な答え(真剣な答えよりもエッセイ)がない場合や、意見に基づいている場合があることも理解しています。それにもかかわらず、私のアプローチが単一責任原則が実際に何であるかを実際に追っているかどうかを知りたいです。

クラスの責任は何ですか?


オブジェクトtoStringは主に利便性が高く、一般的にtoStringはデバッグ中に内部状態をすばやく表示するために使用されます
ラチェットフリーク

3
@ratchetfreak私は同意しますが、それは私が説明した方法で単一の責任が非常に頻繁に破られることを示すための例(ほとんどジョーク)でした。
ピエールアラード

8
単一責任の原則には熱心な支持者がいますが、私の意見では、それはせいぜいガイドラインであり、圧倒的多数の設計選択に対するガイドラインです。SRPの問題は、何百という奇妙な名前のクラスになり、必要なものを見つける(または必要なものが既に存在するかどうかを調べる)ことや、全体像を把握することが本当に難しくなることです。たとえかなりの責任を負っていても、クラスから何を期待すべきかをすぐに教えてくれる、よく知られたクラスが少ないほうがずっと好きです。将来、状況が変わる場合、クラスを分割します。
ダンク

回答:


24

言語間のいくつかの相違を考えると、これは扱いにくいトピックになります。したがって、私はオブジェクト指向の領域内でできる限り包括的になるように、次の解説を作成しています。

まず第一に、いわゆる「単一責任原則」は、概念の結束性の反射-明示的に宣言された-です。当時(70年頃)の文献を読んで、人々はモジュールとは何か、そして素晴らしいプロパティを保持する方法でそれらを構築する方法を定義するのに苦労していました(そして今でも)。だから、彼らは「ここにたくさんの構造と手順があります、私はそれらからモジュールを作ります」と言うでしょうが、このset意的なもののセットが一緒にパッケージ化される理由についての基準なしで、組織はほとんど意味をなさないかもしれません-少しの「凝集」。したがって、基準に関する議論が浮上しました。

したがって、ここで最初に注意することは、これまでのところ、議論は組織とメンテナンスと理解可能性に関連する影響に関するものです(モジュールが「理にかなっている」場合、コンピューターにはほとんど関係ありません)。

次に、他の誰か(マーティン氏)が入り、同じ思考をクラスのユニットに適用して、それに属するべきかどうかを考える際に使用する基準として適用し、この基準を原則に推進します。ここに。彼が言ったのは、「クラスには変更する理由が1つだけあるべきだ」ということでした。

さて、経験から、「多くのこと」を行うように見える多くのオブジェクト(および多くのクラス)がそうする非常に良い理由があることを知っています。望ましくないケースは、メンテナンスなどが不可能なほど機能が肥大化したクラスです。そして、後者を理解するには、mrがどこにあるかを確認する必要があります。マーティンは、このテーマについて詳しく説明したときに目指していました。

もちろん、ミスター氏を読んだ後。マーティンは、それがする方向とデザインのための基準であるこれらをクリアする必要があり、書いてないよう「責任」が病気に定義されている特別な時に、コンプライアンスのいずれかの種類を追求する任意の方法で、一人で強力なコンプライアンスを聞かせていない、問題のあるシナリオを(そしてこれを行います「のような質問原則に違反していますか?」は、広範囲にわたる混乱の完璧な例です)。したがって、残念ながら、それは原則と呼ばれています、人々を誤解させて、それを最後の結果に導こうとします。マーティン氏自身は、分離するとより悪い結果が生じるため、おそらくそのように保つべき「複数のことを行う」設計について議論しました。また、モジュール性に関して多くの既知の課題があります(そして、この主題はその例です)。

しかし、これらのアイデアを外挿すると、ObjectにtoStringクラスがあるのはなぜですか?文字列に変換するのは車や犬の責任ではありません。

さて、ここで何か言いたいことtoStringがあります。モジュールからクラスへの思考の移行を行い、どのメソッドがクラスに属するべきかを考えるとき、一般的に無視されている基本的なことがあります。そして、問題は動的ディスパッチ(別名、遅延バインディング、「ポリモーフィズム」)です。

「オーバーライドメソッド」のない世界では、「obj.toString()」または「toString(obj)」のどちらを選択するかは、構文の好みだけの問題です。ただし、プログラマーが既存のメソッドまたはオーバーライドされたメソッドの個別の実装を持つサブクラスを追加することでプログラムの動作を変更できる世界では、この選択はもはや好みではありません。また、「フリープロシージャ」についても同じことが当てはまらない場合があります(マルチメソッドをサポートする言語には、この二分法から抜け出す方法があります)。したがって、これは組織だけの議論ではなく、セマンティクスについての議論でもあります。最後に、メソッドがどのクラスにバインドされているかは、決定に影響を及ぼします(多くの場合、これまでのところ、物事がどこに属しているかを判断するのに役立つガイドライン以上のものがありますが、

最後に、私たちはひどいデザインの決定を運ぶ言語に直面しています。例えば、小さなものごとにクラスを作成することを強制します。したがって、かつてはオブジェクト(およびクラスランド、したがってクラス)を持つための標準的な理由と主な基準であったもの、つまり、「データのように振る舞う振る舞い」のような「オブジェクト」を持つことしかし、それらの具体的な表現(もしあれば)を直接の操作から保護します(そして、それは、クライアントの観点から、オブジェクトのインターフェースとなるものの主なヒントです)、ぼやけて混乱します。


31

[注:ここでオブジェクトについて説明します。オブジェクトは、オブジェクト指向プログラミングとは、結局のところ、クラスではありません。]

オブジェクトの責任は、主にドメインモデルに依存します。通常、同じドメインをモデル化するには多くの方法があり、システムの使用方法に基づいていずれかの方法を選択します。

私たちが知っているように、入門コースでよく教えられる「動詞/名詞」の方法論はばかげています。なぜなら、それは文をどのように定式化するかに大きく依存するからです。ほとんどすべてを、能動的な声または受動的な声で、名詞または動詞として表現できます。ドメインモデルが間違っていることに依存しているので、要件を定式化することの偶然の結果ではなく、何かがオブジェクトまたはメソッドであるかどうかを意識して設計する必要があります。

しかし、それはありませんあなたが英語で同じことを表現する多くの可能性を持っているだけのように、あなたがあなたのドメインモデルでも同じことを表現する多くの可能性を持っている...そして、それらの可能性のいずれも他よりも本質的に、より正確ではない、というショーを。コンテキストに依存します。

これは、入門オブジェクト指向コースでも非常に人気のある例です:銀行口座。これは通常、balanceフィールドとtransferメソッドを持つオブジェクトとしてモデル化されます。言い換えれば、口座残高はdataであり、振替はオペレーションです。これは銀行口座をモデル化する合理的な方法です。

例外として、それ銀行口座が実際の銀行ソフトウェアでどのようにモデル化されるかではありません(実際、銀行は実際の世界でどのように機能するかではありません)。代わりに、トランザクションスリップがあり、アカウントのすべてのトランザクションスリップを加算(および減算)することにより、アカウントの残高が計算されます。つまり、転送はデータであり、残高は操作です!(興味深いことに、バランスを変更する必要はなく、アカウントオブジェクトとトランザクションオブジェクトの両方を変更する必要がないため、システムは純粋に機能します。これらは不変です。)

についてのあなたの特定の質問に関してtoString、私は同意します。Showable 型クラスの Haskellソリューションが非常に好きです。(Scalaは、型クラスがオブジェクト指向に美しく適合することを教えてくれます。)平等で同じです。平等は非常に頻繁にあるないオブジェクトのが、オブジェクトが使用されるコンテキストのプロパティ。浮動小数点数について考えてみましょう:イプシロンはどうあるべきですか?繰り返しになりますが、HaskellにはEq型クラスがあります。


Haskellの比較が好きです。答えがもう少し…新鮮になります。ではActionRecord、モデルとデータベースリクエスタの両方を許可する設計についてどう思いますか?文脈に関係なく、それは単一責任の原則を破っているようです。
ピエールアラード

5
「...それは文をどのように定式化するかに大きく依存するからです。」わーい!ああ、私はあなたの銀行の例が大好きです。80年代にさかのぼって関数型プログラミングを既に教えられていたことを決して知らなかった:)(データ中心の設計と天びんなどが計算可能であり、保存されるべきではなく、誰かの住所を変更すべきではない、時間に依存しないストレージ新しい事実)...
マルジャンヴェネマ

1
動詞/名詞の方法論が「ばかげている」とは言いません。単純な設計は失敗し、正しくなるのは非常に難しいと思います。反例として、RESTful APIは動詞/名詞の方法論ではないのですか?難易度を上げるために、動詞は非常に制限されていますか?
user949300

@ user949300:動詞のセットが固定されているという事実は、私が言及した問題を正確に回避します。つまり、複数の異なる方法で文を定式化できるため、名詞と動詞はドメイン分析ではなく、執筆スタイルに依存します。
ヨルグWミットタグ

8

多くの場合、単一責任原則はゼロ責任原則になり、何もせずに(セッターとゲッターを除く)貧血のクラスになり、名詞の王国の災害につながります。

完璧になることは決してありませんが、少なすぎるよりも少なすぎることをする方が良いでしょう。

エンコード、IMOの例では、間違いなくエンコードできるはずです。代わりに何をすべきでしょうか?「utf」という名前を持つだけで責任はゼロです。これで、名前はEncoderになります。しかし、コンラッドが言ったように、データ(エンコード)と動作(それを行う)は一緒に属します


7

クラスのインスタンスはクロージャーです。それでおしまい。そのように考えると、よく設計されたすべてのソフトウェアが正しく見え、よく考えられていないソフトウェアはすべて正しく見えません。拡大させてください。

ファイルに書き込むために何かを書きたい場合は、指定する必要があるすべてのもの(OSに対して)を考えてください:ファイル名、アクセス方法(読み取り、書き込み、追加)、書き込みたい文字列。

そのため、ファイル名(およびアクセス方法)を使用してFileオブジェクトを作成します。Fileオブジェクトはファイル名で閉じられます(この場合、おそらくreadonly / const値として取り込まれます)。Fileインスタンスは、クラスで定義された「write」メソッドを呼び出す準備ができました。これは単に文字列引数を取りますが、writeメソッドの本体である実装には、ファイル名(またはそこから作成されたファイルハンドル)へのアクセスもあります。

したがって、Fileクラスのインスタンスは、ある種の複合BLOBにデータをボックス化するため、後でそのインスタンスを簡単に使用できます。Fileオブジェクトがある場合、ファイル名が何であるかを心配する必要はありません。書き込み呼び出しに引数として入力する文字列だけを心配します。繰り返しますが、Fileオブジェクトは、書き込みたい文字列が実際にどこに向かっているのかを知る必要のないすべてをカプセル化します。

解決したいほとんどの問題は、このように階層化されます-前もって作成されたもの、ある種のプロセスループの各反復の開始時に作成されたもの、そしてif elseの2つの半分で宣言されたもの、ある種のサブループの開始、そのループ内のアルゴリズムの一部として宣言されたものなど。より多くの関数呼び出しをスタックに追加すると、よりきめの細かいコードを実行しますが、スタック内の下位層のデータを、簡単にアクセスできるようにする優れた抽象化にまとめました。あなたがやっていることは、機能プログラミングのアプローチのための一種のクロージャー管理フレームワークとして「OO」を使用していることをご覧ください。

いくつかの問題に対するショートカットソリューションには、オブジェクトの可変状態が含まれますが、これはそれほど機能的ではありません。外部からクロージャーを操作しています。少なくとも上記の範囲で、クラスの一部を「グローバル」にしています。セッターを作成するたびに、それらを避けるようにしてください。私は、これがあなたのクラス、またはそれが動作する他のクラスの責任を誤った場所だと主張します。ただし、あまり心配しないでください。認知負荷をかけずに特定の種類の問題を迅速に解決し、実際に何かを実行するために、少し変更可能な状態でかき回すのが実際的です。

要約すると、あなたの質問に答えるために-クラスの本当の責任は何ですか?より詳細な種類の操作を実現するために、基礎となるデータに基づいて一種のプラットフォームを提示することです。それは、データの他の半分よりも頻繁に変更されない操作に関与するデータの半分をカプセル化することです(カリー化...)。たとえば、ファイル名と書き込む文字列。

多くの場合、これらのカプセル化は、表面上は実世界のオブジェクト/問題ドメイン内のオブジェクトのように見えますが、だまされないでください。Carクラスを作成するとき、それは実際には車ではなく、車でやりたいと考えている特定の種類のことを達成するためのプラットフォームを形成するデータのコレクションです。文字列表現(toString)を形成することは、それらのことの1つです。すべての内部データを吐き出します。問題の領域がすべて自動車に関するものであっても、Carクラスは適切なクラスではない場合があることに注意してください。名詞の王国とは対照的に、それは操作であり、クラスが基にするべき動詞です。


4

また、これに対する正確な答え(真剣な答えよりもエッセイ)がない場合や、意見に基づいている場合があることも理解しています。それにもかかわらず、私のアプローチが単一責任原則が実際に何であるかを実際に追っているかどうかを知りたいです。

それはしますが、必ずしも良いコードになるとは限りません。どんな種類の盲目的なルールも悪いコードにつながるという単純な事実を超えて、あなたは無効な前提から始めています:(プログラミング)オブジェクトは(物理)オブジェクトではありません。

オブジェクトは、データと操作のまとまりのあるバンドルのセットです(2つのうちの1つだけである場合もあります)。これらはしばしば実世界のオブジェクトをモデル化しますが、何かのコンピューターモデルとその物自体の違いは違いを必要とします。

物を表す「名詞」とそれを消費する他の物との間に強い境界線を引くことにより、オブジェクト指向プログラミングの重要な利点に反します(関数が状態の不変式を保護できるように関数と状態を一緒に持つ) 。さらに悪いことに、あなたはコードで物理世界を表現しようとしていますが、これは歴史が示しているように、うまく機能しません。


4

先日、このC#コード(この投稿で使用した4番目のオブジェクト言語)に出会いました。

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

また、Encodingor Charsetクラスが実際に文字列をエンコードすることはあまり意味がないと言わざるを得ません 。それは単にエンコーディングが実際に何であるかの表現でなければなりません。

理論上はそうですが、C#は(Javaのより厳密なアプローチとは対照的に)単純化のために正当化された妥協をするものと思います。

場合Encodingにのみ、特定の符号化表現(または識別) -発言権を、UTF-8 -あなたは明らかに必要があると思いEncoderますが、実装することができるように、あまりにも、クラスのGetBytesそれに-しかし、あなたはとの関係を管理する必要があるEncoderのと、Encoding私たちは、Sを私たちの良いオールで終わる

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

あなたがリンクした記事で見事に荒らされました(ちなみに読んでください)。

私があなたをよく理解していれば、あなたはその行がどこにあるのかを尋ねているのです。

スペクトルの反対側には手続き型アプローチがあり、静的クラスを使用してOOP言語に実装するのは難しくありません。

HelpfulStuff.GetUTF8Bytes(myString)

それに関する大きな問題は、HelpfulStuffリーフの作成者と私がモニターの前に座っているとき、どこGetUTF8Bytesに実装されているかを知る方法がないことです。

私はそれを探してくださいHelpfulStuffUtilsStringHelper

メソッドがそれらの3つすべてで実際に独立して実装されている場合、主は私を助けます...それは多くのことが起こります、それは私の前の男がその機能をどこで探すべきかわからず、まだ彼らは別のものを取り出して、今私たちはそれらを豊富に持っています。

私にとって適切な設計とは、抽象的な基準を満たすことではなく、あなたのコードの前に座った後でも簡単に実行できることです。さてどう

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

この面で評価しますか?同様に悪い。同僚に「文字列をバイトシーケンスにエンコードするにはどうすればよいですか?」と叫ぶときに聞きたい答えではありません。:)

だから私は中程度のアプローチで行き、単一の責任について寛大になります。むしろEncoding、エンコードのタイプを識別し、実際のエンコードを実行するクラスの両方が必要です。つまり、動作と分離されていないデータです。

ファサードのパターンとして伝わり、ギアにグリースを塗るのに役立ちます。この正当なパターンはSOLIDに違反していますか?関連する質問:FacadeパターンはSRPに違反していますか?

これは、エンコードされたバイトを取得する方法が内部的に(.NETライブラリで)実装される場合があることに注意してください。クラスは一般に公開されておらず、この「ファサード」APIのみが外部に公開されています。それが本当かどうかを確認するのはそれほど難しくありません。今はやるのが少し面倒です:)おそらくそうではありませんが、それは私のポイントを無効にしません。

内部でより厳格で標準的な実装を維持しながら、親しみやすさのために公開されている実装を単純化することを選択できます。


1

Javaでは、ファイルを表すために使用される抽象化はストリームであり、一部のストリームは単方向(読み取り専用または書き込み専用)であり、他のストリームは双方向です。Rubyの場合、FileクラスはOSファイルを表す抽象概念です。それで、質問はその単一の責任とは何になりますか?Javaでは、FileWriterの責任は、OSファイルに接続されたバイトの単方向ストリームを提供することです。Rubyでは、ファイルの責任はシステムファイルへの双方向アクセスを提供することです。どちらもSRPを満たし、それぞれ異なる責任を負っています。

文字列に変換するのは車や犬の責任ではありません。

もちろん?CarクラスはCarの抽象化を完全に表現することを期待しています。文字列が必要な場所で車の抽象化を使用するのが理にかなっている場合、Carクラスが文字列への変換をサポートすることを完全に期待しています。

大きな質問に直接答えるには、クラスの責任は抽象化として機能することです。その仕事は複雑かもしれませんが、それは一種のポイントです。抽象化は、使いやすく、それほど複雑ではないインターフェースの背後に複雑なものを隠すべきです。SRPは設計ガイドラインであるため、「この抽象化は何を表していますか?」という質問に焦点を合わせます。概念を完全に抽象化するために複数の異なる動作が必要な場合は、そうしてください。


0

クラスには複数の責任があります。少なくとも従来のデータ中心のOOPでは。

単一の責任原則を最大限に適用する場合、基本的にすべての非ヘルパーメソッドが独自のクラスに属する責任中心の設計になります。

FileおよびFileWriterの場合のように、基本クラスから複雑さを抽出しても何も問題はないと思います。利点は、特定の操作を担当するコードを明確に分離できることと、ベースクラス全体(Fileなど)をオーバーライドする代わりに、そのコードの一部のみをオーバーライド(サブクラス)できることです。それにもかかわらず、体系的にそれを適用することは私にとってやり過ぎのようです。処理するクラスがはるかに多くなり、操作ごとにそれぞれのクラスを呼び出す必要があります。ある程度のコストがかかるのは柔軟性です。

これらの抽出されたクラスは、例えば、それらに含まれる操作に基づいて非常に説明的な名前を持っている必要があり<X>Writer<X>Reader<X>Builder。以下のような名前は<X>Manager<X>Handlerそれらが同様に呼ばれている場合、彼らは複数の操作が含まれているため、あなたも達成しているものをクリアしていないことを、すべての操作を説明していません。機能をデータから分離しましたが、抽出されたクラスでも単一の責任原則を破り、特定のメソッドを探している場合、それを探す場所がわからない可能性があります(<X>またはの場合<X>Manager)。

ここで、基本クラス(実際のデータを含むクラス)がないため、UserDataHandlerの場合は異なります。このクラスのデータは外部リソース(データベース)に保存され、APIを使用してそれらにアクセスして操作します。Userクラスは実行時ユーザーを表します。これは永続的なユーザーとはまったく異なるものであり、特に実行時ユーザーに関連するロジックをたくさん持っている場合は、ここで責任(懸念)の分離が役立ちます。

基本的にそのクラスには機能のみがあり、実際のデータはないため、おそらくUserDataHandlerにそのような名前を付けました。ただし、その事実から抽象化し、データベース内のデータをクラスに属すると見なすこともできます。この抽象化を使用すると、クラスの名前ではなく、クラスの動作に基づいて名前を付けることができます。あなたはそれをかなり滑らかなであり、またの使用を示唆UserRepository、などの名前を付けることができリポジトリパターンを

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.