私は現在、ビデオ監視映像の圧縮とインデックス作成を実行するソフトウェアプロジェクトに取り組んでいます。圧縮は、背景オブジェクトと前景オブジェクトを分割し、背景を静止画像として、前景をスプライトとして保存することで機能します。
最近、プロジェクト用に設計したクラスのいくつかを復習することに着手しました。
パブリックメソッドが1つしかないクラスがたくさんあることに気づきました。これらのクラスの一部は次のとおりです。
compress
の入力ビデオを取り込み、タイプRawVideo
の出力ビデオを返すCompressedVideo
メソッドを使用)。split
メソッドを使用して、タイプRawVideo
の入力ビデオを取り込み、タイプRawVideo
の2つの出力ビデオのベクトルを返します)。index
タイプの入力ビデオを取り込み、RawVideo
タイプのビデオインデックスを返すVideoIndex
メソッドを使用)。VideoCompressor.compress(...)
、VideoSplitter.split(...)
、VideoIndexer.index(...)
のような呼び出しを行うために、各クラスをインスタンス化していることに気づきました。
表面的には、クラス名は意図した機能を十分に説明していると思いますが、実際には名詞です。対応して、それらのメソッドも動詞です。
これは実際に問題ですか?
いいえ、これは問題ではなく、まったく逆です。これは、モジュール性とクラスの明確な責任のしるしです。無駄のないインターフェースは、そのクラスのユーザーの視点から理解しやすく、疎結合を促進します。これには多くの利点がありますが、欠点はほとんどありません。より多くのコンポーネントがそのように設計されることを望みます!
オブジェクト指向ではなくなりました。これらのクラスは何も表さないため、関数の単なるベッセルです。
それはそれが間違っているという意味ではありません。機能が十分に複雑である場合、または汎用的である場合(つまり、引数が具体的な最終型ではなくインターフェースである場合)、その機能を個別のmodule。
そこからあなたの言語に依存します。言語に無料の関数がある場合、それらは関数をエクスポートするモジュールである必要があります。それがクラスではないのに、それがクラスではないふりをするのはなぜですか。言語に次のような無料の機能がない場合: Javaの場合、単一のパブリックメソッドでクラスを作成します。まあ、それはオブジェクト指向設計の限界を示しているだけです。場合によっては、機能が単純な方が適していることがあります。
単一のパブリックメソッドを持つインターフェイスを実装する必要があるため、単一のパブリックメソッドを持つクラスが必要になる場合があります。オブザーバーパターンや依存関係の注入などに使用できます。ここでも言語に依存します。ファーストクラスのファンクタ(C++(std::function
またはテンプレートパラメータ)、C#(デリゲート)、Python、Perl、Ruby(proc
)、LISP、Haskell、...)これらのパターンは関数型を使用し、donクラスは必要ありません。 Javaには(まだ、バージョン8にはあります)関数型がないため、単一メソッドインターフェイスと対応する単一メソッドクラスを使用します。
もちろん、私は単一の巨大な関数を書くことを主張していません。プライベートサブルーチンが必要ですが、それらは実装ファイル(C++ではファイルレベルの静的または匿名の名前空間)にプライベートにすることも、パブリック関数( )内でのみインスタンス化されるプライベートヘルパークラスに含めることもできます。データかどうか )。
特定のメソッドを専用クラスに抽出する理由がある場合があります。これらの理由の1つは、依存性注入を許可することです。
VideoExporter
というクラスがあり、最終的にはビデオを圧縮できるはずです。きれいな方法はインターフェースを持つことです:
interface IVideoCompressor
{
Stream compress(Video video);
}
これは次のように実装されます:
class MpegVideoCompressor : IVideoCompressor
{
// ...
}
class FlashVideoCompressor : IVideoCompressor
{
// ...
}
次のように使用します:
class VideoExporter
{
// ...
void export(Destination destination, IVideoCompressor compressor)
{
// ...
destination = compressor(this.source);
// ...
}
// ...
}
badの代替案は、たくさんのパブリックメソッドがあり、圧縮を含むすべてのジョブを実行するVideoExporter
を持つことです。それはすぐにメンテナンスの悪夢となり、他のビデオ形式のサポートを追加するのを難しくします。
これは、関数を引数として他の関数に渡したいという兆候です。あなたの言語(Java?)はそれをサポートしていないと思います。それが事実である場合、選択した言語の欠点であるので、それはあなたのデザインでそれほど失敗しているわけではありません。これは、すべてがクラスでなければならないことを主張する言語の最大の問題の1つです。
これらの偽関数を実際に渡していない場合は、自由/静的関数が必要です。
私はパーティーに遅れていることを知っていますが、誰もがこれを指摘するのを逃したようです:
戦略パターンは、副問題を解決するためのいくつかの可能な戦略がある場合に使用されます。通常、すべての実装でコントラクトを強制するインターフェースを定義し、何らかの形式の Dependency Injection を使用して具体的な戦略を提供します。
たとえば、この場合、_interface VideoCompressor
_を使用し、_class H264Compressor implements VideoCompressor
_や_class XVidCompressor implements VideoCompressor
_などのいくつかの代替実装を使用できます。関係するインターフェースがあることはOPからは明らかではありません。関係がない場合でも、元の作成者が必要に応じて戦略パターンを実装するためにドアを開いたままにしていた可能性があります。どちらもそれ自体が優れたデザインです。
OPがクラスをインスタンス化してメソッドを呼び出すことに気づく問題は、依存関係の注入と戦略パターンを正しく使用していないことです。必要な場所でインスタンス化する代わりに、包含クラスには戦略オブジェクトを持つメンバーが必要です。そして、このメンバーは、例えばコンストラクターに注入する必要があります。
多くの場合、戦略パターンは、単一のdoStuff(...)
メソッドを持つインターフェースクラス(表示)になります。
public interface IVideoProcessor
{
void Split();
void Compress();
void Index();
}
あなたが持っているものはモジュール式であり、それは良いことですが、これらの責任をIVideoProcessorにグループ化する場合、DDDの観点からはおそらくそれがより理にかなっています。
一方、分割、圧縮、インデックス付けがまったく関係がない場合は、それらを個別のコンポーネントとして保持します。
これは問題です。データではなく、設計の機能面から作業しています。実際に持っているのは、OO化された3つのスタンドアロン関数です。
たとえば、VideoCompressorクラスがあるとします。ビデオを圧縮するように設計されたクラスを使用するのはなぜですか?このタイプの各オブジェクトに含まれる(ビデオ)データを圧縮するためのメソッドを持つVideoクラスがないのはなぜですか?
OOシステムを設計する場合、適用できるアクティビティを表すクラスではなく、オブジェクトを表すクラスを作成するのが最善です。昔は、クラスはタイプと呼ばれていました-OOは、新しいデータ型をサポートして言語を拡張する方法でした。OOのように考えると、クラスを設計するためのより良い方法が得られます。
編集:
もう少しよく説明してみましょう。concatメソッドを持つ文字列クラスを想像してみてください。クラスからインスタンス化された各オブジェクトに文字列データが含まれているようなものを実装できるので、
_string mystring("Hello");
mystring.concat("World");
_
しかし、OPは次のように動作することを望んでいます。
_string mystring();
string result = mystring.concat("Hello", "World");
_
現在、関連する関数のコレクションを保持するためにクラスを使用できる場所がありますが、それはOOではありません。コードベースの管理に役立つ言語のOO機能を使用する便利な方法です。そのような場合のオブジェクトは完全に人工的なものであり、このような問題を管理するために言語が提供するものは何もないため、このように単純に使用されます。たとえば、C#などの言語では静的クラスを使用してこの機能を提供します。クラスメカニズムを再利用しますが、オブジェクトのメソッドを呼び出すためだけにオブジェクトをインスタンス化する必要はありません。string.IsNullOrEmpty(mystring)
などのメソッドで終了します。考えるのはmystring.isNullOrEmpty()
に比べると劣ります。
したがって、「クラスを設計するにはどうすればよいですか」と質問された場合は、クラスに含まれる関数ではなく、クラスに含まれるデータについて考えることをお勧めします。 「クラスはメソッドの束です」に行くと、「より良いC」スタイルのコードを書くことになります。 (Cコードを改善している場合、これは必ずしも悪いことではありません)しかし、最高のOO設計されたプログラムを提供することはできません。
[〜#〜] isp [〜#〜] (インターフェース分離の原則)は、クライアントが使用しないメソッドに依存することを強制されるべきではないことを示しています。利点は複数あり、明確です。あなたのアプローチは完全にISPを尊重し、それは良いことです。
また、ISPを考慮した別のアプローチは、たとえば、メソッドごと(または凝集度の高いメソッドのセット)ごとにインターフェイスを作成し、それらすべてのインターフェイスを実装する単一のクラスを持つことです。これがより良いソリューションであるかどうかは、シナリオによって異なります。これの利点は、依存性注入を使用する場合、異なるコラボレーター(各インターフェースに1つ)を持つクライアントが存在する可能性があることですが、最終的にはすべてのコラボレーターが同じオブジェクトインスタンスをポイントします。
ところで、あなたは言った
各クラスをインスタンス化している
、これらのクラスはサービス(したがってステートレス)のようです。シングルトンにすることを考えましたか?
はい、問題があります。しかし、それほど深刻ではありません。私はあなたがこのようにあなたのコードを構成することができ、悪いことは何も起こらないことを意味します、それは保守可能です。しかし、そのような構造にはいくつかの弱点があります。たとえば、ビデオの表現(この場合はRawVideoクラスにグループ化されている)が変更された場合、すべてのオペレーションクラスを更新する必要があると考えてください。または、ランタイムが異なるビデオの複数の表現がある可能性があることを考慮してください。次に、表現を特定の「操作」クラスに一致させる必要があります。また、主観的に、smthで実行する各操作の依存関係をドラッグするのは面倒です。また、操作が不要になった、または新しい操作が必要であると判断するたびに、渡される依存関係のリストを更新します。
また、それは実際にはSRPの違反です。一部の人々は、SRPを責任を分割するためのガイドとして扱います(そして、各操作を個別の責任として扱うことによって、それを遠くに持っていきます)が、SRPは、責任をグループ化するためのガイドであることも忘れています。また、SRPの責任によると、同じ理由で変更された場合はグループ化する必要があるため、変更が発生した場合は、可能な限り少ないクラス/モジュールにローカライズされます。大きなクラスについては、それらのアルゴリズムが関連している限り、同じクラスに複数のアルゴリズムが存在することは問題ではありません(つまり、そのクラスの外で知られてはいけないいくつかの知識を共有します)。問題は、アルゴリズムがまったく関係のない大きなクラスであり、さまざまな理由で変更/変化することです。