ソリッドメソッドと静的メソッド


11

私がよく遭遇する問題は次のとおりです。Productクラスを持つWebショッププロジェクトがあるとします。ユーザーが製品にレビューを投稿できる機能を追加したい。そのため、製品を参照するレビュークラスがあります。次に、製品に対するすべてのレビューをリストする方法が必要です。2つの可能性があります。

(A)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

コードを見て、(A)を選択します:静的ではなく、パラメーターを必要としません。ただし、(A)は単一責任原則(SRP)およびオープンクローズド原則(OCP)に違反しているのに対し、(B)はそうではないと感じています。

  • (SRP)製品のレビューの収集方法を変更する場合、製品クラスを変更する必要があります。ただし、Productクラスを変更する理由は1つだけです。そして、それは確かにレビューではありません。Productの製品と関係のあるすべての機能をパックすると、すぐに散らかってしまいます。

  • (OCP)Productクラスを変更して、この機能で拡張する必要があります。これは原則の「変更のために閉鎖」部分に違反すると思います。レビューを実施するという顧客の要求を得る前に、私は製品が完成したと考え、それを「クローズ」しました。

より重要なことは、SOLID原則に従うか、よりシンプルなインターフェイスを使用するかです。

それとも私はここで何か間違ったことをしていますか?

結果

素晴らしい答えをありがとう 公式の回答として選ぶのは難しいです。

答えから主な議論を要約しましょう:

  • pro(A):OCPは法律ではなく、コードの可読性も重要です。
  • pro(A):エンティティの関係はナビゲート可能でなければなりません。両方のクラスがこの関係について知っている場合があります。
  • pro(A)+(B):両方を行い、(A)から(B)に委任するため、製品が再び変更される可能性は低くなります。
  • pro(C):finderメソッドを静的でない第3クラス(サービス)に配置します。
  • コントラ(B):テストのモックを妨げます。

私の職場の大学が貢献したいくつかの追加事項:

  • pro(B):ORMフレームワークは(B)のコードを自動的に生成できます。
  • pro(A):ORMフレームワークの技術的な理由により、ファインダーの行き先とは無関係に、「閉じた」エンティティを変更する必要がある場合があります。とにかく、私は常に固執することができるとは限りません。
  • コントラ(C):大騒ぎする;-)

結論

現在のプロジェクトでは、委任で(A)+(B)の両方を使用しています。ただし、サービス指向の環境では、(C)を使用します。


2
静的変数でない限り、すべてがクールです。静的メソッドはテストが簡単で、トレースも簡単です。
コーダー

3
ProductsReviewsクラスを用意していないのはなぜですか?その後、製品とレビューは同じままです。または多分私は誤解しています。
エルグリンゴグランデ

2
@Coder「静的メソッドは簡単にテストできます」、本当に?m笑することはできません。詳細については、googletesting.blogspot.com / 2008/12 / をご覧ください。
StuperUser

1
@StuperUser:モックすることは何もありません。Assert(5 = Math.Abs(-5));
コーダー

2
テストAbs()は問題ではなく、それに依存するものをテストすることは問題です。依存するCode-Under-Test(CUT)を分離してモックを使用するための継ぎ目がありません。つまり、アトミックユニットとしてテストすることはできず、すべてのテストはユニットロジックをテストする統合テストになります。テストの失敗は、CUTまたはAbs()(またはその依存コード)にある可能性があり、単体テストの診断上の利点がなくなります。
StuperUser

回答:


7

より重要なことは、SOLID原則に従うか、よりシンプルなインターフェイスを使用するかです。

SOLIDとのインターフェース

これらは相互に排他的ではありません。インターフェイスは、ビジネスモデルの性質をビジネスモデルの用語で理想的に表現する必要があります。SOLIDの原則は、オブジェクト指向のコード保守性を最大化するためのKoanです(広義の「保守性」を意味します)。前者はビジネスモデルの使用と操作をサポートし、後者はコードのメンテナンスを最適化します。

開閉原理

「触らないで!」解釈が単純すぎます。そして、「クラス」がarbitrary意的であり、必ずしも正しいとは限らないことを意味すると仮定します。むしろ、OCPとは、動作を変更しても既存の動作中のコードを直接変更する必要がない(すべきではない)ようにコードを設計したことを意味します。さらに、最初にコードに触れないことが、既存のインターフェイスの整合性を維持するための理想的な方法です。私の見解では、これはOCPの重要な結果です。

