Today, design patterns form an object oriented design heuristic of prime importance. ... This situation is justified as follows: as it is acknowledged that âa solution, the .... Accept and execute instructions from a human operator that wishes to apply ... with past abstraction mechanisms, such as 4GLs (4th-generation languages) ...
A Formal Language for Design Patterns (extended abstract) 1
Amnon H. Eden
Joseph (Yossi) Gil2 Amiram Yehudai1
Abstract This article sets forth a rigorous approach to the concept of a ‘design pattern language.' We present a method of using a programming language to manipulate constructs of ordinary OOP language (such as classes and relations). We show that the specification of many design patterns can be phrased as algorithms in this metaprogramming environment. The advantage is in delivering a precise method of defining design patterns. In addition, we present a ‘patterns wizard’, a tool that automates the application of patterns by metaprogramming techniques, without hiding the source code text from the programmer. We conclude with three case studies and two sample outputs that demonstrate how the application of many design patterns can be automated using metaprogramming techniques. Keywords: Design patterns, metaprogramming, the programmer’s apprentice.
1. Introduction Today, design patterns form an object oriented design heuristic of prime importance. Writing a pattern is an excellent method of conveying experience from the expert to the novice. Using patterns raises the level of abstraction of software documentation of any form, graphic or verbal, by concisely communicating the essentials of the design process or its results. Thus is it consequential to allow a specification of a pattern to be complete and accurate about the design guidelines it signifies. Nonetheless, contemporary design patterns discourse is conducted mostly using an inconclusive and ambiguous medium such as natural language narrative. This situation is justified as follows: as it is acknowledged that “a solution, the essence of a pattern, … transcends the exact nature of its expression” [Johnson & Cunningham 95], many appear to conclude that “a pattern must ultimately reside in one’s own mind.” (ibid.) One must agree with this, as the overall design must be reasoned in one’s own mind, and so are its elements. But does “ultimately” implies “exclusively”? As this article is intended to show, the solution indicated in many design patterns may transform into a robust and explicit expression, that does not compromise with the level of abstraction which pattern specification must have. We will show that the procedure prescribed by accurate specifications of design patterns can be expressed in terms of simple manipulations over programs. We suggest a programming language framework used for precise specification of design patterns by means of such operations, and demonstrate how a formal pattern language can be what is indeed more like a programming language “in the ordinary sense of the term."3 Describing patterns using natural language prose is inevitably informal, inaccurate, and incomplete. For the same reason the pattern application process is manual, time consuming and error prone. However, the use of a more formal technique has great advantages that need no elucidation, but so is the challenge and the difficulty. This paper is not a complete work but a mere case study of a formal approach to the specification of design patterns. It highlights some related problems, proposes initial solutions, and hopefully inspires further research on the topic. 1
The Department of Computer Science, School of Mathematics, Tel Aviv University, Tel Aviv, Israel. Supported in part by a grant from the German-Israeli Foundation for Scientific Research and Development (GIF).
2
The Faculty of Computer Science, Technion -- Israel Institute of Technology, Haifa, Israel.
3
As opposed to the Alexandrine originated usage of “pattern language”, which is not “a programming language in any ordinary sense of the term” [Coplien 94].
The term “design pattern” has been used in the literature with various, sometimes conflicting meanings. We restrict ourselves to the interpretation as in [GoF 95]. According to this interpretation, a design pattern is a description of the essence of an explicitly dictated implementation of a solution reusable across common design problems.
1.1 A framework of pattern specification How should pattern languages be formalized? One possible direction is by using a formal specification language such as Z or Larch to capture the inter-object relationships and dependencies. However, this alone would not have been sufficient: formal specification languages are powerful in describing the external characteristics of any particular system without specifying any implementation details. In contrast, a design pattern is in many cases an implementation strategy that captures a specific solution, and the process of formulating it directly refers to the steps that are taken in its implementation. Formal specification languages, however, were not molded to express implementation details. An even more serious limitation of specification languages is that they are usually not powerful enough to describe a generic family of systems, as it is required in the case design patterns.
Metaprogramming Since specification languages are inadequate we use an ordinary programming language as a leverage for manipulating design patterns. But instead of manipulating design patterns directly, the entities manipulated are the programs that are modified to realize these patterns. This is nothing but metaprogramming, i.e., writing programs that manipulate other programs. The tern metaprogramming signifies that there are two separate programming languages combined in a single environment, where one language manipulates the other. The advantage of this approach is that the semantics and the syntax of programming languages are well understood and precisely defined. Further on this section we discuss the disadvantages of this approach, such as the abstraction level of OOPL's grammatical constructs being too low, and the solution we chose to this problem.
Metalanguage and object language Using an ordinary OOPL by which the pattern specification language (PSL) is phrased subjects it to the syntax and semantics dictated by this OOPL. The use of a common programming language facilitates pattern definitions by available development tools such as compilers and debuggers. We designated the OOPL that is used for pattern specification as the metalanguage, i.e., the language in which the manipulation of programs is phrased. By the approach we conceived, a design pattern is formalized by phrasing it as an algorithm specified in the metalanguage. A pattern algorithm rigorously indicates the sequence of steps taken to apply a pattern, hence it is realized as a routine in the PSL. We denote prescriptive pattern a design pattern that can be, for its most part or as a whole, precisely prescribed in PSL. An example for a prescriptive pattern is the Observer pattern (originally appeared in [GoF 95]) that was used as our first case study and is discussed in detail in section 3.1. The language over which PSL routines operate is denoted the object language. The entities manipulated by pattern routines are the object language constructs, such as classes, objects, inheritance relations, aggregation relations, statements, expressions, etc. A design pattern is applied by passing such constructs as arguments to a pattern routine, which modifies them as required. The internal abstraction of object language that is directly manipulated by the pattern routines is referred to simply as “the internal representation of the object language.” Figure 2 illustrates the internal representation we chose to implement, and section 2.4 elaborates on the abstraction level of the constructs that appear in this diagram.
Metalanguage vs. Pattern Specification Language As indicated, in the metaprogramming framework described we employ an OOPL as the metalanguage that is used as the pattern specification language. Note, however, that since pattern specification is carried out by the manipulation of abstractions of the object language constructs, the metalanguage requires a supplement of some representation of the object language which is the form of the internal representation of the programs manipulated. This is true whatever OOPL utilized as the metalanguage. Section 2.4 gives detail of the class library that constitutes the abstract representation of the object language .
-2-
1.2 The patterns’ wizard We implemented a prototype for a tool by the name ‘patterns wizard' used for the specification and application of design patterns. The wizard is used to semi-automate the application of prescriptive design patterns to existing code, a process which is currently purely manual. As we suggest, the patterns wizard reads object language code, generates an internal representation of it, and can apply pattern routines to this abstraction. Table 1 lists the distinct functions delivered by the pattern wizard, and Figure 1 depicts our vision of software development in collaboration with it. Object language compiler
compile human programmer edit source Object language source
request to create or modify a DP
request to apply Dpi to constructs x1, x2,
parse classes regenerate source
Manipulate
Internal Representation
Patterns Wizard Figure 1: Working with the patterns wizard
The patterns wizard performs (or supports doing) the following tasks: 1.
Parse object language text and produce an internal representation of the program
2.
Read, store, retrieve, and modify PSL routines
3.
Accept and execute instructions from a human operator that wishes to apply a pattern to suitable constructs
4.
Reproduce object language text from the internal representation
Table 1: Distinct functions delivered by the patterns wizard
The ‘pattern application’ process We should clarify the nature of the pattern application process, which is the pattern’s realization process as performed by the wizard. A pattern is expressed as a routine in the metalanguage that accepts as arguments elements of programs (written in the object language) and manipulates them to introduce the pattern into the program. To do so, the manipulated constructs need be available in an abstract form that is convenient to the application of operations by which design patterns are prescribed. We conclude that the abstraction of the object language, the internal representation, needs also be compatible with the PSL routines, i.e., formulated in the metalanguage. As a result of the execution of a pattern routine the internal representation is modified as prescribed in the pattern’s algorithm.
Limits to the wizard’s domain Clearly, only the parts of a pattern specification that are stated in detailed, precise, and formal terms, may be translated into PSL and automated by the wizard. The automation of activities described in more abstract terms is sophisticated -3-
and has the prerequisite of first translating these to concrete programming activities. As a result, the application of the rest of the pattern need be completed by the programmer manually editing the program after translating it to the textual representation. For this reason the support gained from the wizard is termed semi-automation of the pattern application. We are also aware that with many design patterns such as the Strategy pattern, a large share of the pattern is specified by general terms that are too abstract to be translated into a metaprogramming algorithm. More specifically, the relations between the agents that collaborate in the implementation suggested by the Strategy pattern and the manner by which they cooperate depends on a large number of factors and their form cannot be accurately put in concrete expression. Techniques for handling the reasoning necessary to transform such descriptions to metaprogramming algorithms appear to belong to the realm of artificial intelligence, and may benefit from machine learning or knowledge-based techniques. It appears at this point that patterns such as the Strategy pattern are not prescriptive in the sense defined above, and therefore lie beyond the “jurisdiction” of the patterns wizard. It is also clear that the expressive power of existing OOPLs and other programming languages has its own merits, and higher-level tools cannot replace it, but only add to it4. For this reason it is desired that the programmer has direct access to the source code along with manipulating indirectly through application of PSL routines. To summarize, the human programmer is assisted by the wizard in combination with the contemporary tools that commonly participate in the software development process, as also implied by Figure 1.
Outline The rest of this paper is organized as follows: Section 2 describes how PSL was formed, the structure of the internal representation of the object language, and also the implementation of the patterns wizard. Section 3 gives an account of two case studies in translating prescriptive patterns to PSL routines, and an example how the wizard can be used to enforce design principles. Finally, section 4 gives the conclusion and delineates the future research.
2. Implementation 2.1 Forming PSL There is a finite number of recurring formations, such as relations and procedures, that jointly make the building blocks of design patterns. These elements may be viewed as rudimentary design patterns themselves. The definition of a PSL is in fact a definition of a compact collection of building blocks that jointly are sufficient to phrase the metaprogramming algorithm that designates the application of any prescriptive design pattern. It may seem that a complete definition of such collection requires a universal catalog of design patterns. In the absence of such improbable catalog, the recording of patterns building blocks is instrumented by enumerating (the abstracted form of) the constructs of the object language and the necessary operations that may apply to each. It follows that a careful observation of the prescriptive patterns at hand, in combination with a suitable level of abstraction in the representation of the object language, may lead to PSL definition that is suitable also for the specification of other design patterns that were not directly analyzed. The expressiveness of the implementation suggested here is ultimately tested empirically, that is, by using it to specify prescriptive patterns that were not considered in the case studies reported in section 3.
2.2 Programming languages Two OOPLs are involved in the definition of PSL and in the wizard’s implementation: The metalanguage, by which the PSL is defined, and the object language, the text of which is analyzed. It follows from the above discussion that the selection of each language has far reaching implications on the expressiveness and ease of use of the final product. We concisely present here the arguments that led us to the choices we made. For the choice of each language we first considered the issue of whether a statically typed (“static”) or a dynamically typed (“dynamic”) OOPL should be used. As for the object language, static programming languages have the advantage of having type information available, which dynamic languages miss5. Also, a language is suitable to serve as the object 4
Learning from experience with past abstraction mechanisms, such as 4GLs (4th-generation languages), initial CASE tools, code generators and application generators, it appears that when high-level constructs assumed the place of the old-fashioned programming language the result is always a language of a expressive power limited to a very specialized domain.
5
Although at some stage Smalltalk was also considered and was found a suitable candidate, in particular because of the availability of abstract metalinguistic information from Smalltalk systems. Smalltalk is not suitable for the reasons that follow.
-4-
language if an application of the wizard significantly reduces the development effort in terms of textual changes, consistency validation, and managing the technical aspects of the language. Our pick was Eiffel as the first choice for a host of benefits it has as the object language. Eiffel has a simple and well-defined grammar, and its syntactical constructs are close enough to the level of abstraction used by the PSL, which simplifies the generation of the internal representation. Also, a suitable Eiffel parser was available that is described in detail in section 2.5. Given the different functions of the wizard as listed in Table 1, the metalanguage by which PSL is defined is preferably a dynamic OOPL. Smalltalk, supported by an environment such as the Smalltalk/V and a library of useful container classes and formatting tools, is convenient for rapid prototyping of the PSL and generating the internal representation of Eiffel. The advantages of dynamic languages in general and Smalltalk in particular are being powerful and flexible, while performance and safety are not a major issue at this stage. The Smalltalk/V environment is also suitable for the framework of pattern application, i.e., as a storage of pattern routines and their execution. Note that the distinction made between the object language from the metalanguage implies that the framework we suggest is not necessarily reflective. If, for example, Smalltalk was chosen as the object language the framework might have been considered reflexive. However, we do make this discrimination to stress that as a metaprogramming framework it is applicable to any OOPL, in particular there that are not reflexive.
2.3 The wizard’s usage Here we depict a characteristic session of the patterns wizard usage, which assumes the existence of a patterns library that contains the complete specification of a number of useful patterns, and a software project at the implementation (programming) stage. Then, at some point, the programmer desires to apply a design pattern that showed up in the system’s design. 1.
The existing Eiffel source is parsed and an internal representation is generated (refer to section 2.4 for details of the internal representation of the object language, and to section 2.5 for a description of how the internal representation is generated). Paring would involve either the entire system or only a relevant subset of it.
2.
The programmer executes the pattern routine as the actual arguments are selected and passes to it. If the predefined restrictions hold then the routine concludes successfully and the pattern is realized. If the user is not satisfied with the pattern specification, it may be corrected by modifying its routine or adding to its functionality. Thanks to the dynamic nature of the Smalltalk environment, this objective is ever so easy to accomplish without disrupting the work flow.
3.
For the manual completion of the pattern, the system is instructed to transform the internal representation (possibly only of the affected classes) to Eiffel source code. Special notes were by the wizard as references to the locations where modifications were introduced, and to the places where manual programming is necessary to complete the pattern.
The programmer iterates through steps 1 to 3 as many times as she wishes.
2.4 The internal representation of the object language In the implementation of this representation it was necessary to search for the level of abstraction that is most suitable for the sort of manipulations expected in pattern specifications. In one extreme we could use the object language syntactic constructs as the language's actual representation, since these deliver all the detail that exists in the source code. However, the specification of a design pattern by directly operating over its syntactic constructs is tedious, as even a simple operation such as adding a feature to a class warrants a large number of simpler operations. In the other extreme it was possible to use clusters of relations, which may be considered as rudimentary design patterns, as the building blocks of the internal representation. To resolve the question of the most adequate abstractions we identified typical high-level constructs of the object language, each suitable to specify operations of a different level of detail. The exact form by which the object language is abstracted bears significant implications on the PSL since it determines the pattern language’s vocabulary and idioms. For instance, inheritance relations of a class are stored in an object of (a descendant of) the OrderedCollection class. To form an inheritance relation between two classes one must create anInheritRelation and add it to the collection of inheritance relations by calling add: or any one of its versions. For the same reason it is possible to use all available variations of do:, such as collect: and select: and the rest of the OrderedCollection interface, when manipulating inheritance relations.
-5-
PSL is further influenced by two more factors: 1.
Smalltalk’s syntax, library classes, and other linguistic features (such as single inheritance).
2.
The syntax and properties of the Eiffel language: Although we strive to dissociate the object language internal representation from Eiffel’s idiosyncrasy, there is an obvious limit to the likely success of this aspiration. In particular, to reproduce the complete Eiffel source, the internal representation is required to bear indications to Eiffel peculiarities such as the content of select statements.
Our class diagrams use the Booch notation, as defined by Booch [94]. All one-to-many relations were implemented using class OrderedCollection. A top level view of the internal representation class library is given in Figure 2.
A_Construct (from )
Feature Features
0..n
Expression A
Type class
1
ClassDeclaration
name( )
endComment( )
Instruction
mark : Keyword name : String
Type
0..1
Inheritance
replaceTypes: GenTypesColl byTypes: typeColl
String name()
Anchored
1
InstantiatedClass
1..n
class instantiate: aGenericClassDec byTypes: typeColl( )
Instantiated Class
GenericClass Declaration
Simple
String fullName( )
Constraint
Classtype Instantiated
name : String
Type name : String
Actual Generics
GenericType Instantiated instantiate() name : String
GenericArg name : String
Figure 2: A class diagram of the Eiffel abstract internal representation, top level view
2.5 Generating the internal representation of Eiffel TROOPER (Truly Reusable Object Oriented Parser for Eiffel Reengineering) is an Eiffel parser, as described in [Avotins, Maughan & Mingins 95]. Its implementation was enabled by the development of YOOCC (Yes! an Object Oriented Compilers Compiler, also by Avotins et.al [95].) YOOCC has a similar role to that of the established YACC [Aho, Sethi & Ullman 86]. As the authors of YOOCC explain, it differs from YACC mainly to allow maximum separation between the object language’s syntax and semantics, which renders it a more reusable tool. More formally, a language specification given by non left-recursive BNF rules that make use of Meyer’s Parse Eiffel library constructs (aggregate, choice, repetition, and terminal [Meyer 94]) can be transformed by YOOCC to an Eiffel program that -6-
parses this language. The output of executing this program on a text is an Abstract Syntax Tree, in the form of a compound Eiffel object that comprises the respective linguistic constructs. Since the Eiffel language itself is already defined in these terms, the development of an Eiffel parser such as TROOPER was almost entirely automated by applying YOOCC to the established definition of the Eiffel language grammar [Meyer 92]. The exceptions are constructs introduced by the application of left-recursive elimination process as described in [Avotins 95]). Using the TROOPER class library, the internal representation of the object language is generated in two steps: I. Parse: The Eiffel text of each file is parsed and an Abstract Syntax Tree (AST) is generated that represents a single class. TROOPER was adjusted to convert the Eiffel compound object into a sequence of Smalltalk statements that regenerate this tree as an equivalent Smalltalk compound object. That is, each element in the resulting Smalltalk AST stands for some construct of the Eiffel grammar. II. Abstract: The Smalltalk AST is regenerated using the output of the previous step, and the AST of each class is abstracted to produce its associated internal representation. This task is performed by a Smalltalk program that traverses the AST and gathers the information required for the generation of each high level construct.
3. Case studies This section gives account of case studies that demonstrate the usage and implementation of the wizard. In the first two cases we deliver results of its application to prescriptive design patterns originally described in [GoF 95]. The last case study demonstrates how the wizard and PSL can be used to enforce design conventions.
3.1 Observer The Observer design pattern offers a solution to settings where an unfixed number of observers (or “viewers”) continuously exhibit information originating from a single data store (“subject” or “model”). The Observer pattern dictates a mechanism which provides the subject with simple means of notifying its “observers” whenever a change in data occurs. The class diagram in Figure 3 depicts the general formation of a particular interpretation of the Observer pattern that is one out of several possible and equally valid interpretations. A complete report of this pattern can be found in [GoF 95]. In the illustrated implementation we assumed that information of the change is passed using the pull model, as denoted in [Kim & Benner 95]. S UB JE C T S UB JE C T C O N TR O LLE R attach (AB S TR AC T_ O B S E R V E R [S UB JE C T]) dettach(AB S TR AC T_O B S E R V E R [S UB JE C T]) n otify( ) 1 o bservers
S UB JE C T
n
AB S TR AC T_ OBSERVER * u pd ate(S UB JE C T) make(S UB JE C T) A
feature make (s: SUBJECT) is do s.attach(Current) end
OBSERVER u pd ate(S UB JE C T)
feature affect_the_data is do -- make a change that affects data notify end
Figure 3: A class diagram of the Observer pattern. The terminology used in [GoF 95] was slightly modified.
The sequence of steps performed by the Observer routine is recorded in Table 2. The appendix gives the PSL routine’s source and also results of executing it with two different class declarations that served as “observer” arguments.
-7-
Observer pattern arguments The classes designated SUBJECT and OBSERVER are supplied by the user and modified to accommodate to the new roles they assume:
1.
2.
1.
The SUBJECT class is now responsible to pass the information about the change.
2.
The OBSERVER accepts the message notify triggered by changes in the model and react accordingly. Modifications to the observer class:
Corresponding modifications to the observer’s declaration:
1.1.
Inherit from the ABSTRACT_OBSERVER class
Add an inheritance clause
1.2.
Resolve name clashes with the new parent class
Add the necessary rename clauses to the previous inheritance clause
1.3.
Redefine update(SUBJECT) routine
•
1.4.
To remember the subject, for each creator, generate a duplicate which also takes class SUBJECT as an additional argument, and passes it to the abstract observer’s make
Add a redefine clause to the previous inheritance clause • Add to the features a routine stub for update(SUBJECT) Add creator routines which duplicate the originals, with the exceptions of an additional argument and the additional call statement.
Modifications to the subject class:
Corresponding modifications to the subject’s declaration:
2.1.
Inherit from the CONTROLLER class
Add an inheritance clause
2.2.
Resolve name clashes with the new parent class
Add the necessary rename clauses to the previous inheritance clause
Table 2: Operations performed by the wizard in the Observer routine
Routine results 1.
2.
3.
4.
The CONTROLLER class is a generic class having the SUBJECT class as the only formal generic argument. The class definition contains the following features: 1.1.
An observers attribute of type LINKED_LIST[ABSTRACT_OBSERVER[SUBJECT]], which holds the list of observers of the subject.
1.2.
The routine notify()is triggered by the subject whenever a change in model occurs. The routine iterates through the observers list and calls an_observer.update(current) for each observer.
1.3.
attach and detach routines that accept ABSTRACT_OBSERVER[SUBJECT] objects as arguments and maintain the observers list.
The ABSTRACT_OBSERVER generic class detaches the controller from its observers, and at the same time generalizes the possible type subject (as subject is a generic argument). The is supplied with two features: 2.1.
A creator with a SUBJECT argument, which attaches this observer to the subject at the creation of the observer.
2.2.
A deferred routine update (subject: SOME_SUBJECT_CLASS), to be overridden by each OBSERVER.
Modifications to the OBSERVER class are summarized in Table 2: 3.1.
The class becomes a child of ABSTRACT_OBSERVER[SUBJECT].
3.2.
For each creator the class is supplemented by a duplicate with a SUBJECT argument, where the new creators also set the reference to SUBJECT.
Modifications to the SUBJECT class are also listed in Table 2. -8-
Sample output Please refer to the appendix for the implementation of the pattern. The appendix contains two of the major products of the routine: •
The new class ABSTRACT_OBSERVER and its routines listed in Table 4. Note the deferred update which is redefined in the concrete observer class.
•
An EXAMPLE5 class listed in Table 5. Because EXAMPLE5 became an observer of SUBJECT_CLASS it incorporates a new creator which invokes the ABSTRACT_OBSERVER’s creator in order to retain a reference to the subject (as dictated by the interpretation we chose to the Observer pattern).
Discussion Both new classes CONTROLLER and ABSTRACT_OBSERVER are generalized using the Eiffel genericity mechanism, and therefore their declarations need not change. As a result, the two can be “local” with respect to the pattern’s routine. In other words, to deliver both classes it is sufficient to reuse the Eiffel source without change along different applications of the pattern. The wizard is useful when several different observer classes share a single subject class but otherwise have very little in common. Using it relieves the programmer from making repeated changes of similar nature in each of the different observer classes. Looking at the list of changes in Table 2 one may notice that the required operations are mostly technical and have little significance to the programmer. The role of the wizard is to make references to the locations in the source code that require the programmer’s attention, such as the missing body of the newly introduced notify routine in the OBSERVER class. It is possible to further generalize the Controller and the AbstractObserver classes to fit other interpretations to the Observer pattern. In particular, it is possible to combine the pull model with other methods of conveying information of data modifications to the observers as illustrated in Implementation Patterns for the Observer Pattern [Kim & Benner 95]. No attempt was made in this example to modify the SUBJECT to accommodate notifications about changes in the data model. However, it is possible that the relation between the SUBJECT class and the data model is established so that the wizard can detect where modifications to the observed data are made. If fact, computing where modifications to data are made is equivalent to calculating the C++ concept of “const method” for an Eiffel method, a topic that is covered in section “Future directions”. If data-modifying methods can be automatically determined then it is possible for the wizard to insert a call to notify before each return statement in each such routine. Refer to the definition of the routine make_autogen0 given in Table 5 in the appendix as an example for the result of implanting such call. If data-modifying methods can be automatically determined, an alternative notification scheme is possible according to Pascoe [86]. The Encapsulators paradigm suggests creating a wrapper around the SUBJECT, which intercepts the routine calls and delegates them to encapsulated object (much in the form of the Decorator pattern), but additionally issues the necessary notifications. The obvious advantage of this implementation is that the observed class is not modified.
3.2 State The State pattern [GoF 95] suggests a compound object that delivers dynamically alterable implementations (“states”) for a fixed interface. Such design is suitable in case the system behavior is modeled by a state machine, e.g., when one out of several distinct implementations delivers the desired performance in each “state”. Whenever the “state” of the object is changed (as a result of some external or possibly internal message) the new behavior is delivered by switching to the implementation that is associated with the new state.
-9-
class DELEGATOR -- . . . feature do1(a:A,b:B) is do current_state.do1(a,b); -- Change state if necessary: current_state := next_state(current_state,"do1") end
ABSTRACT_STATE *do1(a:A,b:B) *do2(c:C,d:D)
DELEGATOR current_state
do1(a:A,b:B) do2(c:C,d:D)
A
CONCRETE_ STATE1
CONCRETE_ STATE2
do1(a:A,b:B) do2(c:C,d:D)
do1(a:A,b:B) do2(c:C,d:D)
CONCRETE_ STATE3 do1(a:A,b:B) do2(c:C,d:D)
Figure 4: A class diagram of the State design pattern. Deferred routines are marked by an asterisk.
aClient
1: do1(a,b)
D:DELEGATOR
C1:CONCRETE_ STATE3
2: do1(a,b) F
Figure 5: An object diagram of a simple do1 scenario in the implementation of the State design pattern. The role of the delegator object here is to simply delegate.
The implementation of the State pattern is depicted in Figure 4. The appearance of an object having different states is achieved by the collaboration of two separate objects, as illustrated in the object diagram in Figure 5: 1.
A delegator, which decouples the current concrete-implementor from its clients and forwards the messages it receives to the current concrete-implementor. It is also the responsibility of the delegator to switch the current concrete-implementor according to a predetermined transition function.
2.
An instance of some concrete-implementor class that delivers the behavior as specified in the present state.
The appearance of “implementations switch” is sometimes modeled using a state machine, which is the reason why this design pattern has this name. The complete implementation of the pattern was omitted for practical reasons, and is available from the authors.
State pattern arguments 1.
The delegator class name and interface, delivered by a class declaration D with a set of routine stubs (marked do1 and do2 in Figure 4).
2.
A collection of concrete state implementation classes {Ci}ni=1, each delivers the desired behavior in a particular state by its methods. - 10 -
3.
An initial state class Cinit which is one of the above implementation classes Cinit ∈{Ci}ni=1
4.
The transition function, given by a set of triples < Ck , Mdi , Cj > (state, message next state) of size not larger than nxd, where n is the number of state classes and d is the number of possible messages. If this set is smaller than nxd it can be assumed that for every “missing” transition Ck, Mdi Cj then Ck = Cj (message Mdi causes no transition from state Ck).
Æ
Æ
Routine results 1.
2.
3.
Complete implementation of the delegator: 1.1.
An attribute current_state: ABSTRACT_STATE which is used to hold the current state object.
1.2.
A creator that initializes current_state with an object of class Cinit.
1.3.
A transition routine change_state(next_state: ABSTRACT_STATE) which attaches next_state to current_state.
1.4.
Delegations: The state machine’s “message” routines (all D routines other than the creators and change_state) shall consist of a block that comprises a single instruction, which is a call that forwards the routine and its arguments (if any) to the current_state object.
A definition for an ABSTRACT_STATE deferred class, with the following features: 2.1.
A reference to the delegator.
2.2.
A creator that accepts a reference to D so it can be used for the implementation of the transition function.
2.3.
A deferred routine is defined for every “message” routine {MDi} of D, with a similar signature.
Definitions for the concrete state classes {C’i}ni = 1: 3.1.
3.2. 4.
Each state class derives from two classes: •
the respective state-implementation class Ci
•
the ABSTRACT_STATE
A routine stub redefines each of the “message” routine MDi
Implementation of the transition function is integrated in the concrete state classes: 4.1.
For each transition Τ(C’i , MDi ) → C’j , a creator stub is added to the next state class C’j with an argument of the previous state class, C’i. This creator defines how the new state object is constructed from a previous state of class C’i, and like the message classes need be completed manually.
4.2.
For each transition Τ(C’i , MDi ) → C’j , the body of the “message” routine MDi in C’i is supplemented with the next two statements: 4.2.1.
Creation of the next state class Τ(C’i , MDi ) object by a creator that has this class, C’i., as argument;
4.2.2.
A call the delegator’s change_state with the next state object as argument.
Discussion The appendix contains an example of a class that was used as the delegator argument to the State pattern implementation. Note the different implementation classes supplied as arguments C1, …Cn must each be able to carry out the required interface of D, designated {MDi} in the above description. Note also that a concrete implementation class must be able to execute “message” routine (MDi) with its specified arguments. Since different implementations are rarely readily reusable, the discrepancy between the actual and the desired interfaces must be repaired manually. Also note that the encapsulation of the {MDi} routines in a new class C’j that derives from Cj follows the description of the adapter pattern. It is therefore possible to separately define a PSL routine createAdaptor and use it to generate the {C’j} classes.
- 11 -
3.3 Enforcing principles of design: The Law of Demeter The patterns wizard can be used to verify adherence to design principles within class libraries of large scale. The PSL is a language by which design principles can be stated formally and explicitly with little effort, and thus test their realization by the wizard. Following is an example of how to test adherence to a design principle. The Law of Demeter [Liberherr & Holland 89] is a design principle used with the purpose of reducing the dependencies between classes by restricting access only to features of “closely related” classes. It is believed that the complexity of the class library is better controlled by enforcing this law. Concisely, the classes that ”closely relate” to routine R of class C according to the Law of Demeter, are C itself, the classes that are “subparts” of C (its attributes), the declared types of the arguments of the routine R, and the classes of the objects locally declared in R. The last section of the appendix shows how the Law of Demeter is formulated and tested. Table 7 gives the routine that computes the set of classes to which calls within R should be restricted. Table 8 gives the routine that computes the actual set of classes that are used in R. Whether R follows the Law of Demeter is determined by comparing the two sets.
4. Summary and discussion We advocate the following benefits of the proposed formal pattern specification language and the patterns wizard: •
The use of a PSL clarifies and formalizes the semantics of design patterns, as precise semantics are attached to the automated fraction of the pattern.
•
Using a patterns wizard ensures consistency in the application of patterns and standardizes software development. For example, the Observer test case given in section 3.1 demonstrates how turning a set of distinct classes into “observers” of the same subject class is performed by repeatedly executing a single wizard routine with each observer as the argument of this routine. The class declaration given in Table 5 was modified by the same technique and augmented as appropriate.
•
The wizard is safer to use, being more predicted and less error-prone than human programmers.
4.1 Future directions There are numerous other ways to employ a metaprogramming tool for the benefit of software development and validation that are not directly related to the research of design pattern. The case study described in section 3.3 shows how a design principle can be stated formally in PSL and enforced using the patterns wizard.
Eiffel const methods The concept of const correctness was not incorporated in the Eiffel language, but is useful with Eiffel programs. For example, as we have shown in section 3.1, if it was possible to infer which are the non-const methods of the SUBJECT class (of the Observer pattern) then it would have been possible to use the wizard to further complete the pattern and add a call to notify() to each non-const routine. Similar exercises of const correctness in Eiffel programs are abundant. To infer the constness of Eiffel objects it is first required to enumerate which operators of this language do not preserve this property, which is the easy part. The harder part would be to determine, for each of the Kernel library's external routines (which call C standard library functions) whether they preserve constness. Only then we will be free to compute the constness of every Eiffel routine, object, etc.
C++ Exception specification Another example of using the wizard for a different purpose is for the generation of C++ exception handling interface specification clauses. Although C++ as the wizard’s object language was not implemented yet, it is simple to show that a similar PSL can be used to compute the correct exception specification clause for every C++ function. A C++ exception specification clause should (usually) enumerate all exception types that may be thrown during its execution. Although there are means to avoid this rule, obeying it is superior in terms of safety, as statically enumerating the complete set of types of exception that may be thrown, directly or indirectly, during the execution of each function is in many cases essential to the prudent client of a function. What makes proper manual inference of the exception specification difficult is that incorrect exception specifications are contagious. That is, if the interface specification for function bar() is incorrect then it is very likely that the interface - 12 -
specification of every client function of bar, which its own exception specification is determined on the grounds of bar’s, is also incorrect. Thus, to record the exceptions thrown directly and indirectly by the execution of foo means that it is necessary to recursively record the exception specification of every function and operator that may indirectly be called during its execution. It is obvious that by this rule the price of adding a safe function to an existing library is heavy, unless the inference of interface specification is automated.
Further work Some future directions for the development of the patterns wizard are: 1.
Enhance the patterns wizard to other object languages (such as C++ and Smalltalk). Since design patterns are mainly implementation strategies, the structure and contents of a pattern are greatly influenced by the choice of the object language. Moreover, many patterns that are fundamental to one programming language (as the Prototype [GoF 95] is to C++) are trivial in another (as the Prototype is to Eiffel). Furthermore, the so called “language independent treatment of patterns” requires an abstract form that equally represents different OOPLs. Since the program text is reproduced from the internal representation, the problem of finding a common representation for constructs of different OOPLs is nontrivial. As our preliminary investigation shows, the solution to this problem seems to require a careful consideration of the common building blocks of patterns that apply to different languages. To do so it is necessary to find building blocks of patterns that are suitable to each language independently.
2.
Use the wizard to “extract” pattern specifications from an existing class library source. Given class declarations that appear as incorporating a useful design pattern, the wizard can assist in the process of extracting the pattern’s specification by generalizing the elements that are essential part of the pattern (themselves pointed by the programmer).
3.
Build an interactive tool that encapsulates the patterns wizard in “user friendly”, possibly graphic interface. For example, an interactive tool can guide its user to the locations in code where the application of the pattern need be completed manually.
Acknowledgments We are grateful to Karl-Heinz Sylla for careful reading of an earlier draft. His numerous comments helped to improve the presentation. We should also thank Dr. Christopher Landauer of Advanced Programs Operations for shepherding our article, Kyle Brown for his comments, and Prof. Douglas C. Schmidt of Washington University for clarifying what the wizard cannot conjure.
References Aho A., Sethi R., and J. Ullman (1986). Compilers: Principles, Techniques, and Tools .Addison Wesley. Alexander, Christopher (1979). The Timeless Way of Building. Oxford University Press, New York. Alexander, Christopher, Sara Ishikawa, Murray Silverstein, Max Jacobson, Ingrid Fixdahl-King, and Shlomo Angel (1977). A Pattern Language. Oxford University Press, New York. Avotins, Jon (1995). Eliminating Left Recursion from the Eiffel 3 Grammar. Technical report, Monash University. Avotins, Jon, Glenn Maughan, and Christine Mingins (1995). Language Processor Construction: The Case for YOOCC and TROOPER.. TOOLS USA 95. Booch, Grady (1994). Object Oriented Analysis and Design With Applications, 2nd edition. Benjamin/Cummings. Coplien, James O. (1994). Software Design Patterns: Common Questions and Answers. A Web article: http://www.research.att.com/orgs/ssr/people/cope/index.html GoF (1995): See Gamma, Helm, Johnson, & Vlissides 1995. Gamma Erich, Richard Helm, Ralph Johnson, and John Vlissides (1995). Design Patterns: Elements of Reusable Object Oriented Software. Addison-Wesley. Helm, Richard, Ian M. Holland, and Dipayan Gangopadhyay (1990). Contracts: Specifying Compositions in Object Oriented Systems. Proceedings of OOPSLA 1990, SIGPLAN Notices, vol.25 no.10. Johnson, Ralph and Ward Cunningham (1995). Introduction, in: John M. Vlissides, James O. Coplien, and Norman L. Kerth (eds.). Pattern Languages in Program Design 2. Addison-Wesley. - 13 -
Kim, Jung J and Kevin M. Benner (1995). Implementation Patterns for the Observer Pattern, in: John M. Vlissides, James O. Coplien, and Norman L. Kerth (eds.). Pattern Languages in Program Design 2. Addison-Wesley. Liberherr, Karl J. and Ian M. Holland (1989). Assuring Good Style for Object Oriented Programs. IEEE software, September 89. Meyer, Bertrand (1994). Reusable Software: The Base Object Oriented Component Libraries. Prentice Hall. Pascoe, Geoffrey A. (1986). Encapsulators: A New Software Paradigm in Smalltalk-80. Proceedings of OOPSLA 1986, SIGPLAN Notices, vol.21 no.11.
Appendix The appendix contains source code clipped from the wizard’s methods and a sample output of their application. Its purpose is to give the flavor of our definition for a PSL and the form of its outcome.
Source code and output samples of the Observer pattern The routine that performs the modifications listed in Table 2 bears the name: makeClass: observer observeClass: subject controllerClass: controllerClass abstractObserver: absObserver It is a method of class Observer that inherits from class DPWizard. The code of this method and the other methods is presented next to the repeated entries of Table 2: 1.
Modifications to the observer: 1.1. Add an inheritance clause
Related Smalltalk code: self deriveClass: observer fromClass: (absObserver instantiateByTypes: (OrderedCollection with: subject asType)).
resolveFeatureNameConflicts: derivedClass 1.2. Add a suitable number of baseClass: baseClass rename clauses to the previous derivedClass features do: [ :dFeature | inheritance clause (baseClass features select: [ :bFeature | bFeature name = dFeature name]) isEmpty ifFalse: [ self rename: dFeature name inClass: derivedClass inheritedFromType: baseClass asType]]. 1.3. Redefine update(SUBJECT): redefine: oldName inClass: derivedClass 1.3.1. Add redefine clause to inheritedFromClass: baseClass the previous inheritance "If derivedClass inherits from baseClass, add a clause redefine clause to feature named oldName" | inheritClause newFeature |
]. newFeature := derivedClass features atName: oldName. newFeature notNil ifTrue: [(newFeature isRedefined) ifTrue: [(newFeature originClass = baseClass asType) "Redefintion from baseClass already exists" ifTrue: [^ newFeature ] ifFalse: [. newFeature := nil]] ifFalse: [self warning:. newFeature originClass: baseClass asType]] ifFalse: [ "Feature does not exist yet: Copy the feature's stub from baseClass" 1.3.2. Add a routine stub for newFeature := A_ConcreteRoutine update(SUBJECT) redefineFeature: (baseClass features atName: oldName) fromType: baseClass asType. derivedClass features add: newFeature]. ^ newFeature setObserverCreators: observer 1.4. Add creator routines which subjectClass: subject duplicate the originals, with the usingController: controllerType exceptions of an additional abstractObserver: absObserver argument and the additional call statement. "Private: Add to the observer's creation methods a
- 14 -
call for the abstractObserver's creation method:" |newObserverCreators parentCreator Call2ObserverCreator| "Generate creator copies:" newObserverCreators := self getNewObserverCreator: observer toIncludeArg: subject asType. "Add the new creators (consider used names):" self addFeatures: newObserverCreators toClass: observer considerBase: absObserver. "Define the call to the parent's creator:" aCall2ObserverCreator := A_Call featureName: (observer actualFeatureName: parentCreator derivedFrom: absObserver asType) argsColl: (OrderedCollection with: (A_FormalArg name: 's' type: subject asType)). "Add call for the parent's creator on each creator:" newObserverCreators do: [ :creator | creator block addLast: aCall2ObserverCreator ].
2.
Modifications to the subject’s code: 2.1. Add an inheritance clause
self deriveClass: subject fromClass: controllerClass.
2.2. Add a suitable number of rename clauses to the previous inheritance clause Table 3: Sample code clips from the Observer routines deferred class ABSTRACT_OBSERVER[SOME_SUBJECT_CLASS] creation make feature make (subject: SOME_SUBJECT_CLASS) is do subject.attach(current) end; update (subject: SOME_SUBJECT_CLASS) is deferred end end -- class ABSTRACT_OBSERVER Table 4: The ABSTRACT_OBSERVER class is introduced by the Observer routines. The entire class declaration is typeset in bold letters to emphasize the fact it was generated by the wizard.
- 15 -
class EXAMPLE5 inerit ABSTRACT_OBSERVER[SUBJECT_CLASS] rename make as abstract_observer_make redefine update end creation make make_autogen0 feature e6: EXAMPLE6; make
is do !!e6.make end;
make_autogen0 (s: SUBJECT_CLASS) is do !!e6.make; abstract_observer_make(s) end; update (subject: SUBJECT_CLASS) is do -- insert your code here end end -- class EXAMPLE5 Table 5: Class EXAMPLE5 source after becoming a Concrete Observer by the State pattern implementation.. Modifications implanted by the wizard are typeset in bold letters.
Output sample of the State pattern Here we used a simplistic implementation of a matrix class, with only insert and remove routine: class MATRIX creation make feature current_state: ABSTRACT_MATRIX_STATE; make is do !MATRIX_STATE_AS_EXAMPLE4!current_state.make(Current) end change_state (next_state: ABSTRACT_MATRIX_STATE) is do current_state := next_state end; insert (i: INTEGER) is do current_state.insert(i) end; remove (index: INTEGER, a: ARRAY) is do current_state.remove(index,a) end; end -- class MATRIX Table 6: Output sample for a simplistic MATRIX class as a delegator of the State pattern routine/
- 16 -
Implementation of the LoD Here we give the source of two PSL routines that are used to test the Law of Demeter. typesAllowed: aConcreteRoutine ofClass: aClassDeclaration "Return the types allowed by the LoD for method aConcreteRoutine to use:" | result | "Types of attributes:" result := aClassDeclaration features attributesCopy collect: [ :att | att type]. " ^^^^ Note that the law of Demeter itself is violated here" "Types of locals:" result addAll: (aConcreteRoutine localDeclarations collect: [ :local | local type ]). "Types of formal arguments:" result addAll: (aConcreteRoutine formalArgs collect: [ :formalArg | formalArg type ]). "Add the routine return type, if exists:" (aConcreteRoutine type notNil) ifTrue: [result add: (aConcreteRoutine type)]. "Add the class' type:" result add: aClassDeclaration asType. ^ result Table 7: The method that computes the classes that according to the Law of Demeter are “closely related” to routine aConcreteRoutine, i.e., to which calls in aConcreteRoutine are restricted. typesUsed: aConcreteRoutine "Return the Type of the expressions used by qualified calls in aConcreteRoutine:" | calls | "Get the qualified call expressions (unqualified calls use class’ own features):" calls := TreeVisitor dfsAllDescendents: aConcreteRoutine ofRecursiveClass: A_QualifiedCall. ^ calls collect: [ :qualified |
qualified type]
Table 8: The method that computes the types of features that actually appear in calls within the body of aConcreteRoutine.
- 17 -