代码之家  ›  专栏  ›  技术社区  ›  Adrian McCarthy

如何根据对象的状态更改其接口?

  •  4
  • Adrian McCarthy  · 技术社区  · 15 年前

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

    举个具体的例子,想象一个 Printer 对象

    • 最初,对象的界面允许您查询打印机的功能、更改纸张方向等设置,以及启动打印作业。

    • 启动打印作业后,仍可以进行查询,但不能启动其他作业或更改某些打印机设置。你可以开始一页。

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

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

    打印机 对象具有大量方法。如果在不适当的时间调用方法(例如,尝试更改页面中间的纸张方向),调用将失败。也许,如果您在序列中提前跳过并开始发出图形调用 打印机 StartJob() StartPage() 方法。这种方法的主要缺点是对调用者来说不是很容易。接口可能是压倒性的,顺序要求不是很明显。

    另一个想法是将事物分解为单独的对象: , PrintJob ,及 Page . 这个 StartJob() 返回一个 打印作业 Abort() , 起始页() 返回一个 对象,该对象提供用于进行实际图形调用的接口。这里的缺点是机械性。如何在不放弃对对象生命周期的控制的情况下公开对象的接口?如果我给调用者一个指向 ,我不想让他们这么做 delete

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

    6 回复  |  直到 15 年前
        1
  •  1
  •   Steve Jessop    15 年前

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

    除了问题的其余部分,我想特别谈谈这一点。

    Page *Printer::newPage();
    

    我建议不要这样做,并且赞成这样的构造函数:

    Page::Page(Printer &);
    

    也就是说,不要在打印机中分配页面对象,将其返回给调用者,然后必须担心对象生命周期。相反,放弃对对象生命周期的控制 原则上 ,为用户提供灵活性。您希望用户开始一个页面,在其中绘制内容,然后完成该页面。所以,让他们完全这样做:创建一个页面对象,绘制东西,看看它是否有效,或者提供 flush cancel 功能,甚至可能 blockUntilDonePrinting getFailureCode

    如果您确实需要工厂:

    Page *PageFactory::newPage(Printer &);
    

    不管怎样,打印机 作为PageFactory有双重职责,但这里有两个独立的关注点:(1)管理对打印内容的硬件资源的访问,(2)管理用户在软件中创建可打印对象的工作流。打印机不需要同时执行这两项操作,所以可以将它们分开。

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

    我不想让他们删除它

    newPage ,但必须打电话 Printer::close(Page *) 以指针作为参数”。但是正如我在C++中所说的,不像C,你不必创建这样的API。

    我会尝试设计出这个限制(打印队列,任何人?),这样,尽管在任何时候只有一个页面在实际打印,但可以创建多个页面并同时与打印机驱动程序通信。但印刷只是一个例子。如果页面确实需要在整个页面生命周期内独占使用打印机(例如,如果我们讨论的是互斥和互斥会话,而不是打印机和页面),那么打印机应该有一个API(可能是公共的,可能是通过 friend

        2
  •  4
  •   Eric Petroelje    15 年前

    是的,它叫 state pattern .

    一般的想法是打印机对象包含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();
        }
    }
    
        3
  •  3
  •   djna    15 年前

    我会把你的东西分开。这些不需要允许客户做任何不愉快的事情。

    IPage Job.getPage()
    

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

    ---对评论的回应---

    第一个问题是:我们“持有”的物体在我们脚下会发生变化吗。我们的代理所指的页面已完成打印,这是使我们的代理无效还是悄悄地成为下一页的代理?

    有意义的设计很大程度上取决于问题领域的深层次概念,从我对打印机的个人知识来看,这可能没有帮助。

    相反,让我们尝试抽象设计原则。

    所以首先:针对不同状态的单独接口确实会使代码更容易编写。我们只是避免愚蠢,比如让毛虫飞,让蛹交配。然而,我们遇到了有多少子状态的问题。。。饥饿的毛虫不同于睡眠的毛虫不同于非常饥饿的毛虫吗?

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

    在我看来,页面示例似乎有一个额外的部分:状态更改可能由于内部事件而发生。因此,我们有了一个毛虫,突然它变成了一只蝴蝶。现在我认为我们进入了一个完全不同的模式。它更受事件驱动。因此,我认为我们面临着完全不同的设计挑战。

    Job.registerPageEventListener( me )
    

    还有我

    boolean pageStarted(IPage)
    

    也许我可以返回true来表示“打印它”,返回false来表示“保持它”,然后继续工作。

        4
  •  1
  •   xtofl Adam Rosenfield    15 年前

    除此之外,您还需要一个方便的界面: Printer -&燃气轮机; Page Job

    然后,主对象可以在进入不同状态时使所有以前的代理失效。通过这种方式,可以将对象的生存期与其有效性分离。删除代理当然也会将其从主对象的代理列表中删除。

        5
  •  0
  •   RED SOFT ADAIR    15 年前

    从协议的角度来看,这两种可能性是相似的:对于这两种实现,某些调用在特定时间将无效。第一种情况是调用函数,第二种情况是调用获取链接对象。

        6
  •  0
  •   troelskn    15 年前

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

    Page#delete 您的页面对象是一个内部组件。在这种情况下,你不应该直接暴露它。相反,创建一个新类来表示状态,并使用您想要公开的任何方法。是的,最终将得到许多细粒度类,而不是几个(或单个)大类。那是一个 (tm)。