Object-Orientation and Design Patterns

Introduction to OO concepts


Eric Kerfoot, King's College London, August 2018

  • Slides cover basic introduction to the object-orientation programming paradigm
  • Topics to cover:
    • Object definition
    • Classes and object members
    • Interfaces
    • Inheritance
    • Design patterns
  • Python is the main focus language but concepts are universal

What is an Object?

  • Object-orientation is centered on encapsulating data with operations
  • Objects are pieces of data with associated routines which manipulates it
  • OO programs are composed of many objects aggregating together to form structures and cooperate in implementing behaviour
  • Contrast this with imperative languages like C or Fortran
  • Data is defined in types which only describe data fields
  • Routines defined separately which use these types but relationship is loose
  • Data and routines together define an abstract data type (ADT)
struct Dimension { 
    int width, height; 
};

void init(struct Dimension* d, int w, int h);
int area(struct Dimension* d);
  • Abstract in that the definitions for operations are not defined, only declared
  • Clients can use this ADT through the declaration without needing to know implementation details
  • Relationship between data type and routines is loose and implicit
  • Definition cannot (or at least not easily) be changed to modify behaviour or adapt existing code to new situations
  • Object-orientation aims to make the connection between data and code explicit while preserving abstraction
  • Objects are instances of ADTs with additional behaviour added, eg. inheritance
  • C++ equivalent ADT defined as a class:
class Dimension {

private:
  int width, height;

public:
  Dimension(int w, int h);
  virtual int area();
};
  • Maintains abstraction but defines data values with routines which implicitly access them
  • In Python:
In [8]:
class Dimension(object):
    
    def __init__(self,w,h):
        self.width=w   # create attributes here
        self.height=h
        
    def area(self):
        return self.width*self.height
  • Defines same structural type although abstraction is lost
  • Still defines the equivalent interface: a routine for creating and an area() operation
  • Objects are created by instantiating a class:
In [9]:
d=Dimension(10, 20)

  • The instance object will have the members defined by the class
  • Variable d references an instance of Dimension, this object has the type Dimension as well as object
  • Multiple instances of a class can be created
  • Each instance is an independent object with unique identities and distinct members:
In [11]:
d1=Dimension(15, 30)
print(id(d), id(d1)) # id() returns object's identifier (ie. address)
81465528 81468888

  • Objects d and d1 store different values and exist in different sections of memory, but share a common structure
  • Important to note in Python that variables are names for objects, assignment changes what object the name refers to:
In [12]:
d1=d
print(id(d),id(d1))
81465528 81465528

  • d and d1 now name the same object
  • Original object d1 named has not been changed, but is now inaccessible and liable to be collected
  • Components of objects are members (or features) falling in these general categories:

    • Attributes: named data values stored in the object
    • Methods: routines associated with the object and which can refer to the object by name
    • Constructor: special method used to setup a new object's state at the point of instantiation
  • Other languages have different types, usually special forms of these

  • Members of objects are accessed with the dot notation expression: <object>.<member>
In [13]:
print(d.width, d.height) # access
d.width=12 # assign to a member changes the stored value
print(d.width, d.height)
print(d.area) # methods can be accessed without being called
10 20
12 20
<bound method Dimension.area of <__main__.Dimension object at 0x0000000004DB10B8>>
  • One important name that exists in all methods is self
  • Refers to the object whose method was called (the receiver or callee object)
  • Value is set within scope of call
In [15]:
#def area(self): # recall the definition of area()
#    return self.width*self.height

print(d.area()) # self becomes d within scope of call
240
  • Methods define operations associated with objects
  • Relationship is close explicit through the dot notation and self value
  • Attributes and methods also define the interface for interacting with objects
  • A caller (or client) object only needs the interface information to interact with an object
  • Eg. area() defines its part of the interface as a function accepting no arguments and returning a number
  • An attribute represents a value that can be queried or assigned to
  • The interfaces for methods and attributes together form the object interface for their associated objects
  • A class defining a different area() method with the same interface:
In [17]:
class Dimension3(object):
    def __init__(self,w,h,d):
        self.width=w
        self.height=h
        self.depth=d
        
    def area(self):
        return 2*(self.width*(self.height+self.depth)+self.height*self.depth)
    
d3=Dimension3(10,12,15)
print(d3.area())
900
  • Methods Dimension.area() and Dimension3.area() provide the same interface
  • Interface abstracts away the details of implementation
  • Clients need no more information than this to use either:
In [18]:
def calcsquare(obj):
    area=obj.area()
    return int(area**0.5)