最後に、OCPは既存の設計品質の指標と考えています。オープンクラス(またはメソッド)を頻繁にクラッキングしている場合、および/またはそうする理由がない場合(ハ、ハ)、本当に悪い理由がない場合OOのコーディング方法を知っている)。

最善を尽くしてください。

要件分析で、製品とレビューの関係を両方の観点から表現する必要があることがわかっている場合は、そうしてください。

したがって、Wolfgangには、これらの既存のクラスを変更する正当な理由があるかもしれません。新しい要件を考えると、レビューが現在製品の基本的な部分である場合、製品のすべての拡張がレビューを必要とする場合、そうすることでクライアントコードを適切に表現し、それを製品に統合します。


1
+1は、OCPがより良い品質の指標であり、コードの高速ルールではないことに注意してください。OCPを適切に追跡することが困難または不可能であることがわかった場合は、より柔軟な再利用を可能にするために、モデルをリファクタリングする必要がある兆候です。
CodexArcanum

私は製品とレビューの例を選んで、製品はプロジェクトのコアエンティティであり、レビューは単なるアドオンであることを指摘しました。そのため、製品は既に存在し、完成しており、レビューは後で行われ、既存のコード(製品を含む)を開かずに導入する必要があります。
ウルフギャング

10

ソリッドはガイドラインであるため、それを指示するのではなく、意思決定に影響を与えます。

静的メソッドを使用する際に注意すべきことの1つは、テスト可能性への影響です。


テストforProduct(Product product)は問題ありません。

それに依存する何かをテストします。

依存するCode-Under-Test(CUT)を分離してモックを使用するための継ぎ目はありません。これは、アプリケーションの実行中に静的メソッドが必ず存在するためです。

CUT()を呼び出すというメソッドを用意しましょうforProduct()

場合forProduct()であるstaticあなたは、テストすることはできませんCUT()アトミック単位として、あなたのすべてのテストは、テストユニットロジックその統合テストになります。

CUTのテストの失敗は、ユニットテストの診断上の利点を排除する問題(またはその依存コードのいずれCUT()か)によって引き起こされる可能性がありforProduct()ます。

詳細については、次の優れたブログ投稿をご覧ください:http : //googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


これは、テストに失敗したり、それらを取り巻く優れた実践と利益を放棄したりすることに対するフラストレーションにつながる可能性があります。


1
それは非常に良い点です。一般的には、私ではありません。;-)私は、複数のクラスをカバーする単体テストを書くことにそれほど厳密ではありません。それらはすべてデータベースに行きますが、それは私にとっては大丈夫です。そのため、ビジネスオブジェクトをモックしたり、検索したりしません。
ウルフギャング

+1、テストに赤い旗を立ててくれてありがとう!私は@Wolfgangに同意します。通常はそれほど厳密ではありませんが、そのようなテストが必要なときは、静的メソッドが本当に嫌いです。通常、静的メソッドがそのパラメーターとやり取りしすぎる場合、または他の静的メソッドとやり取りする場合は、インスタンスメソッドにすることをお勧めします。
アドリアーノRepetti

これは、使用されている言語に完全に依存していませんか?OPはJavaの例を使用しましたが、質問で言語について言及したことも、タグで指定された言語もありませんでした。
イズカタ

1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.-JavascriptとPythonはどちらも静的メソッドをオーバーライド/モックアウトできると信じています。しかし、100%確実ではありません。
イズカタ

1
@Izkata JSは動的に型指定されるため、持たないためstatic、クロージャーとシングルトンパターンでシミュレートします。Python(特にstackoverflow.com/questions/893015/…)を読んで、継承して拡張する必要があります。オーバーライドはm笑ではありません。コードをアトミ​​ックユニットとしてテストするための継ぎ目がまだないようです。
StuperUser

4

製品が製品のレビューを見つけるのに適切な場所であると思われる場合は、いつでも製品を支援するクラスを提供できます。(あなたのビジネスは製品の観点を除いてレビューについて決して話さないので、あなたは言うことができます)。

たとえば、レビューレトリーバーの役割を果たしたものを挿入したいと思うでしょう。おそらくインターフェイスを提供しますIRetrieveReviews。これを製品のコンストラクターに入れることができます(依存性注入)。変更したい場合はレビューを取得され、どのように異なるのコラボレーターを注入することにより、簡単にそれを行うことができます- TwitterReviewRetrieverまたはAmazonReviewRetrieverまたはMultipleSourceReviewRetrieverまたは任意の他、あなたが必要とします。

