From: AAAI Technical Report WS-98-03. Compilation copyright © 1998, AAAI (www.aaai.org). All rights reserved. Design Patterns Qiang Yang and for Planning Systems Philip W. L. Fong and School of Computing Science Simon Fraser University Burnaby, BC Canada V5A 1S6 qyang@cs.sfu.ca Abstract Edward Kim want to demonstrate how design patterns can be employed to structure a search-based planner design, so that the complexity of constructing AI software can be managed. Second, we want to discuss the role played by a design pattern catalog during our process of designing and evolving the framework. Wewant to report howthe design pattern catalog allowed us to flexibly configure planning systems for different applications. Our work follows closely the current trend in AI Planning in making the systems more modular (BVB96; BHB97). Wewill first introduce the concept of design patterns. Wethen discuss planning as search (section), and the need to reuse past planning frameworks (section ). Wewill showseveral design patterns useful for modular planner design applied to our planner framework. Wereport the issues involved in each decision, the solution we adopted, the alternatives we considered, and in what manner the design pattern catalog helped us in coming up with the solution. Wesummarize our experience in section . In this work, we are interested in building a software engineering discipline for planning system design. Our objective is to enable planning systems to becomemore configurable and modular, with the help of object libraries capturing well designed experiences and a commonplanner-design pattern catalog. It is hoped that the planning systems thus constructed will be more reusable and modifiable.It is also hopedthat this effort will contribute to the movementtowards industrially applicable planning systems, to supply this dynamic subfield of AI with a rigorous software engineeringdiscipline than just smart algorithms for plan generation. To this end, we focus on an object oriented design methodologyto planning. Wefocus on a collection of design patterns for modularizing different searchrelated parts of a typical planning system. Weillustrate our concept using the C++language, although our experienceapply equally well to all object oriented languages. This work is represents our continuing research in knowledgeacquisition and maintenanceeffort in planning systems design. Design Patterns Design pattern is a recent movementin software engineering. The basic concept is to reuse design knowledge. Design pattern is a genre by which standard, experttested solution to canonical design problems is rigorously is documented. The term design pattern could also refer to the solution itself. Such design patterns usually solve a design problem by imposing new organization in the software, and layout a particular protocol in which individual computational entities should interact. By conforming to such restriction, the target program would become more maintainable. For example, iterators "provide a way to access the element of an aggregate object sequentially without exposing its underlying representation." Iterators can be found in nearly all object-oriented container class library. It defines a protocol by which client subprograms can access the internal of an aggregate object. Design patterns can be shown informally as boxes with connections between them. A popular notation is an class/object diagram known as the OMTdiagram (Object Modeling Technique). Figure 1 (a) shows Introduction This paper summarizes our experience in applying design patterns to develop intelligent planning systems based on heuristic search. Over the past few years, we have built a numberof artificial intelligence (AI) planners, schedulers, and other kinds of problem solvers in languages including CommonLisp, C, and C++. As experience cumulates, we intended to design an object-oriented frameworkfor building intelligent problem solvers. Our hope is that the frameworkallows various reusable AI techniques to be mixed and matched in a highly flexible manner. The effort resulted in a C++ framework called Plan++. Our recent implementation of AI planners, as well as our current focus in text-based planning systems, are all done on top of this framework. In this paper, we report howthe design pattern catalog (GHJV94)has shaped the way we design our planning system’s framework. As a starting point, we will concentrate on search aspects of planning algorithms, leaving the rest of the planning frameworkas a part of our future work. Our interest here is twofold. First, we 104 if it finds one of the goal nodes. A typical search routine looks like the following: ConcreteClass Name Operation0 TypeOperation20 AbstractClaasNarne Abstract Operationl0 TypeAbstraetOperation20 plan(init-plan, operators, goal-condition): mark init-plan as "generated"; while (not all generated nodes are expanded) do select a node n that is generated but not expanded; if (n satisfies the goM-condition) then return generate all or some of the successor nodes n~ of n; mark all nI as "generated"; mark n as "expanded"; end while; return "failure"; end Search. instanceVariablel TypeinstanceVariable2 (a) (part-of relationship) (a solid circle meansmorethan one) ~ (creationrelationship) (class Inheritance) (acquaintance relationship,that is, LineShape keepsa referenceto Color) Intuitively, a plan node is "generated" the first time it is visited. A node is "expanded"if all its neighboring nodes are "generated". The efficiency of a search is partly determined by the order in which the state space is traversed, which, in turn, is determined by the order in which nodes are selected for expansion, the definition of the operator, and the representation of the states. The efficiency is also defined by howand in which order a successor node is generated; in partial order planning, a node is generated by resolving threats and achieving open-preconditions. In HTNplanning, a node can also be generated by reducing a non-primitive task by a task network. In case based planning a variety of repair operations can be used to generate successor nodes. Throughout the paper, we will use a sample AI planning domainto illustrate the effect of applying the design patterns. An instance of the water-jug planning problem is often described as follows: "You have two jugs { A, B}, jug A holds m litres (L) of water, jug B holds n L of water. You are allowed to do three things with the jugs: fill up a jug, empty a jug, or pour the content of a jug to another (until one of them is either full or empty). Find a sequence of steps which will result in a jug holding kL of water." For example, in a sequel of the movie Die Hard, the protagonists are given two jugs of capacity 3L and 5L, and they are asked to measure out exactly 1L from the two jugs. Here a state can be represented by a vector (p, q), where p and q are the amount of water in jug and B respectively. The operators are fillJug0, emptyJug0, and pourContentsOfJugIntoOtherJug0; thus, the neighbourhood of a node in the state-space is generated by the execution of all possible operators from this state; then the goal states are all (p, q) so that one ofporqis 1. (b) Figure 1: Introduction to OMTdiagram OMTnotation for abstract and concrete classes. A class is denoted by a box in the figure. The key operations of the class appear below the class name. Any instance variables appear below the operations. Figure 1 (b) shows various relationships between classes. The OMTnotation for class inheritance is a triangle connecting a subclass (LineShape) to its parent class (Shape). The rest of the notations are explained in the figure. In short, OMTis a convenient way of communicating good design experiences in object oriented design, showing relationships between classes and objects. A collection of well-used design patterns forms a design pattern catalog. Such catalogs are widely available on the Internet and in various book formats. Workin design pattern mining from large-scale software code is also starting to generate important results. Planning as Search A significant number of AI and combinatorial optimization problems are instances of heuristic state-space search (Pea84). A state space is a set of states plus a set of operators. Planning is no different. In planning one can differentiate between state-based search (BK95) and plan-space search (Wel94; Yan97). In both frameworks, one define a search engine and a processing module for generating a set of successor nodes from a given current node. Both styles of planning are instances of a state-space search. In a state-space search framework, an operator is a transformation that generates one or more states given the input of a state. A state space, therefore, implicitly defines a graph in which nodes are states, and edges are operator applications. A state space search is basically a graph searching problem such that nodes in the underlying graphs are generated incrementally as the search proceeds. The goal of a search is a set of states with some interesting properties. A search is successful Reusability in Planner Design Reuse is a unique challenge in search-based applications because of several reasons: 1. Frequent shift of representation. The efficiency and quality of search depends largely on the representation of state space. Designers building search application must experiment with multiple representation 105 ¯ Different domain have different ways of specifying and verifying a goal. As we move from one domain to another, or as the representation of states changes, a goal will be represented differently, and the algorithm used for checking a goal will be different. ¯ Different domains have different state space topologies. In particular, the representation of operators determines how the successors are generated. In fact, even if we stay in one domain, and fix the representation of states, both the goal verification and successor generation mechanismsmaystill vary: ¯ As the application mature, one might want to search for goals that previous goal specification methodfails to represent. ¯ One might discover that the reformulation of the search problem by altering the topology of the search space might speed-up the search procedure. This can be achieved by providing multiple successor generation strategies and allowing users of the application to configure the system with one of the alternative strategies. A real-life example is the SNLPplanner (MR91)and its variants with various threat removal strategies (PS93). All SNLP-basedplanners search in the same plan space. The implementation of states and goal verification mechanismare therefore fixed. However, the successor generation mechanismis different for different threat-removal strategies. To summarize, we are dealing with the following issues: ¯ The generic search procedure is more or less standard. Its behavior varies when we have a different mechanismfor verifying goals and generating successors. Wewant to provide a way for the users of our framework to configure the search engine with the appropriate behavior. before an appropriate one is found. A reusable search engine should anticipate such frequent shift of representation. 2. High demandfor flexible composition. Almost no single AI technique can be claimed as being omnipotent. Most of the techniques are heuristic in the sense that it works well in some domain, but not in other domains. Most of the time, more than one search technique has to be combined to yield satisfactory performance. Reuse, therefore, means not so much as the direct recycling of a predetermined set of search techniques, but instead the ability for future users to pick and choose various techniques that fits their domain, and to compose these techniques together in a flexible manner. 3. Obscurity of module boundary. AI techniques are very difficult to modularized. There are implicit interaction and hidden dependency among various components of the system. It takes a very in depth understanding of these techniques in order for the system architect to cleanly isolate the components. The goal of developing the Plan++ framework is to provide an architecture in which individual search techniques can be mixed and matched in a flexible manner, so that search technologies can be readily applied to a wide range of application domains. Encapsulating the Variation of State Representation Variations in State Representation The core search facilities is provided by a search engine object. Users construct a search engine object by supplying their problem description, and subsequent invocation of a memberfunction will return solution search path to the users. template<class State> class Search{ public: ¯ Weanticipate that both the goal verification and successor generation mechanismswill evolve as the users attempt to build increasingly sophisticated problem solvers. Wewant to provide an architecture that supports the enhancement and adaptation of the search engine. ¯ The goal verification and successor generation mechanisms access the state objects, which the search engine should assume zero knowledge if flexibility is to be achieved. Wewant to completely insulate the search engine from any access of the state objects. ¯ .. ¯ Search(constState~ start_state,....) SearchPath<State> findSolution() // Get solution.Returnsearch path. }; Now, within the :findSolution() member function implement the generic search algorithm in section . It is clear that the implementation of findSolutton() needs a way to test if a state object satisfy the goal (goal verification mechanism), and a way to generate the neighbouring states (successor generation mechanism). Wewant to give the users of Plan++ the freedomto represent their states in a waythat best fits their application domain. As such, the search engine should assume minimal knowledge about the implementation of the state objects. However,both the goal verification and the successor generation mechanisms are dependent on the search domain and its representation: To resolve the above issues, we adopted a design that we later recognized to be an incarnation of the Strategy pattern. Goal Verifier The Strategy pattern is applied to encapsulate the goal verification mechanism.Wedefine a goal verifier class Goal<State>, which is a parameterized abstract base class. template<classState> 105 ~ata> bool satisfied(State) I findSolution0; A DomainSpecifieGoal I boosat sfied(State) O- 4 ~2 Iexpand(State) A Search<State> sucg~so~3cnem~r_ findSolutlonO; (3- [- - 1 IsucccssorOcncrater_->cxpand(statc) expand(State) DomainSpecificSuccc,ssor I 1[¢xpsnd(State) [ DomatnSpeclficSuccess~r2 I bool satisfied(State) Figure 3: Successor Figure 2: Goal class JugGoal : Goal<JugState> { public: JugGoal(int target_volume) : t_(target_volume) bool satisfied(const JugState is) // Check if any of the jug in state js // holds target_ litre of liquid. } /* satisfied */ private: int t_ ; }; /* JugGoal */ class Goal { public: . .. virtual bool satisfied(coast State~ state) = O; }; ~¯ /* Goal */ We then introduce into the search engine a pointer that refers to the above abstract base class. Now, suppose we decide to use another goal condition for our planner, and want to search for states so that the volume of both jugs are specified. For example, we want jug A to hold 1L and jug B to hold 2L. To allow such goal to be specified, we define a second goal verifier class for the jug domain. template <class State> class Search { public: Search(const State~ start_state, Goal<State>* goal, ....); SearchPath<State>findSolution() // Get next solution class JugGoal2 : Goal<JugState> { public: JugGoal2(int tl, int t2) tl_(tl), t2_(t2) private: Goal<State>*goal_ ; // Pointer to goal object bool satisfied(const JugState is) // Determine if both jugs in js //satisfy the goal condition. } /* satisfied */ private: int ti_, t2_; }; /* JugGoal2 */ }; /* Search */ When the findSolution()function needs to check if a state statisfies the goal of the search, it delegates the responsibility to the object referenced by goal_. Noticethat the changeof the definition of a goalin the water-jugdomaindoes not requireany modification of the searchengine. We can also easilymodifythe goal condition for it to servea partial-order planner.In thiscasethe class JugState encodes all necessary elements of a partial order plan, including steps, variables, initial and goal steps, causal links, threats and open preconditions. template <class State> SearchPath<State> Search<State>: : ~indSolution() ° ... if (goal_->satisfied(state)) ... ° } /* findSolution */ class JugGoal3 : Goal<JugState> { public: JugGoal3(): { bool satisfied(JugState partial_order_plan) // return True when // (1) no open preconditions exist and // (2) no (negative) threats exist // for all causal links } /* satisfied */ }; /* JugGoal3 */ With the above design, even if we change the goal verification mechanism, the search engine does not need to be changed. All that is required is that the users configure the search engine with a different concrete goal verifier object. The design is summarized in figure 2. We illustrate the implication of this design using the water-jug example domain. Suppose the states in this domain are encapsulated in the class JugState. The goal of the water-jug problem is to reach a state in which one of the jugs holds a specific volume of liquid. To implement such goal verifier, we define a concrete subclass of Goal<JugState>. Successor Generator The Strategy pattern can be applied to isolate the variation of the successor generation mechanism in a simi- 107 lar way (see figure 3). We define su ccessor ge nerator class Successor<State>, which, again, is a parameterized abstract base class. }; After examining the solution of a number of instances of the water-jug problem, we notice that the macro operator "emptyJugB, pourContentsOfJugBIntoJugA" are often used in a solution. We then conjecture that introducing this subsequence as a new operator to the problem might speed-up the search significantly. We build a new successor class for the problem. template <class State> class Successor { public: ..., virtual List<SearchStep<State> > expand(const State~ state) = .... }; class JugSuccessor : public Successor<JugState> { List<SearchStep<JugState> > expand(const JugState is) // A variation of JugSuccessor // that has an extra operator. } /* expand */ /* Successor */ Again, we introduce an abstract re~rence ~om the search engine to the abstract Successor<State> class, and delegate successor generation responsibility to the re,fenced object. template <class State> class Search { public: Search(const State~ start_state, Goal<State>* goal, Successor<State>* successor, ....); SearchPath<State> findSolution(); // Get next solution }; After testing the new successor generator, we notice that the introduction of the new operator increases the branching factor of the search, and actually degrades the performance of the search engine. We then decide to switch back to the original successor generator. Notice that none of the other classes has to be changed during the above evolution of the program. (The technique of building new operators by combining the old ones are called macro-operator learning. It is known that such learning does not guarantee speedup for the resulting problem solver. Such anomaly is called the utility problem. The scenario discussed here is only a mock-up example. Real macro-operator learning is more complex.) Likewise, we can also easily choose to use a partialorder planning algorithm to implement successor generation. Here is an example: .°°° private: Goal<State> *goal_; // Pointer to goal object Successor<State> *successor_; // Pointer to successor generator }; /* Search */ template <class State> SearchPath<State> Search<State>::findSolution() class JugSuccessor : public Successor<JugState> { // A partial order planning example... °... successor_->expand(state) °°.. } /* findSolution */ State-based Search Let us examine how the above designstreamlinethe evolutionof the water~ugplanner To implementthe water~ug planner, a concrete successorgeneratorclassis derived~om the abstract base class Successor<Jug>The expand() method overridden class JugSuccessor : public Successor<JugState> { ..°. List<SearchStep<JugState> > expand(const JugState js) // apply all possible operators to this jugState // ie. fillJugA(), emptyJugB(), // pourContentsOfJugBlntoJugA(), etc. // return list of all possible successor states; } /* expand */ List<SearchStep<JugState> > expand(const JugState is) // Jug state js is now a partial order plan. // Consider the plan is: // If there are threats to a causal link // resolve the threats, // producing successor states; // Else if there is an open precondition ?pre // Find all operators that can achieve ?pre // Find all existing steps that can achieve ?pre // Produce successor plans by inserting // new causal links // for achieving ?pre // Endif // return the list of all successor states; } /* expand */ }; 108 that the search process can be resumed and the next solution is sought. Encapsulating Variation in Search Control Strategy Variations in Search Control Strategy Search control strategy is the policy by which the search engine is employedto select a node for expansion. In almost all cases, a search control strategy is implemented with the help of a node store; generated nodes are stored in the node store. Every time the search routine expands a node, it requests a node from the node store. A search control strategy therefore regulates which node in the node store should be returned to the search routine. For example, breadth-first search (BFS) is realized by making the node store a queue, thereby imposing a first-in-first-out (FIFO) ordering in node expansion. As such, the node store together with the ordering of nodes imposed by the search control strategy forms the instantaneous state of the search process. Over the years, the AI community has developed many different search strategies. The simplest ones are strategies like breadth-first search or depthfirst search (DFS). Others like depth-first iterativedeepening (DFID) (Kor85) or A* utilizes computational resources more efficiently and intelligently. No single search strategy is the best in all cases. Users must analyze the context of the application, and figure out, either empirically or heuristically, the search strategy that best fits their needs. In other words, the implementation of the search engine must be reconfigurable so that users can select the right search control strategy to use, either at compile-time or at run-time. On the other hand, the search routine could be used in manydifferent ways. Someusers want to find one solution, some want to find all, while others might want to examine solutions one by one, picking the one that they are satisfied with. A search routine should therefore be resumable; that is, after finding a solution, the users should be allowed to restart it again. Multiple solution can then be obtained by successively resuming the search. To summarize, we are dealing with the following is- Search Controller The Bridge pattern is applied to resolve the above issues. In particular, we introduce a search controller class to encapsulate the implementation of the node store. template<classState> class SearchControl public: .... virtualSearchNode<State> remove()= virtualvoid insert(constSearchNode<State>~ node) = O; }; /* SearchControl*/ The search engine maintains an abstract coupling with the search controller, thereby accessing its functionalities via a well-defined interface. template<classState> class Search< public: Search(constState~ start_state, Goal<State>* goal, Successor<State> * successor, SearchControl<State> * control); SearchPath<State> findSolution(); // Get next solution private: Goal<State>*goal_; // Pointerto goal verifier Successor<State> *successor_; // Pointerto successorgenerator SearchControl<State> *control_; // Pointerto searchcontroller .... }; /* Search */ sues: template<classState> SearchPath<State> Search<State>::findSolution() * Wewant to avoid a permanent binding between the search engine and its implementation of its node store, so that reconfiguration can occur at both compile-time and run-time. ¯ Weanticipate that, as the search application developed by our users get more and more sophisticated, they will want to experiment with newer and more special purpose search strategy. Standard search strategies provided by our library is going to be become limited. Weneed an arrangement which allows them to create new search control strategy without recompiling the rest of the application. control_->remove() control_->insert(node) .°°, } /* findSolution*/ To build a new search controller, one simply develop a concrete subclass of the SearchControl<State> class. This design, as depicted in figure 4, resolves the issues in the following way: ¯ Users can configure the search engine with any search control strategy by passing the appropriate concrete search controller into the constructor of the search engine. ¯ To allow users to resume a search process, the instantaneous state of the search has to persist the life-time of the search process. Weneed a design in which the instantaneous state of the search can be retained so 109 I SeatchController<Smtc> ] inserlO remove() ConcmteContmller I <State> lnscrtO removeO A controller T controller_->remove (node) II...... ConcretcControlier2<State> insertO remove..() controller->Insert (node) ready in the queue before inserting controller BFS2. I Figure 4: Controller ¯ There is a generic architecture for incorporate new search control strategies that are not found in our library. ¯ Since the search controller captures state of a search process, and since independent of the search process used to resume the search process the instantaneous it has a life-time itself, it can be when necessary. it. Let us call this template <class State> class BFS2 : public SearchControl<State> { public: void insert(const SearchNode<State>~ node) if (! set_.member(node)) // Proceed if node hasn’t been generated. queue_.enqueue(node); // Put node into queue. set.insert(node); // Record that node is generated. } /* if */ } /* insert */ // remove() remains the same. protected: Queue<SearchNode<State>> queue_; // Queue as the node store. Set<SearchNode<State>> set_; // Set to carry generated nodes. Again, we illustrate the implication of the above design by looking at some examples. Our water-jug planner uses a breadth-first search strategy to conduct the search. The BFS<State> controller uses a queue as the node store to impose a FIFO ordering in node expansion. Noticethat no otherpart of the search~ameworkhas to be changedwhen we switchto the BFS2 controller. template <class State> class BFS : public SearchControl<State> { public: Search Controller Decorator An alertreaderwill noticethat most of the code for BFS<State>and BFS2<State>are the same. The commonality of codeis in factnot an accident. Consider the implementation of a DFS controller and its variation for searching a graph. .. ¯ . void insert (const SearchNode<State>~ node) queue_,enqueue(node) // Put node into queue. } /* insert */ template <class State> class DFS : public SearchControl<State> { public: SearchNode<State> remove() SearchNode<State>node ; if (!queue_.empty()) // Proceed if queue isn’t empty. node = queue_,front() // Fetch the first node in the queue. queue_, dequeue( ) // Remove the first node from queue. } /* if */ return node ; // Return node. } /* remove */ protected: Queue<SearchNode<State> > queue ; // Queue as the node store. }; void insert(const SearchNode<State>~ node) stack_.push(node); // Push node into stack. } /* insert */ SearchNode<State> remove() SearchNode<State> node; if (!stack_.empty()) // Proceed if stack isn’t empty. node = stack_.top(); // Fetch the top node in the stack. stack_.pop(); // Delete the top node from stack. } /* if */ return node; // Return node. } /* remove */ protected: Stack<SearchNode<State> > stack_; }; /* DFS */ Examiningthe search performance,we notice that the same nodesin the searchspacesare expandedmore than once.This is due to the fact that the same state can he generatedby two parent nodes, and both instancesare introducedinto the queue.It meansthat the above BFS controllershould only be used with a problemin whichthe searchspaceis a tree.We design a secondBFS controllerwhichchecksif a node is al- 110 SearchColltrollcr<State> ,i in.riO template <class State> class DFS2 : public SearchControl<State> < public: .. ° I void insert (const SearchNode<State>~ node) if (! set_.member(node)) // Proceed if node hasn’t been generated. st ack_.push(node) // Push node into stack. set_.insert(node) // Record that node is generated. i.~.O ...... I I J.~.O rem°veO .,.~.o 0 / o---I...... t O" ""]...... 1 [ [ contron.r_....... ~no~e,; l Figure5: Control]er Decorator } I* if *I °... private: SearchControl<State> *controller_; // Controller being decorated }; /* ControlDecorator */ } I* insert *I // remove() remains the same. protected: Queue<SearchNode<State> > queue_; // Queue as the node store. Set<SearchNode<State> > set_; // Set to carry generated nodes. Here the insert and remove operations will be implemented by classes such as BFS and DFS. These classes only record the differences in data structures that are used for implementing BFS and DFS and so on. Reusable variations of behavior can now be encapsulated into the subclasses of the controller decorator class. They perform message transformation, and delegate the transformed message to the controller object referenced by the member variable controller_ in ControlDecorator<State>. For example, a statement as followscan be used to instantiate the ControIDecc~ ratorby an implementation of breadth-first searchcontrol: }; The repetition of code simply suggests that the notion of "graph version of a search control strategy" is an individual unit of reuse. In general, there are many variations to a given search control strategy. For example, a bounded search version of a search control strategy restricts search depth to a specific level, and a graph version of a search control strategy checks if a node is visited already before it is inserted to the node store. There is a depth-first iterative-deepening version of A*. In fact, depth-first iterative-deepening itself could be considered a variation of DFS. Coding such variations for each search control strategy could be a very tedious, monotonic task. It will be very desirable if one can define the same variation once and then apply it to all search controllers. Howdo we capture behavioral variations of search controllers as reusable objects that can be flexibly combined with existing search controllers? Examining the actual variations, we realize that they are nothing more than message transformations, that is, they perform additional regulation before the actual node insertion or removal occur. Decorator seems to be a natural choice to resolve the above issues. In particular, we definea ControllDecorator<State> class. ControlDecorator<JugState>(bfs) With this design, as depicted in figure 5, a decorator for graph searching can be coded as follows. template <class State> class GraphControl : public ControlDecorator<State> < public: GraphControl(SearchControl<State>* control) : ControlDecorator<State>(control)< .... } void insert(const SearchNode<State>~ node) if (! set_.member(node)) // first perform data-structure specific operations: ControlDecorator<State>::insert(node); // then perform a data-structure independent operatioi set_.insert(node); template <class State> class ControlDecorator : public SearchControl<State> { public: ControlDecorator( SearchControl<State>* controller) // Constructor virtual SearchNode<State> remove() = // Remove a node virtual void insert(const SearchNode<State>~ node) = 0; // Insert a node } /* if */ } /* insert */ SearchNode<State> remove() return ControlDecorator<State>::remove(); } /* remove */ protected: Set<SearchNode<State> > set_; }; /* GraphControl */ In ) the above,ControIDecoratoriStat%::insert(node implementsa data-structure dependentinsertionfunc- 111 tion. The statement commonto all graph control routines, set_insert(node), is now abstracted out. Now composinga graph version of BFSor DFSfor the waterjug domain becomes a simple task: S earchControl<JugSt at e> *bfs = new BFS<JugState>(); SearchControl<JugSt at e> *ctrll= new GraphControl<JugState>(bfs) SearchControl< SugStat e> *dfs = new DFS<JugState>(); SearchControl< JugStat e> *ctrl2= new GraphControl<JugState>(dfs) As we haveseen,theapplication of Decorator pattern allows reusable variations of searchcontrol strategies to be isolated as independent components thatcan be flexibly composedwith other search controllers. For example, we can define a bounded search decorator, which introduces a bound to the search depth of any search controller. To do that, it delegates insertion messages to the underlying controller only when the search depth is not exceeded. A removal message is always delegated to the underlying search controller as is. Such bounded search decorator can be coupled with any search controller. This saves the need to define a separate bounded version of every search controller. Moreover, new search controllers can be built on top of existing search controller. For example, a depth-first iterative-deepening strategy repeatedly invoke depthfirst search with increasing search bound until a solution is found. It has a linear space usage (as in depthfirst search), but it guarantees optimality of solution (as in breadth-first search). Because its asymptotic time complexity is the same as both depth-first search and breadth-first search, it is a very appealing search strategy. Such a search controller can be defined as a decorator object coupled with a search bound decorator, which in turn is coupled with a depth-first search. Summary Frequent shift of representation, high demandfor flexible composition, and obscurity in module boundary make reusing AI planning techniques extremely nontrivial. In this paper, we summarized our experience of applying design patterns to the design of a reusable framework for search-based applications. Welearned several lessons in this process: ¯ The complexity of building AI planning applications can be managedby good object-oriented design practices. Design pattern catalogs make such knowledge accessible to AI system builders. ¯ The terminologies offered by the design pattern catalog greatly improved our communication. At first, we had difficulty communicating the proper use of controller decorator to our users. But then we use the design pattern catalog to introduce the pattern behind the code, then thereafter, they grasp it quickly. 112 ¯ The way design patterns catalogs assist a software designer can be very indirect. Although sometimes we directly discern the applicability of a pattern (as in the case of designing the controller decorator), most of the times, it is the understanding of the principles behind the design patterns that inspire our design practices. Usually, it is only after finishing design that we recognize that our design turn out to be an instance of a knownpattern. ¯ The above phenomenon reinforces the notion that the explanation and elaboration of howissues are resolved in many pattern genre is the most valuable part of a pattern. Most of the times, the actual solution form is not recalled, but the technique being used to resolve issues are quickly remembered and applied. Acknowledgment The work is supported by grants from Natural Sciences and Engineering Research Council of Canada (NSERC), BC Advanced Systems Institute, ISM-BC and Canadian Cable Labs Fund. References L. Nunes de Barros, J. Hendler, and V.R. Benjamins. Par-kap: A knowledge acquisition tool for building practical planning systems. In Proceedings of the 15th IJCAL pages 1246-1251. Morgan Kauffman Publishers, 1997. Fahiem Bacchus and Froduald Kabanza. Using temporal logic to control search in a forward-chaining planner. Technical report, University of Waterloo, Waterloo, Ontario, Canada, 1995. Available via the UI%L ftp://logos.uwaterloo.ca:/pub/tlplan/tlplan.ps.Z. L. Nunes de Barros, A. Valente, and V.R. Benjamins. Modeling planning tasks. In Third International Conference on Artificial Intelligence Planning Systems, AIPS-96, pages 11-18, 1996. E. Gamma,R.. Helm, 1%. Johnson, and 3. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. R. E. Korf. Depth-first iterative-deepening: an optimal admissible tree search. Artificial Intelligence, 27(2):97-109, 1985. D. McAllester and D. Rosenblitt. Systematic nonlinear planner. In AAAI ’91, pages 634-639, 1991. J. Pearl. Heuristics: Intelligent Search Strategies for Computer Problem Solving. Addison-Wesley, 1984. M. Peot and D. Smith. Threat-removal strategies for partial-order planning. In AAAI ’93, pages 492-499, 1993. Daniel Weld. An introduction to least-commitment planning. AI Magazine, Winter, 1994:27-61, 1994. Qiang Yang. Intelligent Planning -- A Decomposition and Abstraction Based Approach. Springer-Verlag, 1997. ISBN 3-540-61901-1.