caspillaga
caspillaga

Reputation: 563

Design Pattern to merge two implementations into one class

I can't figure out the correct way to model this problem. Here I give you a minimalistic version of my code:

# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod

class AreaCalculator():
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    @abstractmethod
    def getArea(self):
        pass

    def compute(self):
        self.getArea()


class PerimeterCalculator():
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    @abstractmethod
    def getPerimeter(self):
        pass

    def compute(self):
        self.getPerimeter()


class TriangleAreaCalculator(AreaCalculator):

    def __init__(self):
        AreaCalculator.__init__(self)

    def getArea(self):
        return area

class TrianglePerimeterCalculator(PerimeterCalculator):

    def __init__(self):
        PerimeterCalculator.__init__(self)

    def getPerimeter(self):
        return perimeter



a = TriangleAreaCalculator()
b = TrianglePerimeterCalculator()

Is there an elegant way to merge "TrianglePerimeterCalculator" and "TriangleAreaCalculator" classes into one, but keeping "PerimeterCalculator" and "AreaCalculator" separated?

[edit] As Kyle suggested in the comments, I can create a new class (let's call it "Triangle") that inherits from "PerimeterCalculator" and "AreaCalculator" at the same time, but what I want is to be able to tell a new instance of "Triangle" to behave as "PerimeterCalculator" or "AreaCalculator", but not both at the same time.

Upvotes: 1

Views: 1499

Answers (3)

martineau
martineau

Reputation: 123521

Here's another answer, following the editing and clarification of your question. It allows creation of a single Triangle instance that can behave like either an AreaCalculator or PerimeterCalculator, as needed.

This programming pattern is called "delegation" and is used where the responsibility for implementing a particular operation is handed off to a different object—in this case an internally held instance of some other class. A common way to do this in Python is by overriding the class's default __getattr__() method.

Since you've never responded to the comment under my other answer about exactly what it is that controls which behavior is used, I added a set_behavior() method to allow it to be specified explicitly.

from abc import ABCMeta, abstractmethod


class AreaCalculator:
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    @abstractmethod
    def getArea(self):
        pass

    def compute(self):
        return self.getArea()


class PerimeterCalculator:
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    @abstractmethod
    def getPerimeter(self):
        pass

    def compute(self):
        return self.getPerimeter()


class TriangleAreaCalculator(AreaCalculator):

    def __init__(self):
        super(TriangleAreaCalculator, self).__init__()

    def getArea(self):
        print('TriangleAreaCalculator.getArea() called')
        area = 13
        return area



class TrianglePerimeterCalculator(PerimeterCalculator):

    def __init__(self):
        super(TrianglePerimeterCalculator, self).__init__()

    def getPerimeter(self):
        print('TrianglePerimeterCalculator.getPerimeter() called')
        perimeter = 42
        return perimeter


class Triangle:

    def __init__(self):
        delegate_classes = TriangleAreaCalculator, TrianglePerimeterCalculator

        # Map delegate classes to instances of themselves.
        self._delegates = {delegate_class: delegate_class()
                            for delegate_class in delegate_classes}

        self.set_behavior(TriangleAreaCalculator)  # Set default delegate.

    def __getattr__(self, attrname):
        # Called only for attributes not defined by this class (or its bases).
        # Retrieve attribute from current behavior delegate class instance.
        return getattr(self._behavior, attrname)

    def set_behavior(self, delegate_class):
        try:
            self._behavior = self._delegates[delegate_class]
        except KeyError:
            raise TypeError("{} isn't a valid {} behavior delegate class"
                              .format(delegate_class, self.__class__.__name__))


if __name__ == '__main__':

    triangle = Triangle()
    # Uses instance's default behavior.
    print('triangle.compute() -> {}'.format(triangle.compute()))

    triangle.set_behavior(TrianglePerimeterCalculator)  # Change behavior.
    print('triangle.compute() -> {}'.format(triangle.compute()))

Output:

TriangleAreaCalculator.getArea() called
triangle.compute() -> 13
TrianglePerimeterCalculator.getPerimeter() called
triangle.compute() -> 42

Upvotes: 1

caspillaga
caspillaga

Reputation: 563

I figured it out myself, with inspiration on the commentas/answers of Kyle and martineau.

I can create a merged class "Triangle" as follows:

class Triangle():

    def __init__(self):
        pass

    def getTriangleArea(self):
        print 'Triangle area'

    def getTrianglePerimeter(self):
        print 'Triangle perimeter'

And then modify TriangleAreaCalculator and TrianglePerimeterCalculator as follows:

class TriangleAreaCalculator(AreaCalculator, Triangle):

    def __init__(self):
        TriangleCalculator.__init__(self)
        AreaCalculator.__init__(self)

    def getArea(self):
        super(TriangleAreaCalculator, self).getTriangleArea()

class TrianglePerimeterCalculator(PerimeterCalculator, Triangle):

    def __init__(self):
        TriangleCalculator.__init__(self)
        PerimeterCalculator.__init__(self)

    def getPerimeter(self):
        super(TrianglePerimeterCalculator, self).getTrianglePerimeter()

This way, I can create a new Triangle-like instance that behaves as "PerimeterCalculator" or "AreaCalculator" (but not both at the same time):

a = TriangleAreaCalculator()
b = TrianglePerimeterCalculator()

a.compute() # correctly prints "Triangle area"
b.compute() # correctly prints "Triangle perimeter"

Upvotes: 0

martineau
martineau

Reputation: 123521

I think the "design pattern" you should use is multiple inheritance. Below is a modified version of your code demonstrating how do it (plus a few other changes to make it actually runnable and all classes new-style).

from abc import ABCMeta, abstractmethod

class AreaCalculator(object):
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    @abstractmethod
    def getArea(self):
        pass

    def compute(self):
        self.getArea()


class PerimeterCalculator(object):
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    @abstractmethod
    def getPerimeter(self):
        pass

    def compute(self):
        self.getPerimeter()


class TriangleAreaCalculator(AreaCalculator):

    def __init__(self):
        super(TriangleAreaCalculator, self).__init__()

    def getArea(self):
        print('TriangleAreaCalculator.getArea() called on instance of {}'.format(
            self.__class__.__name__))
#        return area
        return 13

class TrianglePerimeterCalculator(PerimeterCalculator):

    def __init__(self):
        super(TrianglePerimeterCalculator, self).__init__()

    def getPerimeter(self):
        print('TrianglePerimeterCalculator.getPerimeter() called on instance of {}'.format(
            self.__class__.__name__))
#        return perimeter
        return 42


class MergedCalculator(TriangleAreaCalculator, TrianglePerimeterCalculator):

    def __init__(self):
        super(MergedCalculator, self).__init__()

merged = MergedCalculator()
print('merged.getArea() -> {}'.format(merged.getArea()))
print('merged.getPerimeter() -> {}'.format(merged.getPerimeter()))

Output:

TriangleAreaCalculator.getArea() called on instance of MergedCalculator
merged.getArea() -> 13
TrianglePerimeterCalculator.getPerimeter() called on instance of MergedCalculator
merged.getPerimeter() -> 42

Upvotes: 2

Related Questions