print(calcsquare(d)) # d is instance of Dimension
print(calcsquare(d3)) # d3 is instance of Dimension3
15
30
  • Easy to implement in Python since calcsquare() doesn't check that obj fulfills the needed interface, tries to call area() and raises an exception if something goes wrong
  • Called duck typing since if it looks like a duck, and quacks like a duck, it ain't a moose
  • Static typed languages (C++) require interface to be declared:

    class AreaInterface { 
          public: virtual float area()=0; 
    };
    float calcsquare(const AreaInterface& obj) {...}
    

Inheritance

  • Inheritance allows a class to be defined in terms of others
  • Inheriting type (the subtype or subclass) receives all members of the type being inherited (the supertype or superclass)
  • Allows a class to acquire member definitions without having to redefine them
  • Prevents reinventing the wheel in many cases, important component to reuse
In [19]:
class Rect(Dimension):
    def __init__(self,x,y,w,d):
        Dimension.__init__(self,w,d)
        self.x=x
        self.y=y
        
    def farCorner(self):
        return (self.x+self.width,self.y+self.height)
    
r=Rect(4,4,12,10)
print(r.x, r.y, r.width, r.height)
print(r.area(), r.farCorner())
print(isinstance(r,Rect), isinstance(r,Dimension))
4 4 12 10
120 (16, 14)
True True
  • Rect inherited members from Dimension and introduced new ones
  • It has an area() method which functions as before
  • Instances of Rect are also instances of Dimension; this is one aspect of polymorphism
  • Classes can inherit from multiple supertypes, conflicts can result if multiple inherited members share names
  • Inheritance is more than copy-pasting member definitions (or at least should be)
  • Subtype is a specialization of the supertype, it represents a related concept that is more refined or specific to a particular context
  • Classes are meant to represent a single unified concept in a program (Dimension for a 2D size definition, file for a file object, list, tuple, or dict for data structures, etc.)
  • Subtypes are the same concept but with some added notion or constraint which makes them more specific
  • Principle of Substitution states that substituting an instance of a type in an algorithm with an instance of a subtype should not affect the algorithm's correctness
  • Algorithm may behave differently depending on the instance, but correctness should not be compromised
  • Eg. calcsquare() should function correctly with an instance of Dimension or one of Rect
  • Rect is obviously substitutable since it only adds members.
  • Method overriding allows a subclass to replace an inherited method with a new one
  • Inherit and new methods will call to this new method
  • Useful in modifying the behaviour of other methods that are rely on those being replaced
  • In Python only the name is relevant to what method is overridden, in other languages other rules apply
In [20]:
class Dimension(object):
    def __init__(self,w,h):
        self.width=w
        self.height=h
        
    def midpoint(self):
        return (self.width*0.5,self.height*0.5)
    
    def name(self): # prints name of class and midpoint
        return '%s, mid = %r'%(self.__class__.__name__,self.midpoint())
    
class Rect(Dimension):
    def __init__(self,x,y,w,d):
        Dimension.__init__(self,w,d)
        self.x=x
        self.y=y  
        
    def midpoint(self):
        return (self.x+self.width*0.5,self.y+self.height*0.5)
In [21]:
d=Dimension(10,15)
r=Rect(5,10,10,15)
print(d.name())
print(r.name())
Dimension, mid = (5.0, 7.5)
Rect, mid = (10.0, 17.5)
  • When name() is called on the instance of Rect this method is called, even though name() itself is not overridden
  • Demonstrates that an inherited method is not hard wired to the methods defined in the superclass
  • Reuse mechanism since types can be defined expecting methods to be overridden to adapt them to other uses
  • Common pattern is to define abstract methods which do nothing with algorithms relying on their behaviour in subtypes
  • Principle of substitution states subtypes should be defined in a semantically substitutable way, otherwise clients reliant on the expected behaviour will not necessarily operate correctly
In [22]:
class AbstractList(object):
    def size(self): pass # size of list
    def get(self,i): pass # get i'th element
    def contains(self,v): # True if v in list
        return any(self.get(i)==v for i in range(self.size()))
    
class ConcreteList(AbstractList):
    def __init__(self,vals):
        self.vals=vals
    def size(self):
        return len(self.vals)
    def get(self,i):
        return self.vals[i]
    
cl=ConcreteList(['Graham','John','Eric','Terry'])
print(cl.contains('Eric'))
print(cl.contains('Michael'))
True
False
  • Other OO features not present/needed in Python:
    • Method Overloading: defining multiple methods/constructors with the same name
    • Access Modifiers: added component of declarations control who can access or mutate the member
    • Variable Polymorphism: instances of subtypes to be assigned to variables having the supertype. Eg.:
      Dimension *d=new Rect(5,10,10,15);
      
    • Interfaces: distinct type defining only method interface
    • Templates: C++ parameterized types where internal definition relies on a changeable type argument

