web-dev-qa-db-ja.com

フラグをチェックする必要をなくすデザインパターンはありますか?

文字列ペイロードをデータベースに保存します。 2つのグローバル構成があります。

  • 暗号化
  • 圧縮

これらは、どちらか一方のみを有効にする、両方を有効にする、または両方を無効にするという方法で、構成を使用して有効または無効にすることができます。

私の現在の実装はこれです:

if (encryptionEnable && !compressEnable) {
    encrypt(data);
} else if (!encryptionEnable && compressEnable) {
    compress(data);
} else if (encryptionEnable && compressEnable) {
    encrypt(compress(data));
} else {
  data;
}

私はDecoratorパターンについて考えています。それは正しい選択ですか、それとももっと良い代替案がありますか?

28
Damith Ganegoda

コードを設計するときは、常に2つのオプションがあります。

  1. それを成し遂げるだけです。その場合、ほとんどすべての解決策があなたのために機能します
  2. 知識を深め、言語の癖とそのイデオロギーを利用するソリューションを設計する(この場合、OO言語-決定を提供する手段としての多態性の使用)

言うべきことは本当に何もないので、私は2つのうちの最初のものに焦点を合わせるつもりはありません。機能させるだけの場合は、コードをそのままにしておくことができます。

しかし、あなたがそれを徹底的な方法で行うことを選択し、実際に設計パターンの問題をあなたが望む方法で解決した場合、どうなるでしょうか?

次のプロセスを見てください。

OOコードを設計する場合、コード内にあるifsのほとんどはそこにある必要はありません。当然、intsやfloatsなどの2つのスカラー型を比較す​​る場合は、ifを使用している可能性がありますが、構成に基づいて手順を変更する場合は、 polymorphism を使用して必要に応じて、決定(ifs)をビジネスロジックから、オブジェクトがインスタンス化される場所に移動します- factories に。

現在、プロセスは4つの別々のパスを通過できます。

  1. dataは暗号化も圧縮もされていません(何も呼び出さず、dataを返します)
  2. dataは圧縮されています(compress(data)を呼び出して返します)
  3. dataは暗号化されています(encrypt(data)を呼び出して返します)
  4. dataは圧縮および暗号化されています(encrypt(compress(data))を呼び出して返します)

4つのパスを見ただけで問題が見つかります。

データを操作してそれを返す3つの異なるメソッド(1つとして何も呼び出さない場合は理論的には4)を呼び出す1つのプロセスがあります。 メソッドにはさまざまな名前、いわゆるパブリックAPI(メソッドが動作を伝達する方法)があります。

adapter パターンを使用して、発生した衝突(パブリックAPIを統合できます)の名前を解決できます。簡単に言うと、アダプターは互換性のない2つのインターフェースが一緒に機能するのに役立ちます。また、アダプターは、新しいアダプターインターフェイスを定義することで機能します。クラスは、API実装を統合しようとします。

これは具体的な言語ではありません。これは一般的なアプローチであり、anyキーワードは任意のタイプであることを表すためにあり、C#のような言語では、ジェネリック( <T>)。

私は今、あなたは圧縮と暗号化を担当する2つのクラスを持つことができると仮定します。

class Compression
{
    Compress(data : any) : any { ... }
}

class Encryption
{
    Encrypt(data : any) : any { ... }
}

エンタープライズの世界では、これらの特定のクラスでさえ、classキーワードがinterfaceに置き換えられるなど、インターフェイスに置き換えられる可能性が非常に高くなります(C#、Java、PHPなどの言語を扱う場合)または、classキーワードは残りますが、C++でコーディングする場合、CompressおよびEncryptメソッドは pure virtual として定義されます。

アダプターを作成するには、共通のインターフェースを定義します。

interface DataProcessing
{
    Process(data : any) : any;
}

次に、インターフェースの実装を提供して、インターフェースを有用にする必要があります。

// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
    public Process(data : any) : any
    {
        return data;
    }
}

// when only compression is enabled
class CompressionAdapter : DataProcessing
{
    private compression : Compression;

    public Process(data : any) : any
    {
        return this.compression.Compress(data);
    }
}

// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(data);
    }
}

// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
    private compression : Compression;
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(
            this.compression.Compress(data)
        );
    }
}

これを行うと、4つのクラスになり、それぞれがまったく異なることを行いますが、それぞれが同じパブリックAPIを提供します。 Processメソッド。

