AntsInPants
AntsInPants

Reputation: 35

Object Factory design to initialize parent or child class object

I am building a tool that takes directories as inputs and performs actions where necessary. These actions vary depeding on certain variables so I created a few class objects which help me with my needs in an organised fashion.

However, I hit a wall figuring out how to best design the following scenario.

For the sake of simplicity, let's assume there are only directories (no files). Also, the below is a heavily simplified example.

I have the following parent class:

# directory.py
from pathlib import Path

class Directory:
    def __init__(self, absolute_path):
        self.path = Path(absolute_path)

    def content(self):
        return [Directory(c) for c in self.path.iterdir()]

So, I have a method in the parent class that returns Directory instances for each directory inside the initial directory in absolute_path

What the above does, is hold all methods that can be performed on all directories. Now, I have a separate class that inherits from the above and adds further methods.

# special_directory.py
from directory import Directory

class SpecialDirectory(Directory):
    def __init__(self, absolute_path):
        super().__init__(absolute_path)

    # More methods

I am using an Object Factory like approach to build one or the other based on a condition like so:

# directory_factory.py
from directory import Directory
from special_directory import SpecialDirectory

def pick(path):
    return SpecialDirectory(path) if 'foo' in path else Directory(path)

So, if 'foo' exists in the path, it should be a SpecialDirectory instead allowing it to do everything Directory does plus more.

The problem I'm facing is with the content() method. Both should be able to do that but I don't want it to be limited to making a list of Directory instances. If any of its content has "foo*", it should be a SpecialDirectory.

Directory doesn't (and shouldn't) know about SpecialDirectory, so I tried importing and using the factory but it complains about some circular import (which makes sense).

I am not particularly stuck as I have come up with a temp fix, but it isn't pretty. So I was hoping I could get some tips as to what would be an effective and clean solution for this specific situation.

Upvotes: 1

Views: 981

Answers (1)

martineau
martineau

Reputation: 123473

What you need is sometimes called a "virtual constructor" which is a way to allow subclasses to determine what type of class instance is created when calling the base class constructor. There's no such thing in Python (or C++ for that matter), but you can simulate them. Below is an example of a way of doing this.

Note this code is very similar to what's in my answer to the question titled Improper use of __new__ to generate classes? (which has more information about the technique). Also see the one to What exactly is a Class Factory?

from pathlib import Path


class Directory:

    subclasses = []

    @classmethod
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

    def __init__(self, absolute_path):
        self.path = Path(absolute_path)

    def __new__(cls, path):
        """ Create instance of appropriate subclass. """
        for subclass in cls.subclasses:
            if subclass.pick(path):
                return object.__new__(subclass)
        else:
            return object.__new__(cls)  # Default is this base class.

    def content(self):
        return [Directory(c) for c in self.path.iterdir()]

    def __repr__(self):
        classname = type(self).__name__
        return f'{classname}(path={self.path!r})'

    # More methods
    ...


class SpecialDirectory(Directory):
    def __init__(self, absolute_path):
        super().__init__(absolute_path)

    @classmethod
    def pick(cls, path):
        return 'foo' in str(path)

    # More methods
    ...


if __name__ == '__main__':
    root = './_obj_factory_test'
    d = Directory(root)
    print(d.content())

Upvotes: 1

Related Questions