2.2 Framework Techniques
Overview best practice of inheritance Interlude: Refactoring avoiding copy/paste reuse planning for reuse: self-sends abstract classes and template methods specialisation interfaces Interlude: Refactoring best practice of instantiation factory method abstract factory
Overview best practice of cooperation elimination of class-tests elimination of case-statements double dispatch technique badly placed code
Best Practice of Inheritance avoiding copy/paste reuse planning for reuse: self-sends abstract classes and template methods specialisation interfaces
Reusing the LAN New requirement: “Providing logging (or tracing) facilities: A message is printed, identifying the node and the packet, each time a packet is sent from a node”
Reuse: Copy/Paste Approach LAN System Node accept:thePacket self send:thePacket send:thePacket self nextNode accept:thePacket Logging LAN System Node accept:thePacket self send:thePacket send:thePacket Transcript show: …. self nextNode accept:thePacket copy/paste directly changing code
Copy/Paste Reuse The reused class and the original are not related anymore difficult to maintain: bug fixes and upgrades need to be propagated manually over all duplicates Proliferation of versions code duplication leads to even more code duplication, especially with reuse
Copy/Paste Reuse Avoid copy/paste reuse Avoid direct editing of reusable classes avoid editing of classes in libraries reuse existing classes by using inheritance and method overriding
OOP Solution create subclass LoggingNode of class Node override ‘send’ method
BUT? + Separate discussion (design patterns) How to combine ? Node Node + LoggingNode Printserver Workstation Separate discussion (design patterns) How to combine ? - multiple inheritance - mixin classes
Best Practice of Inheritance avoiding copy/paste reuse planning for reuse: self-sends abstract classes and template methods specialisation interfaces
Recap: self and super refer to the receiver of the currently executing method differ in where method-lookup starts self x super y A x B x y C x y
self in the Node class Node Node name name accept(p:Packet) send(p:Packet) send(p:Packet) SUPER when sending #accept: to an instance of Node LoggingNode send (p : Packet) when sending #accept: to an instance of LoggingNode
Planning for reuse: self-sends can be reused for logging same “external” functionality (send is protected !) cannot be reused for logging
Designing Classes for Reuse Narrow interface principle behavior of a class should be based on a minimal set of methods that can be overridden distinguish “core” behavior/methods from “peripheral” behavior/methods
Best Practice of Inheritance avoiding copy/paste reuse planning for reuse: self-sends abstract classes and template methods specialisation interfaces
Reusing the LAN New requirement: “Extend the LAN system so that documents of different types (Postscript, ASCII) can be printed on a corresponding printer”
Remember ...
Straightforward Approach Printserver subclass: #AsciiPrinter accept: thePacket thePacket addressee = self name and: [ thePacket contents isAscii ] ifTrue: [ self print: thePacket ] ifFalse: [ super accept: thePacket] print: thePacket Transcript show: ‘Packet with contents “, thePacket contents printString Printserver subclass: #PostscriptPrinter accept: thePacket thePacket addressee = self name and: [ thePacket contents isPostscript ] ifTrue: [ self print: thePacket ] ifFalse: [ super accept: thePacket] print: thePacket Transcript show: ‘Packet with contents “, thePacket contents interpretString
Problems may lead to classes that are hard to understand subtle interaction between self and super very deep narrow inheritance hierarchies that are hard to understand Inhibits further reuse and maintenance no abstraction is made of the commonalities between ASCII and Postscript printers
OOP Solution template method Node subclass: #AbstractPrintserver accept: thePacket self isDestinationFor: thePacket ifTrue: [ self print: thePacket ] ifFalse: [ super accept: thePacket] print: thePacket self subclassResponsibility isDestinationFor: thePacket ^thePacket addressee = self name abstract method concrete method
Abstract classes
Reuse Abstract classes AbstractPrintserver subclass: #AsciiPrinter print: thePacket Transcript show: ... isDestinationFor: thePacket ^super isDestinationFor: thePacket and: [ thePacket contentx isAscii ] AbstractPrintserver subclass: #PostscriptPrinter print: thePacket Transcript show: ... isDestinationFor: thePacket ^super isDestinationFor: thePacket and: [ thePacket contentx isPostscript ]
Best Practice of Inheritance avoiding copy/paste reuse planning for reuse: self-sends abstract classes and template methods specialisation interfaces
Which methods to override ? default methods e.g. isDestinationFor: abstract methods e.g. print: extending methods with new behaviour what about overriding template methods ? under what conditions can we override accept ?
Specialisation Interface AbstractPrintServer accept(p:Packet) {*accept, isDestinationFor, print} isDestinationFor(p:Packet) print(p:Packet) client interface: documents the messages that can be sent interface to message passing clients encapsulates the implementation from message passing clients specialisation interface: documents the messages that are sent from an object to itself interface to inheriting clients encapsulates the implementation from inheriting clients
Interpreting the Specialisation Interface (1) Object subclass: #Node Node methodsFor: 'input-output' accept:thePacket self send:thePacket Node methodsFor: 'private' send:thePacket self nextNode accept:thePacket implementation must invoke send Node name accept(p:Packet) {send} send(p:Packet) {nextNode} nextNode Specialisation interfaces “reify” part of the implementation - they link design to implementation Different parts of the implementation can be reified - which parts are interesting to inheritors ? - only essential parts should be documented
Interpreting the Specialisation Interface (2) specialisation clause = UML constraint AbstractPrintServer accept(p:Packet) {*accept, isDestinationFor, print} isDestinationFor(p:Packet) print(p:Packet) super send Specialisation interface = set of all specialisation clauses
Specialisation Interfaces Reify Only Part of the Implementation Object subclass: #Node Node methodsFor: 'input-output' accept:thePacket false ifTrue: [self send:thePacket] implementation must invoke send Node name accept(p:Packet) {send} send(p:Packet) {nextNode} nextNode Different interpretations are possible source level: “self send:...” occurs somewhere in method body run time: “send” must be sent to self in any possible method execution we take the simpler source level interpretation
Using Specialisation Interfaces Terminology for talking about abstract classes and method overriding Designing abstract classes Assess what methods can be overridden
Terminology AbstractPrintServer accept(p:Packet) {*accept, isDestinationFor, print} isDestinationFor(p:Packet) print(p:Packet) A template method is a method that refers to an abstract method in its specialisation interface Core methods are methods with an empty specialisation interface ...
Designing Abstract Classes AbstractPolygon sidesDo(aBlock) circumference {sidesDo} Square PolyLine sidesDo(aBlock) sidesDo(aBlock) circumference {}
Overriding Methods AbstractPolygon UML constraints sidesDo(aBlock) circumference {sidesDo} {Concretisation = sidesDo} {Concretisation = sidesDo} {Coarsening = circumference(-sidesDo)} Square PolyLine sidesDo(aBlock) sidesDo(aBlock) circumference {}
Overriding & introducing methods Node accept(p:Packet) {send} send {Refinement = accept(+isDestinationFor,+print)} {Extension = isDestinationFor, print} AbstractPrintServer accept(p:Packet) {send, isDestinationFor, print} isDestinationFor(p:Packet) print(p:Packet) send
Documenting Inheritance Concretisation: overriding abstract methods with concrete methods Abstraction: overriding concrete methods with abstract methods Refinement: adding message names to the specialisation interface Coarsening: removing message names from the specialisation interface Extension: adding new methods to the client interface Cancellation: removing methods from the client interface 19
Specialisation Interfaces documenting the interface for inheritors basic terminology for inheritance of abstract classes basis for reasoning about inheritance OO is powerful, it has to be used with care.
Interlude: Refactoring Refactoring = reorganization plan for a system to improve reuse A refactoring is behavior preserving A refactoring has a precondition Some refactorings are based on class invariants Refactoring into object-oriented frameworks turning an OO application into a framework
Why Refactoring? Abstract classes and frameworks are generalizations People think concretely, not abstractly Abstractions are found bottom up, by examining concrete examples
Why Refactoring? Generalization proceeds by: finding things that are given different names but are really the same (and thus renaming them) parameterizing to eliminate differences breaking large things into small things so that similar components can be found
Refactoring: Abstraction concrete class A concrete class B abstraction concrete class B concrete class A abstract class X
Refactoring: Abstraction Creating an Abstract Superclass Create a common superclass Make method signatures compatible Add method signatures to the superclass Make method bodies compatible Make instance variables compatible Move instance variables to the superclass Migrate common code to the abstract superclass
Refactoring: Abstraction AbstractPrinter PostscriptPrinter AsciiPrinter AsciiPrinter PostscriptPrinter
Best Practice of Instantiation factory methods abstract factory
Reusing the LAN How to instantiate objects in a reusable, maintainable and adaptable way?
Straightforward Approach Object subclass: LANExample intialize ws1 = Workstation new. ws2 = Workstation new. ps1 = PostscriptPrinter new
Problems names of classes are hardcoded objects are created throughout the whole system what if subclasses are introduced? where in the code are objects created? how to instantiate objects of subclasses without editing existing classes?
OOP Solution abstract the object-creation process do not explicitly create objects inside a method (e.g. do not use ‘new’) provide hooks for a class to create the appropriate objects define methods that do nothing else but instantiating an object = factory methods
Factory methods Object subclass: LANExample intialize ws1 = self newWorkstation. ws2 = self newWorkstation. ps1 = self newPostscriptPrinter newWorkstation ^Workstation new newPostscriptPrinter ^PostscriptPrinter new
Consequences use inheritance and method overriding to instantiate objects of other classes class names are only hard coded in predefined places system is freed of creation code LanExample subclass: OtherLANExample newWorkstation ^LoggingWorkstation new newPostscriptPrinter ^LoggingPostscriptPrinter new
Best Practice of Instantiation factory methods abstract factory
Reusing the LAN How to ensure that a system consistently uses only one family of related objects?
Not So Straightforward Approach use factory methods! Object subclass: LANExample intialize ws1 = self newWorkstation. ws2 = self newWorkstation. ps1 = self newPostscriptPrinter newWorkstation ^Workstation new newPostscriptPrinter ^PostscriptPrinter new LanExample subclass: OtherLANExample newWorkstation ^LoggingWorkstation new newPostscriptPrinter ^LoggingPostscriptPrinter new
Problems what if we forget to override a method? objects will be mixed there is no enforcement factory methods may or may not be overridden factory methods rely on the programmer to not make mistakes
Abstract Factory define a class which is responsible for creating the right objects class consists of nothing but factory methods delegate creation of objects to an instance of this newly defined class
Abstract Factory Object subclass: AbstractLANFactory newWorkstation ^self subclassResponsibility newPostscriptPrinter AbstractLANFactory subclass: #DefaultLANFactory newWorkstation ^Workstation new newPostscriptPrinter ^PostscriptPrinter new AbstractLANFactory subclass: #LoggingLANFactory newWorkstation ^LoggingWorkstation new newPostscriptPrinter ^LoggingPostscriptPrinter new
Consequences abstract the object-creation process provide hooks for a class to create the appropriate objects no risk of mixing two families of objects objects are created in only one place improves maintainability, readability, reusability, ...
But ... how to instantiate Factory objects? use factory method use Singleton design pattern (see later)
Refactorings for factory methods spot creation code inside ordinary methods move this code inside its own (factory) method replace code in original method by invocation of factory method
Refactorings for abstract factory spot factory methods that always get overridden together by subclasses move these methods into a new class replace invocation of factory methods by invocation of appropriate method on factory object
Best Practice of Cooperation elimination of class-tests elimination of case-statements double dispatch technique Law of Demeter badly placed code
Reusing the LAN New requirement: “Providing more than one destination for a packet allows the sender of a packet to print a document either on #printer1 or #printer2, depending on which printer is first encountered in the local area network.” Remember: #printer1 and #printer2 are Smalltalk symbols that are used for identifying nodes in the LAN.
Straightforward Approach Node subclass: #AbstractPrintserver accept: thePacket self isDestinationFor: thePacket ifTrue: [ self print: thePacket ] ifFalse: [ super accept: thePacket] isDestinationFor: thePacket ^thePacket addressee = self name or: [ thePacket addressee = #* ] wildcard symbol
Problems Solution is not very general what about #prin* (any printer name that starts with #prin Solution is brittle towards change based on a convention (e.g. wildcards) adding a new kind of address requires editing the AbstractPrintserver class (and possibly others)
OOP Solution Objectify addresses
OOP Solution Delegate responsibilities Does this work ?
smalltalk recap: polymorphism objects respond to messages different objects can respond differently to the same message KWAAK Speak WOEF Speak
Polymorphic Addresses isDestinationFor: WildcardAddress isDestinationFor: Address AbstractPrintserver accept: Packet print:Packet isDestinationFor:anAddress ^true NodeAddress id:Symbol isDestinationFor: Address = Address isDestinationFor:anAddress ^self = anAddress
Planning for Reuse: Delegation can be reused for different addressing schemes (wildcards, domains, ...) same “external” behavior cannot be reused for different addressing schemes
Delegation of Responsibilities avoids case statements (especially over class types) Example: Address Printserver should not depend on representation of addresses Printserver must delegate address equality checking
Delegation of Responsibilities Names are very important Wrong ASCIIPrinter printAsciiFile PostscriptPrinter printPostsciptFile Right ASCIIPrinter printFile PostscriptPrinter printFile Designing good interfaces is important objects with same interfaces can be substituted
Reusing the LAN New requirement: “Extend the LAN system so that packets can be broadcasted.”
Straightforward Approach
Problems bad coding practice Inhibits further reuse and maintenance class tests should be avoided needs modification of all node classes than can serve as addressee Inhibits further reuse and maintenance what about packets that count the number of hops?
OOP Solution template method abstract method
Class Collaboration Design for a set of classes that collaborate to accomplish a certain task emphasis on the interaction between class instances Possibly consisting of abstract classes template and abstract methods need not reside in the same class Class collaboration can be reused all abstract classes must be filled in with concrete ones first
Reusing Class Collaboration collaboration instance: printing collaboration instance: broadcasting
Reusing the LAN New requirement: “Providing a Document class which represents documents to be printed on a printer.”
Straightforward Approach Object subclass: #Document instanceVariables: ‘contents’ printOn: aPrinter aPrinter print: (self newPacketWithContents: self contents) newPacketWithContents: contents ^Packet new; contents: contents
But ... contents can be ASCII as well as Postscript as well as other types but ... ASCII documents cannot contain figures Postscript and PDF documents can
What is often seen Object subclass: Document instanceVariables: ‘contents’ includeFigure: aFigure self isAsciiDocument ifTrue: [ Transcript show: ‘Error ...’ ] self isPostscriptDocument ifTrue: [ ... ] self isPDFDocument printOn: aPrinter ....
OOP Solution Object subclass: #AbstractDocument instanceVariables: ‘contents’ includeFigure: aFigure self subclassResponsibility AbstractDocument subclass: #AsciiDocument includeFigure: aFigure Transcript show: ‘Error ...’ AbstractDocument subclass: #PostscriptDocument includeFigure: aFigure “code to include the figure”
Refactoring for Specialization Motivation : the design of a framework can be improved by decomposing a large, complex class into several smaller classes the complex class usually embodies both a general abstraction and several different concrete cases that are candidates for specialization
Refactoring: Specialization Specialize a class by adding subclasses corresponding to the conditions in a conditional expression: choose a conditional whose conditions suggest subclasses (this depends on the desired abstraction) for each condition, create a subclass with a class invariant that matches the condition copy the body of the condition to each subclass, and in each class simplify the conditional based on the invariant that is true for the subclass specialize some (or all) expressions that create instances of the superclass
Refactoring: Specialization Disk Management for MSDOS Class Invariant Disk ... copyDisk formatDisk Disk Management for MSDOS+MAC Disk ... copyDisk formatDisk Disk Management for MSDOS+MAC Disk disktype ... copyDisk formatDisk formatDisk self diskType = #MSDOS ifTrue: [ .. code1 ..]. self diskType = #MAC ifTrue: [ .. code2 ..]. MSDOSDisk ... copyDisk formatDisk MACDisk ... copyDisk formatDisk
Best Practice of Cooperation elimination of class-tests elimination of case-statements double dispatch technique badly placed code
Reusing the LAN New requirement: “Make sure documents of a certain type are printed on a corresponding printer.”
Straightforward Approach Object subclass: #AbstractDocument instanceVariableNames: ‘contents’ printOn: aPrinter self subclassResponsibility AbstractDocument subclass: #PostscriptDocument printOn: aPrinter aPrinter class = PostscriptPrinter ifTrue: [ aPrinter print: self newPacketWithContents: self contents ] AbstractDocument subclass: #ASCIIDocument printOn: aPrinter aPrinter class = ASCIIPrinter ifTrue: [ aPrinter print: self newPacketWithContents: self contents ]
Problems class-testing should be avoided what if a subclass of ASCIIPrinter is introduced? what if a printer is introduced that can print both ASCII and Postscript?
Observation the way a document should be printed does not only depend on the type of the document, but also on the type of the printer! delegation by itself cannot solve this problem
Problem late binding polymorphism only takes into account the type of the receiver single dispatch solution to our problem should also take into account the type of the argument double dispatch!
OOP Solution AbstractPrintserver subclass: #PostscriptPrinter printPostsciptDocument: aPSDocument Transcipt show: aPSDocument contents interpret printAsciiDocument: anASCIIDocument Transcipt show: ‘Error’ AsbtractPrintserver subclass: #ASCIIPrinter printPostsciptDocument: aPSDocument Transcipt show: ‘Error’ printAsciiDocument: anASCIIDocument Transcipt show: anASCIIDocument contents print AbstractDocument subclass: #ASCIIDocument printOn: aPrinter aPrinter printASCIIDocument: self AbstractDocument subclass: #PostscriptDocument printOn: aPrinter aPrinter printPostscriptDocument: self
Consequences results in more reusable, adaptable and maintainable code avoids class-testing avoids case-statements
Best Practice of Cooperation elimination of class-tests elimination of case-statements double dispatching badly placed code
Reusing the LAN New requirement: “Return the number of characters after a document is printed.”
Straightforward Approach AddressableNode subclass: #AbstractPrintserver countChars: aDocument ^aDocument contents numberOfChars AbstractPrintserver subclass: #PostscriptPrinter printPostsciptDocument: aPSDocument Transcipt show: aPSDocument contents interpret. ^self countChars: aPSDocument AbstractPrintserver subclass: #ASCIIPrinter printAsciiDocument: anASCIIDocument Transcipt show: anASCIIDocument contents print. ^self countChars: anASCIIDocument
Problems countChars method does not use the receiver no instance variables are used no instance methods are called indication that the method is defined on the wrong class
OOP Solution AbstractDocument subclass: #ASCIIDocument printOn: aPrinter aPrinter printASCIIDocument: self. ^self contents countChars AbstractDocument subclass: #PostscriptDocument printOn: aPrinter aPrinter printPostscriptDocument: self. ^self contents countChars