如何根据状态更改对象的界面?

时间:2009-09-25 13:09:05

标签: c++ oop

给定一个具有大量状态的相当复杂的对象,是否存在根据该状态公开不同功能的模式?

有关具体示例,请设想一个Printer对象。

  • 最初,对象的界面可让您查询打印机的功能,更改纸张方向等设置,然后开始打印作业。

  • 启动打印作业后,您仍然可以查询,但无法启动其他作业或更改某些打印机设置。你可以开始一个页面。

  • 启动页面后,您可以发出实际的文本和图形命令。你可以“完成”页面。您不能同时打开两个页面。

  • 某些打印机设置只能在页面之间进行更改。

一个想法是让一个Printer对象具有大量方法。如果您在不适当的时间呼叫方法(例如,尝试更改页面中间的纸张方向),则呼叫将失败。也许,如果您在序列中跳过并开始发出图形调用,Printer对象可能会根据需要隐式调用StartJob()StartPage()方法。这种方法的主要缺点是对呼叫者来说不是很容易。界面可能非常庞大,序列要求也不是很明显。

另一个想法是将内容分解为单独的对象:PrinterPrintJobPagePrinter对象公开查询方法和StartJob()方法。 StartJob()会返回PrintJobAbort()对象,其中包含StartPage()StartPage()以及仅更改可更改设置的方法。 Page返回一个Page对象,该对象提供用于进行实际图形调用的界面。这里的缺点是机械之一。如何在不放弃对该对象生命周期的控制的情况下公开对象的接口?如果我给调用者一个指向delete的指针,我不希望他们{{1}}它,并且在他们返回第一个之前我不能给他们另一个。

不要太挂在打印示例上。我正在寻找如何基于对象的状态呈现不同接口的一般问题。

6 个答案:

答案 0 :(得分:4)

是的,它被称为state pattern

一般的想法是您的Printer对象包含PrinterState对象。 Printer对象上的所有(或大多数)方法只是委托给包含的PrinterState。然后,您将拥有多个PrinterState类,以不同的方式实现这些方法,具体取决于在该状态下允许/不允许的内容。 PrinterState实现还将提供一个“钩子”,允许他们将Printer对象的当前状态更改为另一个状态。

这是一个有几个州的例子。它似乎很复杂,但是如果你有特定于状态的复杂行为,它实际上使代码和维护变得更加容易:

public abstract class PrinterState {
    private PrinterStateContext stateContext;

    public PrinterState( PrinterStateContext context ) {
        stateContext = context;
    }

    void StartJob() {;}
}

public class PrinterStateContext {
     public PrinterState currentState;
}


public class PrinterReadyState : PrinterState {

    public PrinterReadyState( PrinterStateContext context ) {
        super(context);
    }

    void StartJob() {
        // Do whatever you do to start a job..

        // Switch to "printing" state.
        stateContext.currentState = new PrinterPrintingState(stateContext);
    }
}

public class PrinterPrintingState : PrinterState {

    public PrinterPrintingState( PrinterStateContext context ) {
         super(context);
    }

    void StartJob() {
        // Already printing, can't start a new job.
        throw new Exception("Can't start new job, already printing");
    }
}


public class Printer : IPrinter {
    private PrinterStateContext stateContext;

    public Printer() {
        stateContext = new PrinterStateContext();
        stateContext.currentState = new PrinterReadyState(stateContext);
    }

    public void StartJob() {
        stateContext.currentState.StartJob();
    }
}

答案 1 :(得分:3)

我会选择你的单独物品。这些不需要让客户做任何不幸的事情。

IPage Job.getPage()

IPage界面只显示您需要的内容。它不是“真正的”Page对象,更像是Page的代理。删除(或超出范围)代理需要对真实事物没有影响。

---延伸以回应评论---

第一个问题是:我们“持有”的对象能否在我们的脚下发生变化。我们的代理所指的页面已完成打印,是否会使我们的代理无效或者它是否会悄然成为下一页的代理?

有意义的设计很大程度上取决于问题领域的深层次,我个人对打印机的了解可能没什么帮助。

相反,我们试着抽象出设计原则。

首先:针对不同状态的单独接口确实使代码更容易编写。我们只是避免愚蠢,比如要求蚂蚱飞行和蛹交配。然而,我们遇到的问题是,有多少子基地......饥饿的蚂蚱与不同于非常饥饿的毛毛虫的睡眠蚂蚁有什么不同?

因此,我们可能仍然会得到一些“不能走路,我正在睡觉”的例外情况。

所以接下来的想法,不要设计一个界面,上面写着“在你打电话给bethod A之前,你必须调用方法B”,即。有状态的界面。相反,界面只允许您调用A,并返回一个暴露B的新界面。

在蝴蝶例子中,这种模式很有效。

在我看来,Page示例还有一个额外的部分:状态变化可能由于内部事件而发生。因此,我们有一个蚂蚱,突然它是一只蝴蝶。现在我认为我们正处于一个完全不同的范例。它更受事件驱动。所以我认为我们有一套完全不同的设计挑战。

