web-dev-qa-db-ja.com

スイッチ条件を削除するためのクリーンなコード(ポリモーフィズムを使用)

SOLIDの原則にあるように、スイッチ条件をクラスとインターフェースに変換して削除することをお勧めします。次のコードで実行したいと思います。

注:このコードは実際のコードではなく、自分の考えを入れただけです。

MessageModel message = getMessageFromAnAPI();
manageMessage(message);
...
void manageMessage(MessageModel message){        
    switch(message.typeId) {
        case 1: justSave(message); break;
        case 2: notifyAll(message); break;
        case 3: notify(message); break;
    }
}

次に、switchステートメントを削除します。だから私はそれのためにいくつかのクラスを作成し、ここでポリモーフィズムを実装しようとします:

interface Message{
    void manageMessage(MessageModel message);
}
class StorableMessage implements Message{

    @Override
    public void manageMessage(MessageModel message) {
        justSave(message);
    }
}
class PublicMessage implements Message{

    @Override
    public void manageMessage(MessageModel message) {
        notifyAll(message);
    }
}
class PrivateMessage implements Message{

    @Override
    public void manageMessage(MessageModel message) {
        notify(message);
    }
}

次に、APIを呼び出してMessageModelを取得します。

MessageModel message = getMessageFromAnAPI();

今私の問題はここにあります。モデルがあり、クラスを使用して管理したい。 SOLID例として、私はこのようなことをする必要があります:

PublicMessage message = new Message();
message.manageMessage(message);

しかし、どのタイプがこのメッセージに関連しているのかを知り、それからインスタンスを作成するにはどうすればよいですか(PublicMessageまたはStorableMessageまたはPrivateMessage)?!スイッチブロックをもう一度ここに配置する必要がありますか?

15
Siamak Ferdos

この場合、ファクトリを使用してMessageのインスタンスを取得できます。ファクトリはMessageのすべてのインスタンスを持ち、MessageModelのtypeIdに基づいて適切なインスタンスを返します。

class MessageFactory {
    private StorableMessage storableMessage;
    private PrivateMessage privateMessage;
    private PublicMessage publicMessage;
    //You can either create the above using new operator or inject it using some Dependency injection framework.

    public getMessage(MessageModel message) {
        switch(message.typeId) {
            case 1: return storableMessage; 
            case 2: return publicMessage;
            case 3: return privateMessage
            default: //Handle appropriately
        }
    }
}

呼び出しコードは次のようになります

MessageFactory messageFactory; //Injected 
...
MessageModel messageModel = getMessageFromAnAPI();

Message message = messageFactory.getMessage(messageModel);
message.manageMessage(messageModel);

ご覧のように、これはswitchを完全に取り除いたわけではありません(スイッチを使用すること自体は悪いことではないので、不要です)。 SOLIDが言おうとしていることは、SRP(単一の責任の原則)とOCP(オープンクローズの原則)に従って、コードをクリーンに保つことです。ここで意味するところは、コードに各typeIdを1か所で処理する実際の処理ロジック

ファクトリーを使用して、作成ロジックを別の場所に移動し、実際の処理ロジックをそれぞれのクラスにすでに移動しました。

編集:繰り返しますが、私の答えはSOLID OPの側面に焦点を当てています。個別のハンドラークラス(MessageのインスタンスをOP)SRPを達成します。ハンドラクラスの1つが変更された場合、または新しいメッセージtypeId(message.typeId)(つまり、新しいMessage実装を追加します)、オリジナルを変更する必要がないため、OCPを実現できます。 (これらのそれぞれに簡単なコードが含まれていないと仮定して)。これらはすでにOPで行われています。

ここで私の答えの本当のポイントは、ファクトリを使用してMessageを取得することです。アイデアは、メインアプリケーションコードをクリーンに保ち、スイッチ、if/else、および新しい演算子をインスタンス化コードに制限することです。 (@Configurationクラス/ GuiceでSpringまたはAbstractモジュールを使用するときにBeanをインスタンス化するクラスと同様)。 OO原則しないでくださいスイッチの使用は悪いと言います。それはに依存しますwhereあなたはそれを使用します。アプリケーションコードでそれを使用すると、SOLIDの原則に違反します。それが私が引き出したかったことです。

私はまた、daniu @のアイデアを機能的な方法で使用するのが好きです。同じ方法を上記のファクトリコードで使用することもできます(または単純なマップを使用してスイッチを取り除くこともできます)。

10
user7

あなたはこれを行うことができます:

static final Map<Integer,Consumer<MessageModel>> handlers = new HashMap<>();
static {
    handlers.put(1, m -> justSave(m));
    handlers.put(2, m -> notifyAll(m));
    handlers.put(3, m -> notify(m));
}

これにより、スイッチが削除されます

Consumer<Message> consumer = handlers.get(message.typeId);
if (consumer != null) { consumer.accept(message); }

統合操作の分離原理

もちろんこれをカプセル化する必要があります:

