【问题标题】:Using strategy design pattern with an abstract parameter使用带有抽象参数的策略设计模式
【发布时间】:2023-10-22 09:28:02
【问题描述】:

我目前正在做一个非常简单的项目来提高我的 SOLID 和设计模式知识。 这个想法是为一扇门创建一个“智能锁”,可以通过指纹、面部识别等不同参数来识别一个人。

我立即看到了使用策略设计模式的潜力,因此我创建了一个Lock接口和一个Key抽象类:

public interface Lock {
    boolean unlock(Key key);
}

public abstract class Key {
    private String id;

    public String getId(){
        return (this.id);
    }
}

我创建了两个类来扩展 Key - FacePhotoFingerPrint

public class FacePhoto extends Key {
}

public class FingerPrint extends Key {
}

然后我创建了实现 Lock 的类,例如 FingerPrintRecognizerFacialRecognizer

public class FacialRecognizer implements Lock {
    @Override
    public boolean unlock(Key key) throws Exception {
        if(key instanceof FacePhoto){
            //check validity
            return true;
        }
        else {
            throw new Exception("This key does not fit this lock");
        }
    }
}

public class FingerPrintRecognizer implements Lock {
    @Override
    public boolean unlock(Key key) throws Exception {
        if(key instanceof FingerPrint){
            //check validity
            return true;
        }
        else {
            throw new Exception("This key does not fit this lock");
        }
    }
}

我真的找不到更好的方法来处理 Lock 界面的用户会尝试使用不合适的钥匙打开锁的情况。 另外,我在使用“instanceof”if 语句时遇到了问题,因为它出现在每个实现 Lock 的类中。

在这种情况下,策略是一种好习惯吗?如果不是,那将是一个不错的选择(可能是不同的设计模式)。

【问题讨论】:

  • 您不应该使用instanceof 来区分键,您的Key 接口应该对任何客户端都具有必要的抽象,并且实现它的每个类都有自己的方法版本。对于您问题的另一部分,还有其他选择。您可以将 Key 作为依赖项传递给 Lock,并使用工厂创建与 Key 匹配的 Lock。或者可能切换到命令模式,其中每个实现都包含匹配的密钥和解锁逻辑。可能还有更多解决方案
  • 感谢您的回答。当您说:“您的 Key 接口应该具有任何客户端所需的抽象,并且实现它的每个类都将具有自己的方法版本”您所说的“必要的抽象”是什么意思?这将如何帮助识别密钥?此外,当您说:“您可以将 Key 作为依赖项传递给 Locks,并使用工厂来创建与 Key 匹配的 Lock。”如果您假设每个锁都有一把钥匙,那么也许这是一个好主意。但是一把锁可以有很多钥匙来打开它(很多不同的指纹)。
  • 1) Lock 需要什么来验证 Key?这是所有关键实现共享的东西吗?您的 Key 抽象仅包含一个 id,但这显然不足以进行有效性检查,因为您需要验证 Lock 上 Key 的具体类。 2)如果您需要一个钥匙锁,您将有一些工厂需要一个钥匙,或者如果锁消耗多个钥匙,另一个工厂需要一个钥匙列表。但它们都将是关键实现

标签: java oop design-patterns solid-principles strategy-pattern


【解决方案1】:

策略模式提供了在运行时改变行为的能力。在您的情况下,Lock 的特定具体实现可以与 key 的特定实现一起使用,因此逻辑不允许行为更改,因此该模式不适合当前实现。

策略模式示例。

 class A{
    private Behavior b; //behavior which is free to change
    public void modifyBehavior(Behavior b){
         this.b = b;
    }
    public  void behave(){
          b.behave(); // there is no constraint of a specific implementation but any implementation of Behavior is allowed.
     }
 }

 class BX implements Behavior {
     public void behave(){
           //BX behavior
     }
 }

 class BY implements Behavior {
     public void behave(){
           //BY behavior
     }
 }

interface Behavior {
      void behave();
}

在您的情况下,您需要重构抽象以更好地适应逻辑。

作为重构(不使用当前情况的策略模式作为强制使用设计模式是一种不好的做法,目前违反 SOLID 原则的 L )您可以考虑你的问题的另一个答案。 https://*.com/a/49763677/504133