現在、両方に単一の責任があり(それぞれ製品関連のすべてのことを行い、レビューを取得しています)、将来的にはレビューに関する製品の動作を実際に製品を変更せずに変更できます(拡張することができます)などProductWithReviews)あなたが本当にあなたのSOLID原則についての知識をひけらかすになりたかったが、これは私にとって良い十分だろう場合。


サービス/コンポーネント指向のソフトウェアで非常に一般的なDAOパターンのように聞こえます。オブジェクトを取得することはこれらのオブジェクトの責任ではないことを表現しているため、このアイデアが気に入っています。しかし、私はサービス指向よりもオブジェクト指向の方法を好むので。
ウルフギャング

1
インターフェースを使用IRetrieveReviewsすると、サービス指向ではなくなります-レビューを取得する対象、方法、時期を決定しません。たぶん、それはこのようなことのための多くのメソッドを持つサービスです。たぶん、それは一つのことをするクラスです。たぶんそれはリポジトリであるか、サーバーへのHTTPリクエストを行います。あなたは知らない。知ってはいけません。それがポイントです。
ルニボー

ええ、それで戦略パターンの実装になります。これは3番目のクラスの引数になります。(A)と(B)はこれをサポートしません。ファインダーは間違いなくORMを使用するため、アルゴリズムを置き換える理由はありません。私の質問でこれについてはっきりしていなかったら申し訳ありません。
ウルフギャング

3

ProductsReviewクラスがあります。とにかくレビューは新しいと言います。だからといって何でもいいというわけではありません。変更する理由は1つだけです。何らかの理由でレビューの取得方法を変更する場合は、Reviewクラスを変更する必要があります。

それは正しくありません。

Reviewクラスに静的メソッドを入れているのは、なぜですか?それはあなたが苦労していることではありませんか?それが問題ではないでしょうか?

しないでください。製品のレビューを取得することが唯一の責任であるクラスを作成します。その後、それをProductReviewsByStartRatingにサブクラス化できます。または、サブクラス化して、製品のクラスのレビューを取得します。


Reviewのメソッドを使用するとSRPに違反することに同意しません。製品では、レビューではなく、レビューでした。私の問題は、静的であるということでした。メソッドを3番目のクラスに移動した場合、それはまだ静的であり、productパラメーターがあります。
ウルフギャング

つまり、Reviewクラスの唯一の責任、変更する唯一の理由は、forProduct静的メソッドを変更する必要がある場合だということですか?Reviewクラスには他の機能はありませんか?
-ElGringoGrande

Reviewにはたくさんのものがあります。しかし、私はforProduct(Product)がそれにぴったり合うと思います。レビューの検索は、レビューの属性と構造(属性を変更するスコープ、一意の識別子)に大きく依存します。ただし、製品はレビューを認識していないため、レビューを認識すべきではありません。
ウルフギャング

3

Productクラスにもクラスにも「製品のレビューを取得」機能を配置しませんReview...

製品を取得する場所がありますか?と何かGetProductById(int productId)多分GetProductsByCategory(int categoryId)など。

同様に、あなたが、あなたのレビューを取得するための場所を持っている必要がありますGetReviewbyId(int reviewId)し、多分GetReviewsForProduct(int productId)

製品のレビューの収集方法を変更する場合、製品クラスを変更する必要があります。

ドメインクラスからデータアクセスを分離する場合、レビューの収集方法を変更するときに、いずれのドメインクラス変更する必要はありません。


これは私がそれをどのように処理するかであり、あなたの投稿までこの答えが不足していることで混乱しています(そして自分のアイデアが適切かどうか心配しています)。明らかに、レポートを表すクラスも製品の表現も、実際に他方を取得する責任はありません。サービスを提供する何らかの種類のデータがこれを処理する必要があります。
CodexArcanum

@CodexArcanumさて、あなたは一人ではありません。:
エリックキング

2

パターンと原則はガイドラインであり、石に書かれたルールではありません。私の意見では、質問は、SOLID原則に従うか、よりシンプルなインターフェイスを維持する方が良いかどうかではありません。自問すべきことは、ほとんどの人にとってより読みやすく、理解しやすいものです。多くの場合、これは、可能な限りドメインに近い必要があることを意味します。

この場合、解決策(B)を好むでしょう。なぜなら、私にとって は出発点はレビューではなく製品であるが、レビューを管理するソフトウェアを書いていると想像してください。その場合、中心はレビューであるため、解決策(A)が望ましい場合があります。

