Prototype Pattern

I started reading Mark Summerfield's new book Python in Practice. The first section covers object-oriented software design patterns. Everytime I come across an article or a book that goes over patterns I tell myself I need to make a point of illustrating these from my own experiences, as I can never remember all their names. Here's an example of the prototype pattern from a recent bit of code.

The Pattern

I find the term prototype a bit misleading. What this pattern really represents is cloning - making an copy of an object. The python standard library comes with the copy module that provides a couple of methods for doing just that. My example does not use the copy module however.

The Problem

This blog is made with my own homegrown static site generator. The one problem I had was how to encapsulate a path in the site's file structure so that I could easily transform it from a source file (e.g. a markdown file) to its final location in the build directory (e.g. as an html file).

A source file would look something like the following, and I need to discern both the root directory, the relative site directory path as well as the file name and it's possible extension:

/home/freddie/blog/contents/blog/posts/prototype_pattern.md
\_________________________/\_________/\________________/\___/
             |                  |             |           |
           root                dir           base        ext

Now I want to make a copy my path, but replace any of the parts - most importantly the root and the ext.

/home/freddie/blog/build/blog/posts/prototype_pattern.html
\______________________/\_________/\________________/\___/
          |                  |             |           |
        root                dir           base        ext

The Basic Object

The basic path object looks like this:

class PathInfo(object):

    def __init__(self, root, dir_, base, ext=None):
        self.root = root
        self.dir_ = dir_
        # split the ext from base if it has one
        basename, ext_ = os.path.splitext(base)
        self.base = basename 
        self.ext = ext or ext_
        self.rel_path = join('/', self.dir_.strip('/'), self.base + self.ext)
        self.full_path = join(self.root, self.dir_.strip('/'), self.base + self.ext)
        self.full_dir = os.path.dirname(self.full_path)

I could have implemented the rel_path, full_path and full_dir attributes as methods, but decided is was better to deal with setting their values immediately. The only wonky bit is the ext keyword argument. When fetching directory information from os.walk a file's extension won't be separate and there is no real need to separate it out prior to initializing a PathInfo object, however, in order to be able to transform the file's extension this keyword argument is required.

Copying the Prototype

Making a copy of the PathInfo just involves initializing a new instance with the originating instance's values. That is the essential idea of the prototype pattern - we're just cloning the original object. In addition I am providing the ability to transform the cloned version:

    def copy_as(self, **kwargs):
        root = kwargs.get('root', self.root)
        dir_ = kwargs.get('dir', self.dir_)
        base = kwargs.get('base', self.base)
        ext = kwargs.get('ext', self.ext)
        return PathInfo(root, dir_, base, ext)

The Prototype In Action

Let's say we want to copy a directory of files to another:

def copy_dir(src_dir, dst_dir):
    import shutil

    if not os.path.exists(src_dir) or not os.path.exists(dst_dir):
        raise Exception('The source or destination directory does not exist.')

    # collect the source directory path info
    src_paths = [] 
    for root, dirs, files in os.walk(src_dir):
        relpath = os.path.relpath(root, src_dir)
        relpath = "" if relpath == "." else relpath
        for file_name in files:
            # here's our original prototype
            src_paths.append(PathInfo(src_dir, relpath, file_name))

    # copy and transform our paths with a new root
    for src in src_paths:
        # here's our clone and transform
        dst = src.copy_as(root=dst_dir) 
        if not os.path.exists(dst.full_dir):
            os.makedirs(dst.full_dir)
        shutil.copy(src.full_path, dst.full_path)

Resources

Tags: pythonpatterns

comments powered by Disqus