Friday, December 30, 2011

Factory Method Pattern in Python

Ok, here we go again. This time, I will demonstrate Factory Method Design Pattern  


Let's say we need to design a module to retrieve 4 statistics counters, 
  1. Bytes Transmitted 
  2. Bytes Received
  3. Packet Transmitted
  4. Packets Received
So, we start with creating a base class and 4 inherited classes:


1:  class StatsClass(object):  
2:    
3:    def __init__(self):  
4:      self._counter = 0  
5:      self._name ="Abtract Statistics Class"  
6:        
7:    def Get_Name(self):  
8:      return self._name  
9:      
10:    def Get(self):  
11:      return self._counter;  
12:      
13:    def Reset(self):  
14:      self._counter = 0;  
15:      
16:    def Set(self,counter):  
17:      self._counter = counter;  
18:        
19:    def PollFromHw(self):  
20:      print(self._name, "is poll statistics counter from hardware register")  
21:      
22:    
23:  class StatsBytesTransmitted(StatsClass):  
24:    def __init__(self):  
25:      self._name = "Bytes Transmitted"  
26:      self._counter = 0    
27:    
28:  class StatsBytesReceived(StatsClass):  
29:    def __init__(self):  
30:      self._name = "Bytes Received"  
31:      self._counter = 0  
32:    
33:  class StatsPacketsTransmitted(StatsClass):  
34:    def __init__(self):  
35:      self._name = "Packets Transmitted"  
36:      self._counter = 0  
37:      
38:  class StatsPacketsReceived(StatsClass):  
39:    def __init__(self):  
40:      self._name = "Packets Received"  
41:      self._counter = 0  
42:        


Then, create a Factory Class


1:  # Factory Class  
2:  class AbstractStatsFactory():  
3:    def CreateStats(self,strStats):  
4:      pass  
5:    
6:  class StatsFactory(AbstractStatsFactory):  
7:    def CreateStats(self,strStats):  
8:      if( strStats == "Bytes Received"):  
9:        StatsObj = StatsBytesReceived()  
10:      elif ( strStats == "Bytes Transmitted"):  
11:        StatsObj = StatsBytesTransmitted()  
12:      elif ( strStats == "Packets Transmitted"):  
13:        StatsObj = StatsPacketsTransmitted()    
14:      elif ( strStats == "Packets Received"):  
15:        StatsObj = StatsPacketsReceived()    
16:      else:  
17:        StatsObj = None    
18:    
19:      return StatsObj  




Now, let's put them into use


1:  def Use_StatsObj(StatsObj):   
2:    StatsObj.PollFromHw()  
3:    StatsObj.Set(100)  
4:    print(StatsObj.Get_Name(), "counter is ", StatsObj.Get())    
5:    StatsObj.Reset()  
6:    print(StatsObj.Get_Name(), "counter Reset. Now counter is ", StatsObj.Get())    
7:    
8:    
9:    
10:  strStats = "Bytes Received"  
11:  StatsObj = StatsFactory().CreateStats(strStats)  
12:  Use_StatsObj(StatsObj)  
13:    
14:  strStats = "Bytes Transmitted"  
15:  StatsObj = StatsFactory().CreateStats(strStats)  
16:  Use_StatsObj(StatsObj)  


The output is 
 Bytes Received is poll statistics counter from hardware register  
 Bytes Received counter is 100  
 Bytes Received counter Reset. Now counter is 0  
 Bytes Transmitted is poll statistics counter from hardware register  
 Bytes Transmitted counter is 100  
 Bytes Transmitted counter Reset. Now counter is 0  


No bad so far, though, I am not satisfied.

There is still an if-elif-else statement in the StatsFactory Class. It will be nice if the Factory class knows how many Statistics Objects are there, without explicitly instantiated these concrete objects .


In OO theme, you can use Reflection to do so.  In Python, this is how I implement it


1:  class StatsFactory2(AbstractStatsFactory):  
2:    def CreateStats(self,strStats):  
3:      # Get all subclass  
4:      for childClass in StatsClass.__subclasses__() :  
5:        obj = childClass()  
6:        if( strStats == obj.Get_Name()):  
7:          return obj  
8:    def ListStatsObjNames(self):  
9:      objList = list()  
10:      for childClass in StatsClass.__subclasses__() :  
11:        # print(childClass)  
12:        obj = childClass()  
13:        objList.append(obj.Get_Name())  
14:      return objList   
15:        
16:    


Now, it's time to put the code into test


1:  print("--- now use New Factory --- ")  
2:  strStats = "Bytes Received"  
3:  StatsObj = StatsFactory2().CreateStats(strStats)  
4:  Use_StatsObj(StatsObj)  
5:    
6:  print("Counter can be read: ", StatsFactory2().ListStatsObjNames())  
7:  while(True):  
8:    strStats = input("Which counter do you want to know ?");  
9:    StatsObj = StatsFactory2().CreateStats(strStats)  
10:    if( StatsObj == None ):  
11:      break  
12:    Use_StatsObj(StatsObj)  
13:    


This is the Output


 --- now use New Factory ---   
 Bytes Received is poll statistics counter from hardware register  
 Bytes Received counter is 100  
 Bytes Received counter Reset. Now counter is 0  
 Counter can be read: ['Bytes Transmitted', 'Bytes Received', 'Packets Transmitted', 'Packets Received']  
 Which counter do you want to know ?Bytes Transmitted  
 Bytes Transmitted is poll statistics counter from hardware register  
 Bytes Transmitted counter is 100  
 Bytes Transmitted counter Reset. Now counter is 0  
   


Now, this is beauty part -- let's add two new statistics counters right after the  StatsPacketsReceived 


