web-dev-qa-db-ja.com

何かをクラスにする必要があるのはいつですか?そして私の選んだクラスは良いOOデザインですか?

私の背景:

プログラミングは初めてです。 Pythonは私の唯一のプログラミング知識です。私は趣味としてプログラミングし、たくさんの本を読んで自分を教えています。

OOP書くことOOソフトウェアについては十分に理解しています。しかし、書くことについて不安を感じ続けています...デザインが乱雑に見えるなどクラス、メソッド、関数のどれが適切かわからない。

私は「オブジェクト指向思考」などのOOP本を読んでいます。そのような本は有用ですが、「ブラックジャック」ゲームの作成などの簡単なトピックを常に扱っています。欲しいアプリケーションしかし、書くことは、想像しやすい現実世界のオブジェクトに基づいていません。

アプリケーション:

私は現在プロジェクトに取り組んでいます。ソフトウェアの目的は、私のお気に入りのブログから画像やビデオをダウンロードすることです。 (例:Google Bloggerブログ、またはWordPressホストされているブログ。)

(はい、作るのは簡単です、そして私はすでにそれを作りました、しかし問題は私がエレガントで適切に設計された作り方を学びたいことですOOソフトウェア。)

クラス:

私は次のクラスに決めました:

  1. APIクライアント:これにより、さまざまなAPIリクエストの送受信の処理が容易になります。
  2. BloggerClient、WordPressClient:特定のAPIに特化できるように、ジェネリックAPIクライアントクラスから継承します。 Google、Tumblr、WordpressのAPI。
  3. パーサー:汎用メディアパーサー。これは、ブログからメディアを取得します。
  4. BloggerParser、WordPressParser:汎用的なパーサーから継承するため、各Webサイトに固有の投稿やメディアを取得できます。

使用例:

WordPress.comブログから画像を取得する例を紹介します。

  1. WordPressParserは、WordPressClientにphotography.wordpress.com からブログ投稿を取得するように要求します。
    1. WordPressClientはAPIリクエストをWordPressに送信します。
    2. WordPressクライアントは応答を受け取り、投稿をWordPressParserに送信します。
    3. WordPressParserは、各ブログ投稿のメディアリンク/ URLを解析します。
    4. メディアリンクがディスクにダウンロードされます。

問題:

ここで良い選択をしたかどうかは、実際にはわかりません。

たとえば、メディア(写真とビデオ)をディスクにダウンロードする必要があります。この場合、Parserクラスで 'download_media()'メソッドを作成する必要がありますか、それともDownloaderクラスを作成する方が良い設計ですか?

「Parse」は簡単にメソッドにすることができますが、代わりに「Parser」を使用したので、簡単にクラスにすることができます。何かがクラスやメソッドになるべき時は本当にわかりません。

BlogPostsのクラスが必要ですか?ブログ投稿はオブジェクトのようです(おそらく本のページに相当します)。

私は動詞、「ダウンロード」、「解析」を取り、有効なクラスとして表示するためにそれらを名詞に変換しているようです。

私はとても迷って混乱しています。繰り返しますが、私にとっての目的は、OOソフトウェアを適切に作成および設計する方法を学ぶことです。私は、プログラムを動作させるためだけに見ているわけではありません。

これがこれまでの私のクラスのそれぞれのソースコードです:

APIクライアント:http://Pastebin.com/QRdPqM5F

ベースパーサー:http://Pastebin.com/kV8gBEe6

パーサー:http://Pastebin.com/cC0yPYdK

拡張性の面では、私は将来的により多くのウェブサイトを追加するので、私は保守可能な何かが必要です。

私が間違っていることについて誰かが私に助言してくれることを願っています。上記のリンクで私のソースコードを表示して、私が何について話しているかを実際に確認できます。

ありがとうございました!

6
BBedit

WordPressParserはWordPressClientにブログ投稿の取得を要求します...

これは本当にパーサーがすべきことなのでしょうか?ブログの投稿を求めることは、用語の定義によって「解析」されることではありません。

それはあなたの地元の理髪店に来るようにあなたに招待を送るはさみのようなものです。さて、はさみは確かにそれを実現するためのビジネスを持っていますが、この一連のイベント全体をトリガーすることは、彼らが担当するべきもののようには聞こえません。

たとえば、メディア(写真とビデオ)をディスクにダウンロードする必要があります。この場合、Parserクラスで 'download_media()'メソッドを作成する必要がありますか、それともDownloaderクラスを作成する方が良い設計ですか?