class MessageHandlingService implements Consumer<MessageModel> {
    static final Map<Integer,Consumer<MessageModel>> handlers = new HashMap<>();
    static {
        handlers.put(1, m -> justSave(m));
        handlers.put(2, m -> notifyAll(m));
        handlers.put(3, m -> notify(m));
    }
    public void accept(MessageModel message) {
        Consumer<Message> consumer = handlers.getOrDefault(message.typeId, 
                m -> throw new MessageNotSupportedException());
        consumer.accept(message);
    }
}

あなたのクライアントコードで

message = getMessageFromApi();
messageHandlingService.accept(message);

このサービスは「統合」部分です(「実装」とは対照的に、cfg統合操作の分離原則)。

CDIフレームワークを使用

CDIフレームワークを使用する本番環境の場合、これは次のようになります。

interface MessageHandler extends Consumer<MessageModel> {}
@Component
class MessageHandlingService implements MessageHandler {
    Map<Integer,MessageHandler> handlers = new ConcurrentHashMap<>();

    @Autowired
    private SavingService saveService;
    @Autowired
    private NotificationService notificationService;

    @PostConstruct
    public void init() {
        handlers.put(1, saveService::save);
        handlers.put(2, notificationService::notifyAll);
        handlers.put(3, notificationService::notify);
    }

    public void accept(MessageModel m) {  // as above }
}

実行時に動作を変更できます

@ user7の答えのスイッチに対するこれの利点の1つは、動作を調整できることです実行時。あなたは次のような方法を想像することができます

public MessageHandler setMessageHandler(Integer id, MessageHandler newHandler);

これは与えられたMessageHandlerをインストールして古いものを返します;これにより、たとえば、デコレータを追加できます。

これが役立つ例は、処理を提供する信頼できないWebサービスがある場合です。アクセス可能な場合は、handlerrとしてインストールできます。それ以外の場合は、デフォルトのハンドラーが使用されます。

16
daniu

ここでの重要な点は、インスタンス化と構成実行から分離することです。

OOPを使用しても、if/elseカスケードまたはswitchステートメントを使用して異なるケースを区別することは避けられません。結局、特殊な具象クラスのインスタンスを作成する必要があります。
しかし、これは初期化コードまたはある種のfactoryにあるはずです。

ビジネスロジック内で、_interfacesでジェネリックメソッドを呼び出して、if/elseカスケードまたはswitchステートメントを回避します。振る舞う。

10
Timothy Truckle

通常のクリーンなコードアプローチは、MessageModelにその動作を含めることです。

_interface Message {
    void manage();
}

abstract class MessageModel implements Message {
}

public class StoringMessage extends MessageModel {
    public void manage() {
        store();
    }
}
public class NotifyingMessage extends MessageModel {
    public void manage() {
        notify();
    }
}
_

次に、getMessageFromApiは適切なタイプを返し、スイッチは

_MessageModel model = getMessageFromApi();
model.manage();
_

このように、生成するメッセージを決定する必要があるので、getMessageFromApi()メソッドにスイッチがあります。

ただし、いずれにしてもメッセージタイプIDは満たされるため、問題ありません。また、クライアントコード(スイッチが現在存在する場所)はメッセージの変更に耐性があります。つまり、別のメッセージタイプの追加は正しく処理されます。

4
daniu

あなたが持っている本当の問題は、MessageModelが多態性ではないということです。 MessageModelsをポリモーフィックなMessageクラスに変換する必要がありますが、このクラスのメッセージをどう処理するかのロジックを配置しないでください。代わりに、メッセージの実際のコンテンツが含まれている必要があり、 Eric's Answer に示すようにビジターパターンを使用して、他のクラスがMessageを操作できるようにする必要があります。匿名を使用する必要はありませんVisitor; MessageActionVisitorのような実装クラスを作成できます。

MessageModelsをさまざまなMessagesに変換するには、 ser7の回答 に示すように、ファクトリを使用できます。返されるMessageのタイプを選択することに加えて、ファクトリはMessageを使用してMessageModelの各タイプのフィールドに入力する必要があります。

1
Vaelus

Factory Pattern を使用できます。次の値を持つ列挙型を追加します。

public enum MessageFacotry{
    STORING(StoringMessage.TYPE, StoringMessage.class),
    PUBLIC_MESSAGE(PublicMessage.TYPE, PublicMessage.class),
    PRIVATE_MESSAGE(PrivateMessage.TYPE, PrivateMessage.class);
    Class<? extends Message> clazz;
    int type;
    private MessageFactory(int type, Class<? extends Message> clazz){
        this.clazz = clazz;
        this.type = type;
    }

    public static Message getMessageByType(int type){

         for(MessageFactory mf : values()){
              if(mf.type == type){
                   return mf.clazz.newInstance();
              }
         }
         throw new ..
    }
}

次に、その列挙型の静的メソッドを呼び出して、管理するメッセージのインスタンスを作成できます。

0
Noixes