None/encryption/compression/bothの両方の決定に対処するビジネスロジックでは、以前に設計したDataProcessingインターフェイスに依存するようにオブジェクトを設計します。

class DataService
{
    private dataProcessing : DataProcessing;

    public DataService(dataProcessing : DataProcessing)
    {
        this.dataProcessing = dataProcessing;
    }
}

その場合、プロセス自体は次のように単純になる可能性があります。

public ComplicatedProcess(data : any) : any
{
    data = this.dataProcessing.Process(data);

    // ... perhaps work with the data

    return data;
}

条件文はもうありません。クラスDataServiceは、dataProcessingメンバーに渡されたときにデータに対して実際に何が行われるかを認識していません。また、実際にそれを気にせず、その責任はありません。

理想的には、作成した4つのアダプタークラスを単体テストでテストして、それらが機能することを確認し、テストに合格するようにします。そして、それらがパスした場合、コード内のどこで呼び出しても、それらが機能することはかなり確実です。

この方法でこれを行うと、コードにifsが含まれることはなくなりますか?

いいえ。ビジネスロジックに条件が含まれる可能性は低くなりますが、条件はどこかにある必要があります。場所はあなたの工場です。

そして、これは良いことです。作成の問題と実際にコードを使用する問題を分離します。工場を信頼できるものにする場合(Javaでは、 Guice Googleのフレームワークのようなものまで使用できます)、ビジネスロジックでは、注入される正しいクラス。あなたはあなたの工場が働いていることを知っていて、求められているものを届けるからです。

これらすべてのクラス、インターフェースなどが必要ですか?

これで最初に戻ります。

OOPでは、ポリモーフィズムを使用するパスを選択し、実際にデザインパターンを使用したい、言語の機能を利用したい、および/またはすべてをオブジェクトイデオロギーにしたい場合は、そうです。そして、それでも、この例では、必要になるすべてのファクトリを示していません。CompressionおよびEncryptionクラスをリファクタリングしてそれらをインターフェイスにする場合は、それらの実装も含める必要があります。

最終的に、非常に具体的なことに焦点を当てた、何百もの小さなクラスとインターフェースができあがります。これは必ずしも悪いことではありませんが、2つの数値を加算するだけの簡単なことをしたいだけの場合は、最善の解決策とは言えません。

あなたがそれを迅速にやりたいなら、あなたは Ixrecの解決策 をつかむことができます。少なくとも、else ifブロックとelseブロックを排除できました。単純なifよりも。

これがmyの優れたOOデザインを作成する方法であることを考慮してください。実装ではなくインターフェースへのコーディング。これが過去数年間私がそれを行った方法であり、私が最も快適なアプローチです。

私はif-lessプログラミングを個人的に気に入っており、5行のコードよりも長いソリューションを高く評価しています。これは、コードの設計に慣れている方法であり、コードを非常に快適に読むことができます。


更新2:私のソリューションの最初のバージョンについては、激しい議論がありました。議論は主に私によって引き起こされました。

解決策を見る方法の1つになるように回答を編集することにしましたが、唯一の方法ではありません。アダプターはファサードのバリエーションであるため、ファサードを意味するデコレーター部分も削除しました。

16
Andy

現在のコードで私が目にする唯一の問題は、設定を追加すると組み合わせが爆発するリスクです。これは、次のようにコードを構造化することで簡単に軽減できます。

if(compressEnable){
  data = compress(data);
}
if(encryptionEnable) {
  data = encrypt(data);
}
return data;

これが例と考えることができる「設計パターン」または「イディオム」を知りません。

118
Ixrec

あなたの質問は実用性を求めているのではないと思います。その場合、lxrecの答えは正しいものですが、設計パターンについて学ぶ必要があります。

明らかに コマンドパターン は、提案した問題のような些細な問題に対してはやりすぎですが、ここでは例として説明します。

public interface Command {
    public String transform(String s);
}

public class CompressCommand implements Command {
    @Override
    public String transform(String s) {
        String compressedString=null;
        //Compression code here
        return compressedString;
    }
}

public class EncryptCommand implements Command {
    @Override
    public String transform(String s) {
        String EncrytedString=null;
        // Encryption code goes here
        return null;
    }

}

public class Test {
    public static void main(String[] args) {
        List<Command> commands = new ArrayList<Command>();
        commands.add(new CompressCommand());
        commands.add(new EncryptCommand()); 
        String myString="Test String";
        for (Command c: commands){
            myString = c.transform(myString);
        }
        // now myString can be stored in the database
    }
}