Design Patterns

  • Medium-level architectural idiom which captures some common idiom or useful organizational/creational notion in an object-oriented system
  • No implementation of a pattern is like another, definition is deliberately vague because patterns are inherently adaptable
  • Many patterns rely on static typing so don't appear in Python
  • Others are very common and built into the Python language and library

Subject-Observer

  • Defines a relationship between subject objects and observer objects
  • Observer objects register their interest in the subject, when a particular event occurs the subject notifies the observers
  • Allows objects to keep track of when state changes and channel the process for reacting to change through a specific mechanism
  • Eg. button in a UI is a subject, press button and observers react
In [23]:
class AbstractSubject(object):
    def __init__(self):
        self.observers=set() 
        
    def addObserver(self,o):
        self.observers.add(o)
        
    def removeObserver(self,o):
        self.observers.remove(o)
        
    def notifyObservers(self):
        for o in self.observers:
            o.notify(self)
            
class AbstractObserver(object):
    def notify(self,subject): pass
In [25]:
class NameSubject(AbstractSubject):
    def __init__(self,n):
        AbstractSubject.__init__(self)
        self.name=n
    
    def setName(self,n):
        self.name=n
        self.notifyObservers()

class NameObserver(AbstractObserver):
    def __init__(self,n):
        self.name=n
        
    def notify(self,subject):
        print(self.name,'saw that',subject.name,'changed names')
        
s=NameSubject('Terry')
o1=NameObserver('John')
s.addObserver(o1)
o2=NameObserver('Michael')
s.addObserver(o2)
s.setName('Graham')
John saw that Graham changed names
Michael saw that Graham changed names

Iterator

  • Object which traverses a data structure by producing successive values upon request
  • Abstracts away how traversing works
  • Common interface for multiple types of traversable structures
  • Ubiquitous in Python, used most commonly in for loops, built into language in other ways
In [26]:
r=[0,1,2,3,4] # data structure
print(r)
it=iter(r) # get an iterator from the structure
print(it)
print(next(it)) # get the first value
print(next(it)) # get the next value after that
[0, 1, 2, 3, 4]
<list_iterator object at 0x0000000004DCFB38>
0
1
  • Relationship between the data structure and its iterators is one-to-many
  • Allows a single structure to be traversed by multiple iterators which may do so in different ways
  • In Python, an object is an iterator if it implements a __next__() method which returns the next value in its notional sequence, and raises StopIteration when exhausted
  • An object is iterable if it implements a method __iter__() which returns an iterator
  • The subject of a for loop must be iterable
In [29]:
class mylistiterator(object):
    def __init__(self,lst):
        self.pos=0
        self.lst=lst
    def __next__(self): # returns successive values
        if self.pos<len(self.lst):
            self.pos+=1
            return self.lst[self.pos-1]
        raise StopIteration # indicates no more items
        
it=mylistiterator([1,2,3,4,5])
print(next(it), next(it), next(it), next(it), next(it))

try:
    print(next(it)) # try to get more items
except StopIteration:
    print('No more')
1 2 3 4 5
No more
In [30]:
class mylist(object):
    def __init__(self,lst):
        self.lst=lst
    def __iter__(self):
        return mylistiterator(self.lst)
    
m=mylist([1,2,3,4,5])

for i in m:
    print(i)
1
2
3
4
5
  • The for statement given previously is roughly equivalent to the following:
In [31]:
it=iter(m)
while True:
    try:
        i=next(it)
    except StopIteration:
        break
        
    print (i)
1
2
3
4
5

Template Method

  • An algorithm method is defined in terms of abstract methods declared along side
  • Expectation is that the methods are implemented in a subtype
  • Allows the skeleton of an algorithm to be defined which can be adapted to many uses through subtyping and overriding
  • Saw this in action with AbstractList
In [32]:
class AbstractAlgorithm(object):
    def doSomething(self): pass
    
    def doSomethingElse(self): pass
    
    def doAlgorithm(self):
        self.doSomething()
        self.doSomethingElse()
        # other actions...

Conclusion

  • Object-orientation is a programming paradigm which emphasizes the association between data and operations, modularity, abstraction, reusability, and genericity
  • Objects represent individual concepts in software systems, inheritance allows these concepts to be specialized for specific contexts or applications
  • OO systems of composed of many objects associated together in cooperative relationships
  • Design patterns represent a more formal and systematic way of describing common and useful idioms in a way that is generic enough to be adapted to specific problems