Search code examples
pythonoopmoduleooad

Advantages to two modules with same interface or two classes with the same interface?


For example I can create two modules (Mod1 and Mod2) that have f(). Or I can create two classes that implement f().

I can then either

if condition:
    import Mod1 as m
else:
    import Mod2 as m

m.f()

or

if condition:
    m = Class1()
else:
    m = Class2()
m.f()

Obviously if I want to store any state I need to use classes, but if I am just using class methods, are there advantages to either method over the other?

EDIT: I am mostly concerned with maintainability/extensibility/readability and not with runtime performance.


Solution

  • General Idea

    In your specific case, you could go with simple module level function, not to overcomplicate everything. But as far as you are interested in the general case, I would recommend you to go with the Abstract Factory pattern. Here is an example solution for you:

    class YourAbstractFactory(object):
        @classmethod
        def get_factory(cls, condition):
            cond_map = {
                'impl1': Impl1,
                'impl2': Impl2
            }
    
            impl = cond_map.get(condition)
    
            return impl() if impl else None
    
        def f1(self):
            pass
    
        def f2(self):
            pass
    

    YourAbstractFacotry class is obviously an abstract class, that simply defines the interface to be implemented, i.e. the collection of your f functions. Moreover it defines and implements the get_factory class method that returns the appropriate implementation, depending on the condition.

    class Impl1(YourAbstractFactory):
        def f1(self):
            print('Impl1.f1')
    
        def f2(self):
            print('Impl1.f2')
    
    class Impl2(YourAbstractFactory):
        def f1(self):
            print('Impl2.f1')
    
        def f2(self):
            print('Impl2.f2')
    

    The above classes are the two independent implementations of your interface. The are unaware of each other and can exist independently.

    # This is where you put your condition, and get appropriate instance
    >>> impl = YourAbstractFactory.get_factory('impl2')
    >>> impl.f1()
    Impl2.f1
    

    So the advantage of this solution in general case is that your client code will be decoupled of you implementations. Meaning, that you will only pass the condition and get the required implementation, and have no any idea and dependency on that implementation inside you client code. The only piece that is aware of the concrete implementations of the interface is the get_factory method, which is easy to maintain.

    Additional Improvements

    To further enhance this solution and increase the security (although, Python is a language for adults ;) ), you can also use the abc module, with its ABCMeta metaclass and abstractmethod decorator, to prevent initialising instances of classes that do not implement all the interface. In this case you will need to define your factory class like this.

    from abc import ABCMeta, abstractmethod
    
    class YourAbstractFactory(object):
        __metaclass__ = ABCMeta
    
        @classmethod
        def get_factory(cls, condition):
            cond_map = {
                'impl1': Impl1,
                'impl2': Impl2
            }
    
            impl = cond_map.get(condition)
    
            return impl() if impl else None
    
        @abstractmethod
        def f1(self):
            pass
    
        @abstractmethod
        def f2(self):
            pass