次の理由により、別のクラスを作成します。

  1. ダウンロードは解析とは別の責任です- 単一責任の原則 に留意してください。プロジェクトの成長に合わせてどのクラスが何を実行するかを追跡するのに役立ち、コードを柔軟に保つことができます-変更が簡単です。

  2. 異なるパーサーがダウンロードを同様の方法で、またはまったく同じ方法で処理する場合があります。この機能を別のクラスに抽出することにより、同じDownloader実装を異なるパーサーで再利用できるため、コードの重複を回避できます。

BlogPostsのクラスが必要ですか?ブログ投稿はオブジェクトのようです(おそらく本のページに相当します)。

それらが文字列だけである場合-純粋なテキスト-それは必要ないかもしれませんが、BlogPostクラスを作成するコストはそれほど高くなく、少なくとも、コード全体でそのようなことを明示的にしますそのような変数はブログの投稿だけを表します!

大体の目安は プリミティブ型よりもドメイン固有の型を優先する です。 BlogPost手段何か。テキストの一部は何でもかまいません。私たちは人間であり、ドメイン固有のタイプを使用することで、バグのないコードの記述に役立ちます。タイプシステムに、タイプの不一致の違反を犯さないようにするためです(たとえば、ブログ投稿のURLリンクとブログ投稿のコンテンツの混同)。

免責事項:私はPythonを知りません、私は普遍的なOODの質問として質問に取り組みます。ここで私を逃れるPython固有のものがあれば、誰かが指摘してくれれば幸いです。

6
Konrad Morawski

私は動詞、「ダウンロード」、「解析」を取り、有効なクラスとして表示するためにそれらを名詞に変換しているようです。

私はとても迷って混乱しています。繰り返しますが、私にとっての目的は、OOソフトウェアを適切に作成および設計する方法を学ぶことです。私は、プログラムを動作させるためだけに見ているわけではありません。

オブジェクト指向設計を開始する際の最大の課題は、コンピューターに何かを実行させるために必要な手順を考えるのではなく、目的を達成するために相互作用するさまざまなオブジェクトの観点から問題について考えるために必要な精神的な変化です。

助けることができる1つの事は擬人化の練習です

友達や同僚を何人かつかむか、おもちゃのフィギュアや軍人を何人かつかんで部屋に行き、人を組織して、各人が1つのことをしたり、別のオブジェクトにやらせたりできるようにして、問題を解決します。ひとこと。

ほんの数人が多くの仕事をしているところから始めるかもしれません。他の人に他のタスクを実行するように指示しながら、各人が1つのことだけを実行するように、人を分割し続けます。

誰かに何かをするように言うことは彼らにメッセージを渡すことであり、そしてオブジェクト間で受け渡すメッセージはOODの中核ですです。オブジェクト自体を他のオブジェクトに渡すこともできます。そのため、第三者がこれを必要とする場合に、第三者が第三者に渡すことを想像してください。

この方法を使用してシステムを設計してみてください。 1人がやりすぎている場合は、さらに2人に分けます。 「私はこれをし、次にボブにこれをするように言います」と大声で言っている人々の上に部屋ができます。

この演習にはコードがないことに気づくでしょう。それは意図的です。システムの設計は、システムの実装に使用されるコンピューター言語よりもはるかに重要です。 OOプログラミング言語を学ぶよりも、オブジェクト間でメッセージパッシングを通じて目標を達成する動作の単位として考える方法を学ぶことの方がはるかに重要です。

この演習は、実際の問題だけでなく、上記のような非常に抽象的な問題の両方にも役立ちます。最終的には、各人が行う作業の詳細(引き出しを開いてフォームを取得するか、HTTP Webサーバーからフォームをダウンロードするため)彼らが言うことができるほどには関係がない。

1
Cormac Mulhall

最初に、一般的な問題解決のアドバイスをいくつか提供してから、それを問題に使用して、より経験のある人がこれにどのように取り組むかを確認します。

問題ドメイン

あなたが他で読んだかもしれないように、あなたのデザインとあなたのコードはあなたが解決しようとしている問題のあなたの理解の代表です。したがって、コーディングを開始する前、および設計を開始する前に、問題自体を分析する必要があります。これにより、ソリューションの設計を簡単に作成できます。途中で、問題または問題のviewを簡略化できる場合、それによってソリューションも同様に単純になります!そしてシンプルは良いです。