Job.registerPageEventListener( me )

我实施

boolean pageStarted(IPage)

也许我可以回复说“打印”而错误地说“抓住它”,然后继续努力。

答案 2 :(得分:1)

基本上,您似乎需要State模式:不同的行为由不同的实现建模。

最重要的是,您需要一个方便的界面:Printer - > Page - > Job分离是一个相当好的分离,清楚地显示了行动的顺序。您可以通过使它们成为主对象的代理来模拟它们可以失效的事实。

然后,主要对象在进入不同状态时可以使所有先前的代理无效。这样您就可以将对象的生命周期与其有效性分离。删除代理当然也会将其从主对象的代理列表中删除。

答案 3 :(得分:1)

如何在不放弃对该对象生命周期的控制的情况下公开对象的界面?如果我给调用者一个指向Page

的指针

除了问题的其余部分之外,只是为了解决这一点。

您似乎在谈论的API看起来有点像这样:

Page *Printer::newPage();

我建议不要这样做,并赞成一个看起来像这样的构造函数:

Page::Page(Printer &);

也就是说,不要在打印机中分配Page对象,将其返回给调用者,然后再担心对象生命周期。相反,放弃对对象的生命周期的控制作为原则,为您的用户提供灵活性。您希望用户启动页面,向其中绘制内容,然后完成页面。所以让他们做到这一点:创建一个Page对象,绘制东西,看它是否有效,可能提供flushcancel函数,甚至可能blockUntilDonePrintinggetFailureCode和等等。然后,当他们完成页面时,他们就会销毁它(或者,更可能的是,他们只是让它超出范围),然后他们就可以创建另一个。

如果你确实需要工厂:

Page *PageFactory::newPage(Printer &);

无论哪种方式,让Page本身知道如何处理打印机以便打印东西。打印机与Pages的工厂不同。嗯,实际上,它实际上是在现实世界中,但这并不意味着它也应该在软件中,因为我们的Page对象实际上不是一个物理页面,而是绘制页面的过程。如果我们的Page对象只是表示一个页面,那么根本不需要与打印机对象进行交互 - 我们可以构建我们的Pages,将它们序列化为postscript,然后担心打印机如何打印它们。

无论如何,打印机可以作为PageFactory提供双重任务,但这里有两个不同的问题:(1)管理对打印东西的硬件资源的访问,以及(2)管理用户在软件中创建可打印对象的工作流程。打印机不需要同时执行这两项操作,因此您可以将它们分开。

同样,对于具有状态的任何对象 - 将对象本身(具有两个或更多个状态)与进入这些状态的会话或工作流分开。

我不希望他们删除它

API完全可以说“用户不得删除newPage返回的指针的引用,而必须以指针作为参数调用Printer::close(Page *)”。但正如我在C ++中所说的,与C不同,您不必像这样创建API。

在他们返回第一个之前我不能给他们另一个。

我会尝试设计这个限制(打印队列,任何人?),因此虽然任何时候实际只打印一个页面,但可以创建多个页面并同时与打印机驱动程序进行通信。但印刷只是一个例子。如果一个页面确实需要在整个页面的生命周期中独占使用打印机(例如,如果我们讨论Mutex和MutexSession而不是打印机和页面就是这种情况),那么打印机应该有一个API(可能是公共的,也许是公共的根据Page实现是否对Printer实现而言是唯一的,可以通过friend访问。 Page使用它来获取独占访问权限(称之为“打印机令牌”)。如果您尝试使用同一个打印机存在另一个页面时创建一个页面,那么该页面将失败(或阻止,或适用于问题域的任何内容)。

答案 4 :(得分:0)

从协议视图来看,两种可能性都是相似的:对于这两种实现,某些调用在某些时候将无效。在第一种情况下,它调用一个函数,在第二种情况下调用它来获取一个链接对象。

从建筑的角度来看,尽管它总是更好,但要把大班分成小而独立的。这将是更好的可维护性等。所以我也建议这种方法。

答案 5 :(得分:0)

  

另一个想法是将内容分解为单独的对象:Printer,PrintJob和Page。 Printer对象公开查询方法和StartJob()方法。 StartJob()返回一个PrintJob对象,该对象具有Abort(),StartPage()和仅更改可更改设置的方法。 StartPage()返回一个Page对象,该对象提供用于进行实际图形调用的界面。这里的缺点是机械之一。如何在不放弃对该对象生命周期的控制的情况下公开对象的接口?如果我给调用者一个指向页面的指针,我不希望他们删除它,在他们返回第一个页面之前我不能给他们另一个。

此。

将对象拆分为较小的对象,对于相关的每个状态位。 Page#delete的问题在于您的Page对象是内部组件。在这种情况下,您不应直接公开它。而是使用您想要公开的任何方法创建一个新类来表示状态。是的,你最终会得到许多细粒度的课程,而不是一些(或单一的)大课程。这是好事(tm)。