ご覧のとおり、コマンド/変換をリストに入れると、それらを順次実行できます。明らかにそれは両方を実行するか、またはそれらの一方だけがif条件なしでリストに入れたものに依存します。

明らかに、条件文はコマンドリストをまとめるある種のファクトリで終了します。

@ texacreのコメントの編集:

ソリューションの作成部分でif条件を回避する方法はたくさんあります。たとえば、デスクトップGUIアプリを取り上げましょう。圧縮および暗号化オプションのチェックボックスを使用できます。の中に on clicこれらのチェックボックスのイベントにより、対応するコマンドをインスタンス化してリストに追加するか、オプションの選択を解除している場合はリストから削除します。

12

「デザインパターン」は、不必要に「ooパターン」に向けられており、より単純なアイデアを完全に避けていると思います。ここで話しているのは、(単純な)データパイプラインです。

私はそれをclojureでやろうと思います。関数がファーストクラスである他の言語もおそらく問題ありません。後でC#の例を使用できるかもしれませんが、それはいいとは言えません。これを解決する私の方法は、次のステップで、クロジュリアンでない人のためのいくつかの説明があります:

1。一連の変換を表します。

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

これは、キーワードから関数へのマップ、つまりルックアップテーブル/辞書/その他です。別の例(キーワードから文字列へ):

(def employees { :A1 "Alice" 
                 :X9 "Bob"})

(employees :A1) ; => "Alice"
(:A1 employees) ; => "Alice"

したがって、(transformations :encrypt)または(:encrypt transformations)は暗号化関数を返します。 ((fn [data] ... )は単なるラムダ関数です。)

2。キーワードのシーケンスとしてオプションを取得:

(defn do-processing [options data] ;function definition
  ...)

(do-processing [:encrypt :compress] data) ;call to function

。提供されたオプションを使用してすべての変換をフィルタリングします。

(let [ transformations-to-run (map transformations options)] ... )

例:

(map employees [:A1]) ; => ["Alice"]
(map employees [:A1 :X9]) ; => ["Alice", "Bob"]

4。関数を1つに結合する:

(apply comp transformations-to-run)

例:

(comp f g h) ;=> f(g(h()))
(apply comp [f g h]) ;=> f(g(h()))

5。そして、一緒に:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress])

「debug-print」などの新しい関数を追加する場合の唯一の変更点は次のとおりです。

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )
                       :debug-print (fn [data] ...) }) ;<--- here to add as option

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress :debug-print]) ;<-- here to use it
(do-processing [:compress :debug-print]) ;or like this
(do-processing [:encrypt]) ;or like this
7
NiklasJ

[基本的に、私の答えは 上記の@Ixrecによる回答 の続きです。 ]

重要な質問:カバーする必要のある個別の組み合わせの数は増えるのでしょうか?サブジェクトドメインをよく理解している。これはあなたの判断です。
バリアントの数は増える可能性がありますか?まあ、それは考えられないことではありません。たとえば、より多くの異なる暗号化アルゴリズムに対応する必要がある場合があります。

個別の組み合わせの数が増えることが予想される場合は、 戦略パターン が役立ちます。アルゴリズムをカプセル化し、呼び出し元のコードに互換性のあるインターフェイスを提供するように設計されています。特定の文字列ごとに適切な戦略を作成(インスタンス化)する場合でも、少量のロジックがあります。

上記でコメントしました 要件の変更を期待していません。バリアントの数が増えると予想しない場合(またはこのリファクタリングを延期できる場合)は、ロジックをそのままにします。現在、ロジックは少量で管理可能です。 (おそらく、戦略パターンへの可能なリファクタリングについてのコメントに自分へのメモを入れます。)

5
Nick Alexeev

scalaでこれを行う1つの方法は次のようになります。

val handleCompression: AnyRef => AnyRef = data => if (compressEnable) compress(data) else data
val handleEncryption: AnyRef => AnyRef = data => if (encryptionEnable) encrypt(data) else data
val handleData = handleCompression andThen handleEncryption
handleData(data)

上記の目標(個々の処理ロジックの分離とそれらの相互接続方法)を達成するためにデコレーターパターンを使用するのは、冗長すぎます。

OOプログラミングパラダイムでこれらの設計目標を達成するために設計パターンが必要な場合、関数型言語は、ファーストクラスシチズン(コードの1行目と2行目)および関数型として関数を使用してネイティブサポートを提供します構成(3行目)

1
Sachin K