Abstract Classes and Factory Design Pattern in Python

Posted on Dec 19, 2013

Abstract Classes are one of the most useful and important concepts in Object Oriented Programming. I’ll attempt to illustrate their usefulness, and their usage in Python 2.7 with the following (seemingly contrived) example:

Let us say, you want/have to implement posting updates on Facebook using Python. Your code might look something like this:

1# Attempt 0: Bad code.
2
3def facebook_share_init(*args, **kwargs):
4    # Initialize OAuth with facebook
5    # ...
6
7def share_on_facebook(*args, **kwargs):
8    # Post to Facebook
9    # ...

It works and everyone is happy. Then one day, you decide to support posting tweets in your application. You add the following:

1def twitter_share_init(*args, **kwargs):
2    # Initialize OAuth with twitter
3    # ...
4
5def share_on_twitter(*args, **kwargs):
6    # Post to Twitter
7    # ...

and the part of your code which has to figure out the appropriate sharing function to call might look something like:

1if requested_sharing_platform == "facebook":
2    facebook_share_init(*args, **kwargs)
3    share_on_facebook(*args, **kwargs)
4elif requested_sharing_platform == "twitter":
5    twitter_share_init(*args, **kwargs)
6    share_on_twitter(*args, **kwargs)

Things quickly become complicated when you have to implement Google+ sharing for example and you add two more functions and another couple of lines to the if-elif chain.

The real fun starts however when someone else in your team decides to copy-paste your if-elif chain and 10 days later, when you implement LinkedIn sharing, you expand your if-elif chain, but not your colleague’s, leading to all kinds of problems.

The above problem can be neatly solved using Abstract Classes (and the Factory Pattern).

A good rule of thumb in OOP is to “program to an interface, not an implementation”. What this means is to create an abstraction over related objects, that enforces a contract between the caller and the callee. For example, consider set of classes called Cat, Lion and Dog. A cat meows, dog barks and lion roars. But we may see these as special cases of an abstraction called speak, which translates to roaring for lions and barking for dogs. Our class definitions may look like:

 1class Animal(object):
 2    def speak():
 3        pass
 4
 5class Cat(Animal):
 6    def speak():
 7        # Meow
 8
 9class Dog(Animal):
10    def speak():
11        # Bark
12
13class Lion(Animal):
14    def speak():
15        # Roar

The problem with the above approach is that it is not enforcing. We can easily create a class like:

1class Fish(Animal):
2    def swim():
3        ...

Callers of Cat, Dog or Lion can call the speak method without any problem but calls to speak method of Fish instances will be delegated to the superclass, which might be a problem in real life examples. Ideally we would want users of our abstraction to have an iron clad contractual obligation with respect to the methods that we have exposed. This is where abstract classes come in.

Python originally did not have support for abstract classes and some people still consider them to be unpythonic. Python has decided to take the middle path on this (see PEP 3119) and has added support for abstract classes using the abc module, but has not changed the core syntax of the language.

Here is how our original problem of social sharing can be solved using abstract classes:

 1import abc
 2
 3class AbstractSocialShare(object):
 4    __metaclass__ = abc.ABCMeta
 5
 6    @abc.abstractmethod
 7    def __init__(self, *args, **kwargs):
 8        pass
 9
10    @abc.abstractmethod
11    def share(self, *args, **kwargs):
12        pass
13
14
15class FacebookShare(AbstractSocialShare):
16    def __init__(self, *args, **kwargs):
17        # Initialize Facebook OAuth
18        ...
19
20    def share(self, *args, **kwargs):
21        # Share on Facebook
22        ...
23
24
25class TwitterShare(AbstractSocialShare):
26    def __init__(self, *args, **kwargs):
27        # Initialize Twitter OAuth
28        ...
29
30    def share(self, *args, **kwargs):
31        # Share on Twitter
32        ...

Try creating an object of the abstract class with:

1obj = AbstractSocialShare()

You will receive a TypeError.

Now try creating and instantiating this class:

1class IncorrectShare(AbstractSocialShare):
2    def __init__(self, *args, **kwargs):
3        ...

Since IncorrectShare has not implemented the share method, we will not be able to instantiate it.

How do we take care of the ugly if-elif chain? Thats where the Factory pattern comes in.

 1class SocialShareFactory(object):
 2    __share_classes = {
 3        "facebook": FacebookShare,
 4        "twitter": TwitterShare
 5    }
 6
 7    @staticmethod
 8    def get_share_obj(name, *args, **kwargs):
 9        share_class = SocialShareFactory.__share_classes.get(name.lower(), None)
10
11        if share_class:
12            return share_class(*args, **kwargs)
13        raise NotImplementedError("The requested sharing has not been implemented")

The usage will be something like:

1obj = SocialShareFactory.get_share_obj("facebook", *args, **kwargs)
2
3obj.share("Something")

Conclusion

Python is an interpreted language which supports and encourages duck typing. Abstract classes may seem superficial in such a language but as I have attempted to illustrate above, they vastly improve code maintainability and reuse.

Comments are welcome!

comments powered by Disqus