何かを設計したりコーディングしたりするときに問題が発生するのは、多くの場合、自分が取り組んでいる問題の一部を完全に理解していないためです。それ以外の場合は、物事を簡単にするいくつかの設計/実装手法に慣れていないためです。そのため、物事を読んでいるのは良いことです。

問題を分析するときは、問題のドメインのエンティティが誰であり、それらがどのように相互作用するかを確認してください。誰が誰に何を、誰が誰から何を、何がプロセスを行っているか、データはどこから/にデータが流れているかなど。エンティティとその相互作用を列挙できる場合、これは、思いつくソリューションが完全であることを保証するのに役立ちます。

設計

プログラムを設計するときは、問題空間から概念を取り出し、作業している言語/パラダイムにマッピングしようとします。OOPの場合、これはオブジェクトとメソッドです。

OOPを設計するときは、問題空間の名詞と動詞を確認する必要があります。誰が誰に何をしたか、だれが誰から何を取得したかなどです。これらはオブジェクト(「who(m)」)とメソッド( 「する」)。

これについて考えているときに私に起こったのは、どの名詞がアクションの対象であり、どの名詞がオブジェクトであるかを検討すること、つまり「誰」と「誰」を区別することは有益だということです。これらは当然、どのオブジェクトが何に対してどのアクションを実行するかを導きます。例えば。:

person.putOn(hat)

この場合、personhatに作用するものであるため、メソッドは人物に属します。

ただし、デザインを逆にすると、デザインがうまく機能する場合があります。

hat.sitOn(person)

それはデザインの残りの部分と物事がどのようにマップするかによって異なります。これらは、注意して検討すべきことです。

ハードで高速OOPは、問題空間から最も簡潔な方法で物事をマッピングできるとは限りません。アクションは、特定のオブジェクトに属しているように見えない場合があります。そのような場合は、単純な関数を使用できます-それ以外の場合は、クラスに気づいた動詞の名詞化を取得します( 他の人もこれに気付いた 。)

あなたのプロジェクト

上記を取り上げて、プロジェクトに適用しましょう。

まず、問題のあるドメインを見てみましょう。

ソフトウェアの目的は、私のお気に入りのブログから画像やビデオをダウンロードすることです。 (例:Google Bloggerブログ、またはWordPressホストされているブログ。)

選手は何ですか?アクション?プロセス?物事はどのように関連していますか(時間、場所などで)?

まあ、ブログがあり、ブログには投稿があり、投稿にはメディアがあります。ブログはサービスによってホストされています。サービスには、ブログ/投稿/メディア*にアクセスできるAPIがあります-APIを使用すると、特定の形式(JSON、XMLなど)でブログ(フィード)をダウンロードでき、ブログのコンテンツは、特定の構造で配置されますこのフォーマット。このコンテンツの一部はメディアへの参照になります。

*サービスの投稿のメディアアイテムにアクセスするために使用するAPIが必要かどうか、または通常のWeb URLを介してそれらにアクセスできるかどうかはわかりません。ここで注意を払い、それらにアクセスするためにAPIが必要であると想定します

次に、問題のドメインでデータがどのように流れるかを考えてみましょう。サービスのAPIにリクエストを送信し、メディアアイテムへの参照を含むフィードコンテンツを含むレスポンスを取得します。これらのメディア参照を抽出し、APIを使用してメディアアイテムのコピーを取得し、ディスクにファイルとして保存します。 (このような相互作用を紙に描くと便利です。たとえば、 シーケンス図 を使用します。)

次に、問題のドメインがOOP(+ functions)にどのようにマップされるかを見てみましょう:

問題のドメインの名詞はプログラムのオブジェクトにマッピングされると上記で述べました。これは本当ですが、必ずしもすべてがマップされるわけではありません。この問題空間の名詞の一部はリモートの場所(サーバーなど)にあるため、プログラムでそれらをモデル化する必要がない場合があります(ただし、それらが存在することを知っておくと便利です)。

大きなアイテムから始めます。物事のリモート側には、サービスがあります。サービスにはAPIがあります。私たちの側では、私たち(プログラム)と、サービスのAPIと対話するためのものがあります。したがって、API接続-またはclient-は、実装済みの概念であり、すでに実行済みです。

クライアントはブログフィードをダウンロードするためにAPIに接続するため、APIクライアントクラス(名詞)にはdownloadFeedメソッド(動詞/アクション)が必要です。

お気づきのように、これはデザイン面で少しトリッキーになる場所です。なぜなら、基本的に同じ概念に対して複数のレベルの抽象化があるためです。ブログとその投稿とメディアには抽象的な概念がありますが、それだけではありませんAPIクライアントがサービスから受信するブログフィードの具体的な表現(JSON、XML)。

