状态模式和封装

6
我最近正在开发一款Java应用程序,并尝试遵循GoF的状态模式来整理代码。
该程序使用多智能体系统的代理来代表“超级代理”执行指令(下面是示例)。
超级代理可以存在两种状态,到处都有if语句检查状态,然后执行特定于状态的行为,这使得代码变得混乱不堪。
以下是程序的(非常)简化版本。实际实现具有更多的状态特定行为。
public class superAgent
{
    //the state of the super agent
    private States state;

    //Contains information related to the operation of exampleClass. This should not be exposed through mutator methods.
    private HashMap<String, SpecificInstructionData> instructionData

    private LinkedBlockingQueue<ExampleInstruction> exampleQueue

    private final Object instructionLock = new instructionLock

    public enum States
    {
        STATE1,
        STATE2;
    }

    public void setState(state s)
    {
        state = s
    }

    //Called by a thread that continuously takes from the queue 
    private void runningThread()
    {
        while(isRunning)
        {
            synchronized(instructionLock)
            {
                ExampleInstruction ei = exampleQueue.take();
                //Add some data about the instruction into instructionData
                //send the instruction to an available agent
            }
        }
    }

    public void instructionResponseRecievedFromAgent()
    {
        if(state == States.STATE1)
        {
            doState1Behavior();
        }
        else if(state == States.STATE2)
        {
            doState2Behavior();
        }
    }

    private void doState1Behavior()
    {
        synchronized(instructionLock)
        {
            //make state specific modifications to instructionData
        }
    }

    private void doState2Behavior()
    {
        synchronized(instructionLock)
        {
            //make state specific modifications to instructionData
        }
    }
}

状态模式非常适合根据 GoF 设计模式将特定状态的行为封装到不同类中(超级代理类将作为上下文)。但是,有两个问题(我认为)破坏了封装性:
  1. 大多数状态特定行为需要更改超级代理的私有成员(在上面的示例中,instructionData)。这些成员包含数据,可能不应该可访问,也绝对不应该对包装类进行更改。

  2. 状态特定行为需要与非状态特定行为同步。如果不通过将其公开或使用 getter 暴露锁对象(在上面的示例中 instructionLock),则状态和上下文无法共享锁。暴露锁违反了 OOP,因为它可能被包装/扩展类使用。

请问有人对如何封装此特定状态行为有任何建议吗?请考虑示例和上述两点。

  1. 你不需要传递可变数据。状态方法可以返回超级代理可以处理的数据。我不知道这是否比将数据作为参数传递更有益。
  2. 共享锁并不违反我所知道的任何规定。
- Bart
如果锁要在上下文和状态之间共享,那么它的可访问级别将违反Oracle的安全编码标准。 - AJS49
1个回答

2
您可以使用状态实例和superAgent实例之间的Double Dispatch来解决这两个问题,以避免破坏封装性。假设您已经实现了状态模式,那么instructionResponseRecievedFromAgent将如下所示:
public void instructionResponseRecievedFromAgent() {
  state.instructionResponseRecievedFromAgent();
}

每个State都使用双重分派来实现instructionResponseRecievedFromAgent,具体如下:

abstract class State {
  abstract void instructionResponseRecievedFromAgent();
}

class State1 extends State {
  void instructionResponseRecievedFromAgent() {
    // instance variable
    agent.instructionResponseRecievedFromAgentFromState1();
  }
}

class State1 extends State {
  void instructionResponseRecievedFromAgent() {
    // instance variable
    agent.instructionResponseRecievedFromAgentFromState2();
  }
}

通过这样做,您可以让每个“State”指定要做的内容,但是由superAgent实例决定如何做。您可以完全访问状态和锁定,而无需将它们公开。
最后,在superAgent中实现instructionResponseRecievedFromAgentFromState1instructionResponseRecievedFromAgentFromState2
public void instructionResponseRecievedFromAgentFromState1() {
    //this is doState1Behavior original implementation
    synchronized(instructionLock)
    {
        //make state specific modifications to instructionData
    }
}

public void instructionResponseRecievedFromAgentFromState2() {
    //this is doState1Behavior original implementation
    synchronized(instructionLock)
    {
        //make state specific modifications to instructionData
    }
}

请记住,即使 instructionResponseRecievedFromAgentFromState1instructionResponseRecievedFromAgentFromState2 是公共方法(因此 State 实例可以调用它们),但它们仅意味着在状态模式的上下文中使用。


非常有趣的解决问题方式。这个解决方案肯定是解决问题的潜在方案。然而,这种方法并不完全适合我的情况,因为“如何做到”方法仍然暴露出来,而我所谈论的功能确实不应该被暴露出来。最终,我通过将超级代理的3个引用变量(包括锁)通过构造函数传递给状态类来解决了这个问题,从而允许我在状态内部进行改变而不会暴露它们。我仍然使用这种方法来处理一些特定于状态的行为。非常有用的建议。 - AJS49
嗨!请记住,暴露的方法告诉我们“做什么”,而“如何做”则与您实现它们的方式有关。关于你的方法:注意,通过暴露内部状态来允许对象被从外部改变并不是一个好主意。除了处理重复代码之外,例如,您无法确保不会破坏不变量。干杯! - nick2083
注意,通过暴露内部状态允许对象从外部进行变异并不是一个好主意。为什么呢?虽然所有对象都被声明为final(尽管由于Java的值引用而无关紧要),但我看不到任何不这样做的理由,除了对维护程序员来说不是完全明显之外。但是,如果有很好的文档说明,我认为这不会有问题。能否详细解释一下? - AJS49
当instructionLock上使用final关键字时,它意味着它不能被重新绑定,但是没有阻止外部对象持有对它的引用并执行synchronized(instructionLock){...},从而导致潜在的死锁。另一个例子:想象一下将用于保持内部状态的集合传递到外部:没有任何防止其他对象持有该引用并添加或删除元素,从而在您不知情的情况下更改您的私有状态。为什么要记录如果您做某些不完全正确的事情可能会发生什么,当您可以通过设计避免它呢?让我知道您的想法!干杯! - nick2083

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接