1:  class StatsFcsError(StatsClass):  
2:    def __init__(self):  
3:      self._name = "Fcs Error"  
4:      self._counter = 0  
5:    
6:  class StatsDiscardPacket(StatsClass):  
7:    def __init__(self):  
8:      self._name = "Discard Packets"  
9:      self._counter = 0    
10:    


Then, run my test code without any change. Voila, the two new classes are automatically recognized and can be used. 


 --- now use New Factory ---   
 Bytes Received is poll statistics counter from hardware register  
 Bytes Received counter is 100  
 Bytes Received counter Reset. Now counter is 0  
 Counter can be read: ['Bytes Transmitted', 'Bytes Received', 'Packets Transmitted', 'Packets Received', 'Fcs Error', 'Discard Packets']  
 Which counter do you want to know ?Discard Packets  
 Discard Packets is poll statistics counter from hardware register  
 Discard Packets counter is 100  
 Discard Packets counter Reset. Now counter is 0  
 Which counter do you want to know ?  




Beautiful, isn't it ?

Wednesday, December 28, 2011

Decorator Pattern in Python

I re-read Head-First Design Pattern lately. And decide to implement Decorator Design Pattern in Python.


The following is based on the example from the book

  1. The existing code has base class Beverage, and subclass DarkRoast, Espresso. These code has been fully tested and in production (line 1-21). 
  2. Three months later, your boss want to have more varieties of drink, .e.g Whip Cream, Vanilla. So, you need to write more code to support that.
  3. It's best to use Decorator Design Pattern, as it obeys the OCP(Open-Closed) Principle. (line 24-51)
  4. Mocha, WhipCream, Vanilla are all decorator class.


Source Code

1:  class Beverage:  
2:    """ beverage class """  
3:    def __init__(self):  
4:      self._desc = "Abstract Drink"  
5:      self._cost = 0.0  
6:      
7:    def get_cost(self):  
8:      return self._cost  
9:      
10:    def get_desc(self):  
11:      return self._desc  
12:      
13:  class DarkRoast(Beverage):  
14:    def __init__(self):  
15:      self._cost = 3.5  
16:      self._desc = "Dark Roast ($" + str(self._cost) + ")"  
17:          
18:  class Espresso(Beverage):  
19:    def __init__(self):  
20:      self._cost = 3.0  
21:      self._desc = "Espresso ($" + str(self._cost)+ ")"  
22:    
23:    
24:  # Design Pattern   
25:  # Abstract Decorator  
26:  class Condiments(Beverage):  
27:    def __init__(self):  
28:      self._desc = "Abstract Condiments class"  
29:      self._cost_condiment = 0.0      
30:      
31:  class Mocha(Condiments):  
32:    def __init__(self, beverage):  
33:      self._cost_condiment = 1.0  
34:      self._beverage = beverage;      
35:      self._desc = "Mocha($"+ str(self._cost_condiment)+ ") " + self._beverage.get_desc()   
36:      self._cost = self._cost_condiment + self._beverage.get_cost()   
37:        
38:  class Vanilla(Condiments):  
39:    def __init__(self, beverage):  
40:      self._cost_condiment = 0.6  
41:      self._beverage = beverage;      
42:      self._desc = "Vanilla($"+ str(self._cost_condiment)+ ") " + self._beverage.get_desc()   
43:      self._cost = 0.6 + self._beverage.get_cost()   
44:           
45:    
46:  class WhipCream(Condiments):  
47:    def __init__(self,beverage):  
48:      self._cost_condiment = 0.4  
49:      self._beverage = beverage;      
50:      self._desc = "WhipCream($"+ str(self._cost_condiment)+ ") " + self._beverage.get_desc()   
51:      self._cost = 0.4 + self._beverage.get_cost()   
52:        
53:  ########################################################################################3    
54:    
55:    
56:  b = DarkRoast()  
57:  print(b.get_desc(), "Cost is", b.get_cost())      
58:    
59:  b = Mocha(DarkRoast())  
60:  print(b.get_desc(), "Cost is", b.get_cost())    
61:    
62:  b = Mocha(Espresso())  
63:  print(b.get_desc(), "Cost is", b.get_cost())    
64:    
65:  b = Vanilla(DarkRoast())  
66:  print(b.get_desc(), "Cost is", b.get_cost())   
67:    
68:  b = Vanilla(Mocha(DarkRoast()))  
69:  print(b.get_desc(), "Cost is", b.get_cost())   
70:    
71:  b = WhipCream(Mocha(DarkRoast()))  
72:  print(b.get_desc(), "Cost is", b.get_cost())   
73:    
74:    
75:  b = Vanilla(WhipCream(Mocha(DarkRoast())))  
76:  print(b.get_desc(), "Cost is", b.get_cost())   
77:    


Output

 Dark Roast ($3.5) Cost is 3.5  
 Mocha($1.0) Dark Roast ($3.5) Cost is 4.5  
 Mocha($1.0) Espresso ($3.0) Cost is 4.0  
 Vanilla($0.6) Dark Roast ($3.5) Cost is 4.1  
 Vanilla($0.6) Mocha($1.0) Dark Roast ($3.5) Cost is 5.1  
 WhipCream($0.4) Mocha($1.0) Dark Roast ($3.5) Cost is 4.9  
 Vanilla($0.6) WhipCream($0.4) Mocha($1.0) Dark Roast ($3.5) Cost is 5.5  


Why not using Python built-in Decorator ?

This is good question. My reasons are

  1. You have to modify the existing code. In the source code above, you have to add @decorator in DarkRoast or Espresso class. With Decorator Design Pattern, you don't have to modify existing code. 
  2. You cannot undo the @decorator during run-time. Once it's there, it's there forever. If you use Decorator Design Pattern, you still can enjoy the original espresso. :)