だから何をすべきか?まあ、APIクライアントがフィードを処理する唯一のものである場合は、異なるAPIクライアントのそれぞれで具体的な表現のままにすることができます。ただし、プログラムでフィードデータを処理する場合は、すべてのAPIで共通の表現(ブログフィードクラス、ブログ投稿クラス、メディアアイテムまたはメディア参照クラスなど)を使用する必要があります。クライアントは、プログラムの残りの部分を使用するために戻る必要があります。

フィードデータをプログラムの他の部分で使用する必要がありますか?私の答えは「はい」です。 単一の責任の原則(SRP) 、および 懸念の分離(SoC) の理由を確認するために、いくつかのベストプラクティスに目を向けることができます。

SRPは、すべてのクラスまたはメソッドが1つのこと、1つのことだけを行う責任を負うべきだと述べています。 APIクライアントの目的は、プログラムの残りの部分に代わってさまざまなAPIと通信することです。 APIクライアントクラス内にデータを保持する方法を見てみましょう。

クライアントでAPIからデータをダウンロードし、フィードの特定の表現を取得します。これは問題ありません。クライアントが実行することになっています。

次に、フィードからメディア情報を抽出します。これは、この場合のサブ責任のようなものです。フィードの形式はAPIに直接関連していますが、APIクライアントはAPIとの通信のみを目的としています。この場合、抽出はアクションなので、関数を使用しますが、この関数をAPIクライアントクラスの一部にすることは許容できます。データの抽出が十分に複雑である場合、抽出を支援するためにヘルパークラスが必要になる可能性があります。これは、Parserタイプとして構築したものです。これについては少し後で説明します。

次に、画像をダウンロードする必要があります(アクション、つまりメソッド)。これは、APIと通信する必要があるため、APIクライアントのレルム内にあると判断しました。

