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クエリとデータベースアクセスを実行する責任も持たないようにする必要があるということです。これにより、たとえば、RailsのRubyが使用するActionRecordパターンが無効になります。
先日、このC#コード(この投稿で使用されている4番目のオブジェクト言語)に遭遇しました。
byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);
そして、Encoding
またはCharset
クラスが実際にencodes文字列であることは、私にはあまり意味がありません。それは単にエンコーディングが実際に何であるかを表すものでなければなりません。
したがって、私は次のように考える傾向があります。
File
クラスの責任ではありません。Xml
クラスの責任ではありません。User
クラスの責任ではありません。ただし、これらのアイデアを推定すると、なぜObject
にtoString
クラスができるのでしょうか。自分自身を文字列に変換するのは、車や犬の責任ではありません。
実用的な観点から、厳密なSOLIDフォームに従うという美しさのためにtoString
メソッドを取り除くことは、コードを無用にすることでコードをより保守しやすくすることは、許容できるオプション。
私はまた、これに対する正確な回答(真剣な回答よりものエッセイである)がないかもしれない、または意見ベースであるかもしれないことも理解しています。それでも、私のアプローチが実際に単一責任の原則が実際に何であるかに従っているかどうか知りたいと思います。
クラスの責任は何ですか?
言語間の相違を考えると、これはトリッキーなトピックになる可能性があります。したがって、私はOOの領域内でできる限り包括的になるように、次のコメントを作成しています。
まず第一に、いわゆる「単一責任原則」は、概念cohesionの反射(明示的に宣言された)です。当時の文献を読んで('70年頃)、人々はモジュールが何であるか、そしてニースのプロパティを保持する方法でそれらを構築する方法を定義するのに苦労しました(そして今もそうです)。それで、彼らは「ここに一連の構造と手順があります、私はそれらからモジュールを作ります」と言います、しかし、この任意のセットが一緒にパッケージされる理由に関する基準がなければ、組織はほとんど意味をなさなくなるかもしれません-少しの「まとまり」。したがって、基準に関する議論が浮上した。
したがって、ここで最初に注意する点は、これまでのところ、議論はメンテナンスと理解しやすさに対する組織と関連する影響(モジュールが「理にかなっている」かどうかはほとんど問題ではない)に関することです。
次に、他の誰か(マーティン氏)がやって来て、同じ考え方をクラスのユニットに適用して、何に属すべきか、またはすべきでないかを考えるときに使用する基準として、この基準を原則に推進しました。ここに。彼が述べたポイントは、 "クラスは変更する理由が1つだけあるべきです" でした。
まあ、私たちは経験から、「多くのこと」をしているように見える多くのオブジェクト(および多くのクラス)がそうするための非常に正当な理由があることを知っています。望ましくないケースは、メンテナンスなどが浸透しないほど機能が肥大化したクラスです。後者を理解するには、mrの場所を確認する必要があります。マーティンは彼が主題について詳しく述べたときに目指していた。
もちろん、ミスターを読んだ後。マーティンは、これらが問題のあるシナリオを回避するための基準と設計の基準であることを明確にすべきであると述べました。強いコンプライアンス、特に「責任」が明確に定義されていない場合(そして「これは原則に違反していますか?」などの質問は、広範な混乱の完璧な例です)。したがって、原則と呼ばれ、人々にそれを最後の結果に導こうと誤解させ、それが役に立たないことを残念に思います。マーティン氏自身は、分離するとより悪い結果が生じるため、おそらくそのようにしておく必要がある「複数のことを行う」設計について話し合った。また、モジュール性に関しては多くの既知の課題があり(これはその例です)、それについてのいくつかの簡単な質問に対してさえ、良い答えを得る段階にありません。
しかし、これらのアイデアを推定すると、なぜObjectにtoStringクラスがあるのでしょうか。自分自身を文字列に変換するのは、車や犬の責任ではありません。
さて、ここでtoString
について何か言います。モジュールからクラスへの思考の移行を行い、どのメソッドがクラスに属すべきかについて考えるときに、一般的に無視されている基本的なことがあります。そして、それは動的ディスパッチです(別名、遅延バインディング、「ポリモーフィズム」)。
「オーバーライドするメソッド」のない世界では、「obj.toString()」または「toString(obj)」のどちらを選択するかは、構文設定だけの問題です。ただし、プログラマーが既存/オーバーライドされたメソッドの個別の実装を持つサブクラスを追加することによってプログラムの動作を変更できる世界では、この選択はもはや好みではありません。プロシージャをメソッドにすることで、メソッドをオーバーライドの候補にすることもできます。同じことは「無料手順」には当てはまらない可能性があります(マルチメソッドをサポートする言語には、この二分法から抜け出す方法があります)。したがって、これは組織のみに関する議論ではなく、セマンティクスについての議論です。最後に、メソッドがバインドされているクラスも影響を与える決定になります(多くの場合、これまでのところ、さまざまな選択肢から明らかでないトレードオフが発生するため、物事がどこに属しているかを判断するのに役立つガイドラインしかありません) 。
最後に、私たちはひどい設計上の決定を運ぶ言語に直面しています。たとえば、あらゆることのためにクラスを作成するように強制します。したがって、オブジェクト(およびクラスランド、つまりクラス)が存在するための標準的な理由と主な基準は何でしたか。つまり、「データのように動作する動作」の一種である「オブジェクト」を持つことです。ただし、具体的な表現(存在する場合)を直接操作しないようにしてください(クライアントの観点からは、オブジェクトのインターフェイスがどうあるべきかについての主要なヒントです)。ぼやけて混乱します。
[注:ここではオブジェクトについて説明します。オブジェクトとは、結局のところ、クラスではなく、オブジェクト指向プログラミングのことです。]
オブジェクトの責任が何であるかは、主にドメインモデルに依存します。通常、同じドメインをモデル化するには多くの方法があり、システムの使用方法に基づいて、いずれかの方法を選択します。
ご存じのように、入門コースでよく教えられる「動詞/名詞」の方法論は、文の作り方に大きく依存するため、ばかげています。ほぼすべてを、名詞または動詞として、能動的または受動的な声で表現できます。ドメインモデルがそれに依存しているのは誤りであり、要件を定式化する方法の偶然の結果だけでなく、何かがオブジェクトまたはメソッドであるかどうかに関係なく、設計上の選択を意識する必要があります。
ただし、doesは、同じことを英語で表現する可能性がたくさんあるように、同じことを英語で表現する可能性がたくさんあることを示しています。ドメインモデル…そして、これらの可能性のいずれも、他のものよりも本質的に正確ではありません。状況によります。
これは、入門でも非常に人気のある例ですOOコース:銀行口座。通常、balance
フィールドとtransfer
メソッドを使用してオブジェクトとしてモデル化されます。つまり、口座残高はdataであり、転送はoperationです。これは合理的です銀行口座をモデル化する方法。
例外として、それはではありません銀行口座が実際の銀行用ソフトウェアでモデル化される方法です(実際、銀行が現実の世界で機能する方法ではありません)。代わりに、トランザクションスリップがあり、アカウントの残高は、アカウントのすべてのトランザクションスリップを加算(および減算)して計算されます。言い換えると、転送はdataで、残高は操作です。 ! (興味深いことに、これによりシステムは純粋に機能します。残高を変更する必要がないため、アカウントオブジェクトとトランザクションオブジェクトの両方を変更する必要がなく、不変です。)
toString
に関する具体的な質問については、同意します。私はShowable
type class のHaskellソリューションを非常に好みます。 (Scalaは、型クラスはOOに美しく適合することを教えてくれます。)同じことは同じです。平等は、オブジェクトのプロパティではなく、オブジェクトが使用されるコンテキストのプロパティである場合が多くあります。浮動小数点数について考えてみてください。イプシロンはどうあるべきですか?繰り返しになりますが、HaskellにはEq
タイプのクラスがあります。
単一責任の原則がゼロ責任の原則になりすぎて、最終的に Anemic Classes が機能せず(セッターとゲッターを除く)、 名詞の王国の災害につながる) 。
あなたはそれを完璧にすることは決してないでしょうが、クラスにとっては少なすぎるよりも多すぎることをする方が良いです。
IMOエンコードの例では、確実にエンコードする必要があります。代わりに何をすべきですか? 「utf」という名前を付けるだけでは責任はありません。これで、名前はEncoderになるはずです。しかし、Konradが言ったように、データ(エンコード)と動作(それを行う)は一緒に属します。
クラスのインスタンスはクロージャです。それでおしまい。そのように考えると、よく見たすべてのソフトウェアは正しく見えますが、よく考えられていないソフトウェアはすべて正しくありません。拡大させてください。
ファイルに書き込むものを書き込みたい場合は、(OSに対して)指定する必要があるすべてのことを考えてください。ファイル名、アクセス方法(読み取り、書き込み、追加)、書き込みたい文字列。
したがって、ファイル名(およびアクセス方法)を使用してFileオブジェクトを作成します。 Fileオブジェクトはファイル名で閉じられます(この場合、おそらくreadonly/const値として取り込まれます)。これで、Fileインスタンスは、クラスで定義された「write」メソッドを呼び出す準備ができました。これは文字列引数を取るだけですが、書き込みメソッドの本体、実装では、ファイル名(またはファイル名から作成されたファイルハンドル)にもアクセスできます。
したがって、Fileクラスのインスタンスは、一部のデータをある種類の複合BLOBにボックス化し、後でそのインスタンスをより簡単に使用できるようにします。 Fileオブジェクトがある場合は、ファイル名が何であるかを気にする必要はありません。書き込み呼び出しの引数としてどの文字列を入力するかだけが気になります。繰り返しますが、Fileオブジェクトは、書き込む文字列が実際にどこに移動するかについて知る必要のないすべてのものをカプセル化します。
あなたが解決したいほとんどの問題はこのように階層化されます-最初に作成されたもの、ある種のプロセスループの各反復の開始時に作成されたもの、次にif elseの2つの半分で宣言された異なるもの、そして次に作成されたものある種のサブループの始まり、そのループ内のアルゴリズムの一部として宣言されたものなど。スタックに関数呼び出しを追加するにつれて、きめ細かいコードが実行されますが、毎回スタックの下位層のデータを、アクセスしやすいようにするある種のニース抽象化にボックス化しました。関数型プログラミングアプローチの一種のクロージャ管理フレームワークとして「OO」を使用していることを確認してください。
いくつかの問題のショートカットソリューションには、オブジェクトの変更可能な状態が含まれますが、これはそれほど機能的ではありません。外部からクロージャーを操作しているのです。少なくとも上記のスコープに対して、クラス「グローバル」でいくつかのものを作成しています。セッターを書くたびに-それらを避けるようにしてください。私はこれがあなたがあなたのクラス、またはそれが動作する他のクラスの責任を誤って得た場所だと主張します。しかし、あまり心配する必要はありません。認識しやすい負荷をかけずに、特定の種類の問題をすばやく解決し、実際に何かを実行するために、少し変更可能な状態でかき混ぜることは実際的です。
要約すると、あなたの質問に答えるために-クラスの本当の責任は何ですか?より詳細な種類の操作を実現するために、基盤となるデータに基づいてプラットフォームの種類を提示することです。データの残りの半分よりも変更頻度が少ない操作に含まれるデータの半分をカプセル化することです(カレーを考える...)。例えば。書き込むファイル名と文字列。
多くの場合、これらのカプセル化は、表面的には実世界のオブジェクト/問題ドメインのオブジェクトのように見えますが、だまされてはいけません。 Carクラスを作成するとき、それは実際には車ではなく、車で実行したいと考えるかもしれない特定の種類のことを達成するためのプラットフォームを形成するデータのコレクションです。文字列表現(toString)を形成することは、それらの1つです-内部データをすべて吐き出します。問題の領域がすべて自動車に関するものである場合でも、Carクラスは適切なクラスではない場合があることを覚えておいてください。名詞の王国とは対照的に、クラスはその基になる動詞、つまり動詞です。
私はまた、これに対する正確な答えがないかもしれない(これは深刻な答えよりもエッセイになるでしょう)か、それは意見に基づいているかもしれないことも理解しています。それでも、私のアプローチが実際に単一責任の原則が実際に何であるかに従っているかどうか知りたいと思います。
それは可能ですが、必ずしも良いコードになるとは限りません。盲目的に守られたあらゆる種類のルールが悪いコードにつながるという単純な事実を超えて、あなたは無効な前提から始めています:(プログラミング)オブジェクトは(物理的な)オブジェクトではありません。
オブジェクトは、データと操作のまとまりのまとまりのセットです(場合によっては2つのうちの1つのみ)。これらは実際のオブジェクトをモデル化することがよくありますが、何かのコンピューターモデルとそのもの自体の違い必要違いです。
物を表す「名詞」とそれを消費するその他の物との間に一線を画することにより、オブジェクト指向プログラミングの主要なメリットに逆らうことになります(関数と状態を組み合わせて、関数が状態の不変条件を保護できるようにする)。 。さらに悪いことに、あなたは物理的な世界をコードで表現しようとしていますが、これは歴史が示しているように、うまく機能しません。
先日、このC#コード(この投稿で使用されている4番目のオブジェクト言語)に遭遇しました。
byte[] bytes = Encoding.Default.GetBytes(myString); myString = Encoding.UTF8.GetString(bytes);
そして、
Encoding
またはCharset
クラスが実際にencodesであることは、私にはあまり意味がないと言わなければなりません文字列。それは単にエンコーディングが実際に何であるかを表すものでなければなりません。
理論上はそうですが、C#は(Javaのより厳密なアプローチとは対照的に)単純化のために正当な妥協案を作っていると思います。
Encoding
が特定のエンコーディング(たとえばUTF-8)のみを表す(または識別する)場合、Encoder
も実装できるように、明らかにGetBytes
クラスも必要になります。その上-しかし、あなたはEncoder
sとEncoding
sの間の関係を管理する必要があるので、私たちは良いol
EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)
あなたがリンクしたその記事でとても見事に大騒ぎしました(ところで、大いに読んでください)。
私があなたをよく理解しているなら、あなたはどこに線があるのか尋ねています。
まあ、スペクトルの反対側には手続き型のアプローチがあり、OOP言語で静的クラスを使用して実装するのは難しくありません:
HelpfulStuff.GetUTF8Bytes(myString)
それについての大きな問題は、HelpfulStuff
の作者が去り、私がモニターの前に座っているとき、GetUTF8Bytes
が実装されている場所を知る方法がないということです。
HelpfulStuff
、Utils
、StringHelper
で検索しますか?
メソッドが実際に3つすべてで独立して実装されている場合、主が私を助けてくれます...これはよく起こります。私の前の人もその関数を探す場所がわからず、何もないと信じていただけです。それでも彼らは別のものを取り出し、今では私たちはそれらを豊富に持っています。
私にとって、適切な設計とは、抽象的な基準を満たすことではなく、コードの前に座った後も実行しやすいことです。今どうやって
EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)
この面でのレート?同様に貧しい。同僚に「文字列をバイトシーケンスにエンコードするにはどうすればよいですか」と叫んだときに聞きたくない回答です。 :)
だから私は適度なアプローチで行き、単一の責任について寛大になります。むしろ、Encoding
クラスを使用して、エンコーディングのタイプを識別し、実際のエンコーディングを実行します。つまり、動作から分離されていないデータです。
ファサードパターンとして通過するので、ギアにグリースを塗るのに役立ちます。この正当なパターンはSOLIDに違反していますか?関連質問: ファサードパターンはSRPに違反していますか?
これは、エンコードされたバイトを取得する方法が内部的に実装されている場合があることに注意してください(.NETライブラリ)。クラスは一般に公開されておらず、この「ファサード」APIのみが外部に公開されています。それが本当かどうかを確認することはそれほど難しくありません、私は今それを行うのがちょっと面倒です:)それはおそらくそうではありませんが、それは私のポイントを無効にしません。
より親密で標準的な実装を内部で維持しながら、親しみやすさのために公開されている実装を簡略化することもできます。
Javaでは、ファイルを表すために使用される抽象化はストリームですが、一部のストリームは単方向(読み取り専用または書き込み専用)です。他のストリームは双方向です。Ruby FileクラスOSファイルを表す抽象化です。問題はその単一の責任になりますか?Javaでは、FileWriterの責任はOSファイルに接続されるバイトの単方向ストリームを提供することです。Rubyでは、Fileの責任はへの双方向アクセスを提供することですシステムファイル。どちらもSRPを実行しますが、責任は異なります。
自分自身を文字列に変換するのは、車や犬の責任ではありません。
もちろん?私はCarクラスが完全にCar抽象を表すことを期待しています。文字列が必要な場所で車の抽象化を使用することが理にかなっている場合は、Carクラスが文字列への変換をサポートすることを十分に期待しています。
より大きな質問に直接答えるには、クラスの責任は抽象化として機能することです。その仕事は複雑かもしれませんが、それは一種の要点です。抽象化は、使いやすく、複雑でないインターフェースの背後に複雑なものを隠す必要があります。 SRPは設計ガイドラインであるため、「この抽象化は何を表すのか」という質問に焦点を当てます。概念を完全に抽象化するために複数の異なる動作が必要な場合は、そのようにします。
クラスは複数の責任を持つことができます。少なくとも、古典的なデータ中心のOOPでは。
単一責任の原則を最大限に適用すると、基本的にすべての非ヘルパーメソッドが独自のクラスに属する responsibility-centric design が得られます。
FileやFileWriterの場合のように、基本クラスから複雑さの一部を抽出しても問題はないと思います。利点は、特定の操作を担当するコードを明確に分離できること、および基本クラス全体(ファイルなど)をオーバーライドする代わりに、そのコードの一部のみをオーバーライド(サブクラス)できることです。それにもかかわらず、それを体系的に適用することは、私にはやり過ぎだと思われます。処理するクラスが増え、それぞれのクラスを呼び出すために必要なすべての操作を実行できます。ある程度のコストがかかるのは柔軟性です。
これらの抽出されたクラスは、それらに含まれる操作に基づいて非常にわかりやすい名前を持つ必要があります。 <X>Writer
、<X>Reader
、<X>Builder
。 <X>Manager
、<X>Handler
のような名前は、操作をまったく記述していません。複数の操作が含まれているために、そのように呼び出された場合、何を達成したかは明確ではありません。機能をデータから分離しましたが、抽出されたクラスでも単一責任の原則を破り、特定のメソッドを探している場合、それを探す場所がわからない場合があります(<X>
または<X>Manager
の場合)。
基本クラス(実際のデータを含むクラス)がないため、UserDataHandlerのケースは異なります。このクラスのデータは外部リソース(データベース)に格納され、APIを使用してそれらにアクセスし、操作します。 Userクラスはランタイムユーザーを表します。これは永続ユーザーとは本当に異なるものであり、特にランタイムユーザー関連のロジックがたくさんある場合は、責任(懸念)の分離がここで役立つことがあります。
基本的にそのクラスには機能のみがあり、実際のデータはないため、おそらくUserDataHandlerにそのような名前を付けます。ただし、その事実から抽象化し、データベース内のデータをクラスに属すると見なすこともできます。この抽象化を使用すると、クラスの名前ではなく、それが何であるかに基づいてクラスに名前を付けることができます。 UserRepositoryのような名前を付けることができます。これはかなり洗練されており、 repository pattern の使用を提案します。