【讨论】:

  • 谢谢!您建议如何重构抽象?
  • 接受抽象的类必须允许任何实现,在您的情况下,如 FacialRecognizer 接受抽象,因此抽象应该是允许 FacialRecognizer 的所有实现,因此您可能有抽象 FacePhoto 并且可以有多种实现。虽然这取决于您的用例,如果只有不同的对象但 FacePhoto 的单一实现,那么您可以考虑不使用策略模式,因为当前案例不需要在运行时更改行为。
  • 强制一个设计模式是不好的。当情况需要一个时,只有我们应该应用它们。通常遵循以下原则导致设计模式之一。在您的实施中,您违反了 L in SOLID
【解决方案2】:

一般来说,当两个抽象之间的关系是一对多时,策略模式是好的。例如,如果您有一把锁和许多可用于打开锁的钥匙。例如,以下是策略模式的一个很好的例子:

public class Lock {

     public void unlock(Key key) {
         // Unlock lock if possible
     }
}

public interface Key {
    public int someState();
}

public class FooKey implements Key {

    @Override
    public int someState() { ... }
}

public class BarKey implements Key {

    @Override
    public int someState() { ... }
}

你的问题是一个多对多的问题,有许多锁可以用多把钥匙打开,其中一些钥匙可以用来打开一些锁,而不能打开其他锁。对于这类问题,Visitor Pattern 是一个不错的选择,其中算法是解锁过程,对象是锁。这种方法的好处是成功或失败锁定(特定密钥是否解锁特定锁定)包含在简单的方法中,而无需使用instanceof

一般来说,使用instanceof 表示需要某种形式的多态性(即,不是测试每个提供的对象以查看它是否是特定类型并基于该类型执行逻辑,而是该类型应该具有多态方法,其行为因对象类型而异)。这个问题太常见了,有一个标准的重构来代替它:Replace Conditional with Polymorphism

要根据您的目的实施访问者模式,您可以尝试以下类似的方法:

public class UnlockFailedException extends Exception {

    public UnlockFailedException(Lock lock, Key key) {
        this("Key " + key.getClass().getSimpleName() + " failed to unlock lock " + lock.getClass().getSimpleName());
    }

    public UnlockFailedException(String message) {
        super(message);
    }
}

public interface  Lock {
    public void unlock(Key key);
}

public interface Key {
    public void unlock(FacialRecognizer lock) throws UnlockFailedException;
    public void unlock(FingerPrintRecognizer lock) throws UnlockFailedException;
}

public class FacialRecognizer implements Lock {

    @Override
    public void unlock(Key key) {
        key.unlock(this);
    }
}

public class FingerPrintRecognizer implements Lock {

    @Override
    public void unlock(Key key) {
        key.unlock(this);
    }
}

public class FacePhoto extends Key {

    @Override
    public void unlock(FacialRecognizer lock) throws UnlockFailedException {
        // Unlock the lock
    }

    @Override
    public void unlock(FingerPrintRecognizer lock) throws UnlockFailedException {
        throw new UnlockFailedException(lock, this);
    }
}

public class FingerPrint extends Key {

    @Override
    public void unlock(FacialRecognizer lock) throws UnlockFailedException {
        throw new UnlockFailedException(lock, this);
    }

    @Override
    public void unlock(FingerPrintRecognizer lock) throws UnlockFailedException {
        // Unlock the lock
    }
}

将每个Lockunlock 逻辑分组到一个抽象类中可能很诱人(因为每个Lock 实现都相同),但这会破坏模式。通过将this 传递给提供的Key,编译器知道要调用哪个重载方法。这个过程称为double-dispatch。虽然看起来很乏味,但调用的逻辑很简单(一行),因此虽然有重复,但并不严重。

这种方法的缺点是Key 接口必须为Lock 的每个实现 提供一个unlock 方法。如果缺少一个,编译器会在实现Lock 时抱怨,因为它的unlock 方法将在Key 上调用unlock,它不包含接受新的Lock 实现的方法。从这个意义上说,编译器起到了检查的作用,确保Key 实现可以处理(解锁或解锁失败)每个Lock 实现。

您还可以实现一个包含许多Key 对象的KeyRing,可以使用每个Key 对象解锁Lock,直到找到一个打开Lock 的对象。如果KeyRing上没有Key可以打开Lock,一个UnlockFailedException

public class KeyRing {

    public final List<Key> keys = new ArrayList<>();

    public void addKey(Key key) {
        keys.add(key);
    }

    public void removeKey(Key key) {
        keys.remove(key);
    }

    public void unlock(Lock lock) throws UnlockFailedException {

        for (Key key: keys) {
            boolean unlockSucceeded = unlockWithKey(lock, key);
            if (unlockSucceeded) return;
        }

        throw new UnlockFailedException("Could not open lock " + lock.getClass().getSimpleName() + " with key ring");
    }