最後に、イメージをディスクにキャッシュする必要があります(別のアクションですが、これは名詞化する必要がある場合があります)。これは、APIクライアントの責任に違反しています。クライアントは、サービスAPIと通信するためのものであり、ディスクに物をキャッシュするためのものではありません。キャッシングは別の問題です!さらに、クライアントにキャッシュを実行させると、同じロジックをすべてのクライアントで繰り返す必要があり、これは の違反になります。 [〜#〜]乾燥[〜#〜]

そのため、クライアント間で使用できるように、APIクライアントの外部にキャッシュロジックを配置することをお勧めします。これは、フィードをキャッシングコードで利用できるように、フィードの外部の一般的な表現が必要になることを意味します。

次に、各APIクライアントは、フィードの特定の表現(「whom」名詞)をクライアントの外部で使用するための一般的な表現(「whom」名詞)に変換(動詞)する必要があります。これは関数で行う必要があります(ヘルパークラスを使用する場合があります)。

これまでのフローは次のとおりです。APIクライアントはブログフィードのAPI固有の表現をダウンロードし、この表現を一般的な表現に変換してから、この表現を外部プログラムに返します。外部プログラムはこの一般的な表現を受け取り、それをディスクにキャッシュするキャッシュロジックに渡します。

このフローは良好ですが、メディアが1つ不足しています。画像と動画をダウンロードする必要があります!

メディアは、ブログフィードがダウンロードされてからディスクにキャッシュされるまでの間のいずれかの時点でダウンロードする必要があります。これは別の設計上のポイントです。いつcan完了し、いつすべきか完了します。

メディアをダウンロードする明らかなポイントの1つは、フィードからのデータが抽出されて一般的な表現に入れられた直後で、APIクライアントから残りのプログラムに返される前です。メディアは、フィード機能の出力に追加できます。したがって、次のようになります。

def downloadFeed():
    rawFeed = self.downloadRawFeed()
    commonFeed = self.extractFeedDataFrom(rawFeed)
    self.downloadAndPopulateImagesOf(commonFeed)
    return commonFeed

これに代わるものがあり、それはそれらがキャッシュされる直前にメディアをダウンロードさせることです-このように、あなたはすべての単一のメディアアイテムを含むフィードで終わることはありませんそのフィードからすぐにメモリに!

あなたができることは、たとえば、メディアに関する情報を保持するMediaReferenceクラスのインスタンスと一般的な表現を、実際にダウンロードできるようにAPIクライアントへの参照と一緒に、そしてキャッシングすることです。コードは、MediaReferenceインスタンスに、メディアをダウンロードして1つずつ与えるように指示できます(一度にメモリに存在しないようにするため)。

そして、あなたはこのもののデザインについていくつかのインプットを持っています。 (くそー、なんて長い記事!)

ここで良い選択をしたかどうかは、実際にはわかりません。

あなたのスタートはかなり良かったので、それを続けてください!できます!

パーサー

あなたがパーサーと呼んでいるものについての簡単なメモ。

コメントで述べたように、パーサーと呼んでいるものは実際のパーサーではありませんが、パーサーなので、気にしないでください。実際の parser (コンピューターサイエンスの定義を参照)は lexer とともに、入力文字列(たとえば、JSON)を取り、それをオブジェクトのツリーに変換します。階層。それでおしまい。それ以降は、そのデータに対して何もしません。

あなたがしたいことは scrape 情報のためのデータです。パーサー(APIの使用方法に応じてJSONまたはXMLパーサーのいずれか)がこれに役立つ可能性があり、APIデータをコードで簡単に解読できる形式にして、一連の文字列操作を行う必要がないようにします。 。

これで完了です。

1
paul

それは本当にあなたが行くところに依存します。メソッドではなくクラスを使用する理由は、クラスが、利用できるいくつかの強力なOOパターンの開始点であるからです。

クラスを取得し、継承とポリモーフィズムを追加すると、コントラクトベースのプログラミング(つまり、インターフェイス)を利用できるようになります。これは非常に重要です。これにより、動作を抽象的に定義し、コードベースのさまざまな部分を明確に分離することができます。これは制御の反転(IoC)です。コードは、具象クラスではなく、抽象化/インターフェースに依存する必要があります。

インターフェイスに対してプログラミングを開始すると、コードの実装を別のコードから分離できます。コントラクトが変更されない限り、一方のコードを変更しても、もう一方に影響を与える必要はありません。

ここでSOLIDが登場し始めました。単一責任原則(SRP)は、コードは変更する理由が1つだけあるべきだと述べています。クラスがきれいに分離されている場合はそうなります。

次に、Open/Closed原則(OCP)は、コードを拡張のために開き、変更のために閉じる必要があることを示しています。つまり、古いインターフェースを使用する新しいインターフェースを作成することにより、インターフェースを拡張できるため、古いインターフェースの実装は引き続き有効です。同様に、入出力(コントラクト)が同じである限り、インターフェースの具体的な実装で何が起こっているかをリファクタリングできます。

これで分離ができたので、依存関係の注入やモックされた依存関係を介したユニットテストなど、さらに優れた機能を実行できます。 DIを使用すると、すべての外部依存関係が(通常は)コンストラクターを介してオブジェクトに「注入」されるようにコードを構造化できます。クラスはそれ自体のサービス作成を行わず、それらを提供するためにそのコンシューマーに依存する必要があります。これはSRPと一致しています。これにより、モックされたオブジェクトを渡し、テストをそのクラスに分離できるため、テストが容易になります。

私がここに入ることができる他のものがあります、それは非常に広いトピックです。しかし、それは高いレベルです。基本的に、サービスをクラスに変更することは、サービスを変更したり、別の方法で実行したりできると考えた場合に適しています。たとえば、あなたのダウンロードは、SRPによると、それは間違いなく独自のものでなければなりません。それがヘルパーメソッドとクラスのどちらであるかについては、自問してみてください。すべて(入力)で同じように使用されるようにこれを書くことができますか?ディスク(出力)以外にダウンロードしたい場所はありますか?他のケースが考えられる場合は、コンシューマーによって異なる実装とのインターフェースにしてください。 (その実装をファクトリで取得します)。

パーサーも、クラスやインターフェースであることから恩恵を受けます。これは、ポリモーフィズムを使用して、実行し始めたときに解析していることの種類に基づいて、実行時に実装を変更できるためです。

ここで欠けているのは、Parser(またはいくつかの標準的な標準ではIParser)のインターフェースが1つあることです。 IParserインターフェイス、すべてのパーサーの共通コードを実装するParser基本クラス、およびサイト固有の方法でコントラクトを実行するParserから継承するIParserの具体的な実装が必要です。次に、メインコードパスはDIまたはファクトリを使用してIParserの具体的なインスタンスを取得し、それに対して作業を行います。これは、依存関係がインターフェースに依存しているだけであり、どの実装もないため、リファクタリング、テスト、および保守性が大幅に向上します。

私は続けることができましたが、私は十分に取り乱したように感じています。これがお役に立てば幸いです。

1
control