The absolute statement is false.

Application of Factory Method Pattern in Python

(NOTE: this article includes content translated by a machine)

Every now and then, there are always a few days when I feel like messing things up. This time, I reformatted the whole hard drive and reinstalled the dual system. As a result, I couldn’t play League of Legends anymore with Win10 + A card… So I had to go back to Ubuntu. While wandering online, I came across this picture, which provided strong support for my truancy~

2015-03-28

Then, a Blog based on git-pages was born.


    Ramble Section


I looked at the source code of the ioloop module in Tornado without knowing the depth of the sky and the earth, and I was clueless. Other things aside, tornado.ioloop.IOLoop.instance().start() is used to start Tornado, but IOLoop().start() is not implemented in the source code, while PollIOLoop().start() is. Let’s see what instance() really is:

:::python
>>> import tornado.ioloop
>>> repr(tornado.ioloop.IOLoop.instance())
'<tornado.platform.epoll.EPollIOLoop object at 0x7f4ac177ae50>'

Where did EPollIOLoop come from?

Inheritance relationship:
                +-------------------+
                | util.Configurable |
                +-------------------+
                        ^
                        |
                +---------------+
                | ioloop.IOLoop |
                +---------------+
                        ^
                        |
                +-------------------+     +------------------------------+
                | ioloop.PollIOLoop | <-- | platform.select.SelectIOLoop |
                +-------------------+     +------------------------------+
                      ^            ^
                      |            |
+----------------------------+    +------------------------------+
| platform.epoll.EpollIOLoop |    | platform.kqueue.KQueueIOLoop |
+----------------------------+    +------------------------------+

The documentation for util.Configurable describes it as: Base class for configurable interfaces. This is an abstract class, and its constructor (__new__() [1]) plays the role of a factory function for one of its implementation subclasses (which should be this guy ioloop.IOLoop). It also says that its implementation subclasses can use configure() to globally change instances at runtime, although it doesn’t seem to be used here. By using the constructor as a factory method, this interface behaves like a normal class, and methods like isinstance can be used normally. This pattern is most useful in these situations: when the chosen implementation may be a global decision (if epoll is available, it will replace select, which is available on *unix), or when a previously-monolithic class (what?) is divided into specific subclasses.

Looking at the class documentation, I was enlightened. Could this be the factory method pattern we talked about in the last design pattern class? Well, I didn’t really understand it during the class anyway…


The functions of these classes are as follows:

  • util.Configurable: Creates instances through the constructor __new__() as a factory method.
  • ioloop.IOLoop: When you call tornado.ioloop.IOLoop.instance(), it will return the best one of SlectIOLoop/EpollIOLoop/KQueueIOLoop through configrable_defult() according to the platform you are using, and give it to util.Configurable to create an instance.
  • ioloop.PollIOLoop: Creates a select-like method with consistent interfaces for IOLoop.
  • platform.select.SlectIOLoop/platform.epoll.EpollIOLoop/platform.kqueue.KQueueIOLoop: Platform-specific subclasses.

Let’s take a look at this simplified code snippet to understand how this magic works:

from __future__ import print_function

EPOLL, SELECT = 2, 1

class Configurable(object):

    def __new__(cls, **kwargs):
        """ Constructor, Factory Method """
        impl = cls.configurable_default()
        instance = super(Configurable, cls).__new__(impl)
        instance.initialize(**kwargs)
        return instance

    @classmethod
    def configurable_default(cls):
        raise NotImplementedError()

    def initialize(self): # Can be replaced with __init__, because __init__ is not a constructor
        pass


class IOLoop(Configurable):

    @staticmethod
    def instance():
        """ Simple Singleton Pattern """
        if not hasattr(IOLoop, '_instance'):
            IOLoop._instance = IOLoop()
        print("IOLoop -> instance()")
        return IOLoop._instance

    @classmethod
    def configurable_default(cls):
        print("IOLoop -> configurable_default()")
        if EPOLL: # Assume EPOLL is the best way
            return EPollIOLoop
        return SelectIOLoop

    def initialize(self):
        print("IOLoop -> initialize()")
        pass


class PollIOLoop(IOLoop):

    def initialize(self, impl, **kwargs):
        print("PollIOLoop -> initialize()")
        super(PollIOLoop, self).initialize()


class EPollIOLoop(PollIOLoop):

    def initialize(self, **kwargs):
        print("EPollIOLoop -> initialize()")
        super(EPollIOLoop, self).initialize(impl=EPOLL, **kwargs)


class SelectIOLoop(PollIOLoop):

    def initialize(self, **kwargs):
        print("SelectIOLoop -> initialize()")
        super(SelectIOLoop, self).initialize(impl=SELECT, **kwargs)

if __name__ == '__main__':
    print(repr(IOLoop.instance()))
    print(repr(IOLoop.instance()))

Output

IOLoop -> configurable_default()
EPollIOLoop -> initialize()
PollIOLoop -> initialize()
IOLoop -> initialize()
IOLoop -> instance()
<__main__.EPollIOLoop object at 0x7f91f7da1590>  # The two object ids are consistent
IOLoop -> instance()
<__main__.EPollIOLoop object at 0x7f91f7da1590>

When IOLoop.instance() calls IOLoop(), it will call Configurable’s __new__() to instantiate an object. When calling __new__, it will call IOLoop.configurable_default() to get a best choice (EPollIOLoop), then instantiate this choice, return this instance, so the instance obtained by IOLoop() is an EPollIOLoop object.

The core of the factory method pattern is how to instantiate and get one specific instance object, but the system does not want our code to be coupled with this class’s subclasses, or we simply do not know what subclasses this class has available, or we do not know which subclass is better. Tornado uses it very well here, allowing IOLoop to decide which subclass to instantiate based on the platform, without the user worrying about how the subclass is created, just knowing what methods IOLoop has.

BTW: The source code of Tornado is far more complicated than I imagined. I took a simple look at the structure of httpserver and it was more than complex. It seems that it is not easy to understand these simple codes app = Application(); http_server = tornado.httpserver.HTTPServer(app); http_server.listen(options.port); tornado.ioloop.IOLoop.instance().start(), but sooner and later ……


[1] __init__() is not a true constructor in the sense that __init__() is responsible for initializing variables after the class object is created. __new__() is the one that truly creates instances, and it is the constructor of the class.