    private boolean unlockWithKey(Lock lock, Key key) {
        try {
            lock.unlock(key);
            return true;
        }
        catch (UnlockFailedException e) {
            return false;
        }
    }
}

如果UnlockFailedException 过于突兀,可以将Keyunlock 方法更改为返回一个boolean,表示解锁过程是否成功。例如:

public interface Key {
    public boolean unlock(FacialRecognizer lock);
    public boolean unlock(FingerPrintRecognizer lock);
}

public class FacePhoto extends Key {

    @Override
    public boolean unlock(FacialRecognizer lock) {
        // Unlock the lock
        return true;
    }

    @Override
    public boolean unlock(FingerPrintRecognizer lock) {
        return false;
    }
}

public class FingerPrint extends Key {

    @Override
    public void unlock(FacialRecognizer lock) {
        return false;
    }

    @Override
    public void unlock(FingerPrintRecognizer lock) {
        // Unlock the lock
        return true;
    }
}

使用boolean返回值也简化了KeyRing的实现:

public class KeyRing {

    public final List<Key> keys = new ArrayList<>();

    public void addKey(Key key) {
        keys.add(key);
    }

    public void removeKey(Key key) {
        keys.remove(key);
    }

    public boolean unlock(Lock lock) throws UnlockFailedException {

        for (Key key: keys) {
            boolean unlockSucceeded = lock.unlock(key);
            if (unlockSucceeded) return true;
        }

        return false;
    }
}

【讨论】:

    【解决方案3】:

    可以使用特定类型的Key 打开Lock

    interface Lock<K extends Key> {
        void unlockUsing(K key);
    }
    
    interface Key {
        // TODO
    }
    

    一个Door 由多个Lock 对象组成。每个Lock 可能需要不同类型的密钥。但是你想保持界面“单项”。

    class Door {
        private Lock<FacePhoto> faceLock;
        private Lock<FingerPrint> printLock;
    
        public void unlockUsing(Key key) {
            // which lock to use?
        }
    }
    

    我们需要某种方式将密钥分配给正确的锁。如果Key 使用FacePhoto,我们希望使用faceLock

    目前,Key 是唯一知道/决定应该使用哪个锁的人。 为什么不允许Key 决定使用哪个锁?

    首先,为了让钥匙决定使用哪个锁,我们需要以某种方式将这些锁传递给钥匙。我们可以将不同的锁隐藏在门面后面并将其传递给Key

    class Door {
        private LockSet locks;
    
        public void unlockUsing(Key key) {
            key.unlock(locks); // the key will decide!
        }
    }
    
    interface Key {
        void unlock(LockSet locks);
    }
    
    class LockSet {
        private Lock<FacePhoto> faceLock;
        private Lock<FingerPrint> printLock;
    
        public void unlockUsing(FacePhoto photo) {
            faceLock.unlockUsing(photo);
        }
    
        public void unlockUsing(FingerPrint print) {
            printLock.unlockUsing(print);
        }
    }
    

    现在来实现你的密钥:

    class FacePhoto implements Key {
        public void unlock(LockSet locks) {
            locks.unlockUsing(this);
        }
    
        public boolean matches(FacePhoto photo) {
            boolean matches = false;
            // TODO: check if match
            return matches;
        }
    }
    
    class FingerPrint implements Key {
        public void unlock(LockSet locks) {
            locks.unlockUsing(this);
        }
    
        public boolean matches(FingerPrint print) {
            // TODO: check if match
        }
    }
    

    您不能使用错误的钥匙和错误的锁。所有潜在的锁都通过LockSet 指定。由于LockSet 公开了一个类型安全的接口,因此您不能尝试用FingerPrint 打开Lock&lt;FacePhoto&gt;,编译器不会让您这样做(这是一件好事——在运行前捕获不匹配错误)。您也不能尝试使用不受支持的密钥。

    这种设计称为visitor pattern。如果有什么你不同意的地方,或者需要进一步的解释,请告诉我。

    【讨论】:

    • 访问者模式的使用令人印象深刻,但现在 LockSet 需要了解 Key 和 Lock 的所有实现,但无论如何,这是一个权衡取舍,应该可以接受。只有当您能帮助提及 OP 对当前情况下使用策略模式的担忧时。加上一个用于访问者模式的用法,我在我的答案中包含了指向您答案的链接。
    最近更新 更多