このような多くのメソッド(クラス間の「接続」)がある場合、それらをすべて外部で取り除き、1つ(または複数)の新しい静的クラスを作成して整理します。通常、それらはクエリまたはリポジトリの一種として見ることができます。


また、両端のセマンティクスと、どちらが「より強力」であるかというこのアイデアについても考えていたので、ファインダーを主張します。だから(B)を思いついた。また、ビジネスクラスの「抽象化」の階層を作成するのに役立ちます。製品は、レビューが上位にある単なる基本的なオブジェクトでした。そのため、ReviewはProductを参照できますが、その逆はできません。このようにして、ビジネスクラス間の参照の循環を回避できます。これは、リスト上の別の問題を解決することになります。
ウルフギャング

少数のクラスのセットでは、両方のメソッド(A)と(B)を提供するのもいいと思います。あまりにも多くの場合、UIはこのロジックを書いている時点ではあまり知られていません。
アドリアーノRepetti

1

製品は静的なReviewメソッドに委任できます。この場合、自然な場所(Product.getReviews)に便利なインターフェイスを提供しますが、実装の詳細はReview.getForProductにあります。

SOLIDはガイドラインであり、シンプルで理にかなったインターフェイスになるはずです。または、単純で実用的なインターフェイスからSOLIDを派生させることもできます。コード内の依存関係管理がすべてです。目標は、摩擦を引き起こす依存関係を最小限に抑え、避けられない変化に対する障壁を作り出すことです。


これが欲しいかどうかはわかりません。このように、他の開発者が両方のクラスを調べてどこに配置するかを見る必要がないため、両方の場所にメソッドがあります。一方、静的メソッドと「閉じた」Productクラスの変更の両方の欠点があります。代表団が摩擦の問題を解決するかどうかはわかりません。ファインダを変更する理由があった場合、署名の変更、そして製品の変更が最も確実に行われます。
ウルフギャング

OCPの鍵は、コードを変更せずに実装を置き換えることができることです。実際には、これはリスコフ置換と密接に関係しています。ある実装は別の実装に代わるものでなければなりません。DatabaseReviewsとSoapReviewsをIGetReviews.getReviewsとgetReviewsForProductsの両方を実装する異なるクラスとして使用できます。その後、システムは拡張のために開かれ(レビューの取得方法を変更する)、変更のために閉じられます(IGetReviewsへの依存は壊れません)。それが依存関係管理の意味です。あなたの場合は、コードを修正して動作を変更する必要があります。
フリーズ

それはあなたが説明している依存性反転の原理(DIP)ではないですか?
ウルフギャング

それらは関連する原則です。置換ポイントはDIPによって達成されます。
フリーズ

1

私はこれについて、他のほとんどの答えとは少し異なる見解を持っています。基本的にデータ転送オブジェクト(DTO)であるProductと思いますReview。私のコードでは、DTO /エンティティが動作しないようにしています。これらは、私のモデルの現在の状態を保存するための素晴らしいAPIです。

OOとSOLIDについて話すとき、通常は状態を表すのではなく(必要に応じて)、代わりに質問に答える、または作業の一部を委任できるサービスを表す「オブジェクト」について話します。 。例えば:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

その後、あなたの実際ProductRepositoryExistingProductGetProductByProductIdメソッドなどを返します

これで、単一の責任原則に従います(継承するものIProductはすべて状態を保持し、継承するものIProductRepositoryはすべて、データモデルを永続化および再水和する方法を知る責任があります)。

データベーススキーマを変更すると、DTOなどを変更せずにリポジトリの実装を変更できます。

つまり、要するに、どちらのオプションも選択しないと思います。:)


0

静的メソッドはテストするのが難しいかもしれませんが、それはあなたがそれらを使用できないことを意味しません-あなたはそれらをテストする必要がないようにそれらをセットアップする必要があるだけです。

product.GetReviewsとReview.ForProductの両方を、次のような1行のメソッドにします。

new ReviewService().GetReviews(productID);

ReviewServiceには、より複雑なコードがすべて含まれており、テスト容易性のために設計されたインターフェイスがありますが、ユーザーには直接公開されません。

100%のカバレッジが必要な場合は、統合テストで製品/レビュークラスメソッドを呼び出してください。

クラスの設計ではなくパブリックAPIの設計について考えると、役立つ場合があります-そのコンテキストでは、論理的なグループ化を備えたシンプルなインターフェイスがすべてです-実際のコード構造は開発者にのみ関係します。

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