3

In Python, I have a class C which embeds objects from classes A and B. Is it considered good practice to creates shortcuts from the properties of embedded objects of classes A and B as attributes of container class C?

The goal is to create a simpler interface in class C, abstracting the implementation details of classes A and B:

A minimal example:

class A:
    def __init__(self):
        self._a = 1

    @property
    def a(self):  # Read-only property
        return self._a


class B:
    def __init__(self):
        self._b = 2

    @property
    def b(self):  # Read-write property
        return self._b

    @b.setter
    def b(self, value):
        self._b = max([value, 0])


class C:
    def __init__(self):
        self.a = A()
        self.b = B()

        # The following attributes provide "shortcuts" 
        # to the properties of the contained objects
        self.a_val = self.a.a
        self.b_val = self.b.b
blunova
  • 388
  • 1
  • 4
  • 16

2 Answers2

4

This is one way of mitigating violations of law of Demeter. I would provide caution on two things:

  1. When you assign the subattributes of A and B to attributes, you're taking a snapshot in time. Futher changes to a.a won't be reflected in a. This may or may not be desirable

    It might be preferable to make a method which returns the current value of a.a on-demand, rather than storing its value at the time of initialization.

  2. In some cases this process of exposing constituent properties makes sense to do conceptually, but in others it doesn't.

    Consider a car, which contains driving controls and an engine.

    This would be bad:

    drivingControls.engine.openThrottleBody()
    

    This would be preferred:

    drivingControls.acceleratorPedalPressed()
    
Alexander
  • 3,562
  • 1
  • 19
  • 24
2

Since this is Python, you could implement the "shortcuts" to A.a and B.b as properties of C, so that changes in the underlying instances of A and B are instantly reflected when you query C.a or C.b, respectively. This avoids the first issue raised by @Alexander.

Example:

class C:
    def __init__(self):
        self.a = A()
        self.b = B()

    @property
    def a(self):
        return self.a.a

    @property
    def b(self):
        return self.b.b

    @b.setter
    def b(self, value):
        self.b.b = value

I said could, because as you can see, this leads to some verbosity - you'd have to add properties (and potentially their setters) for every attribute you want to expose. You should really evaluate whether this makes sense conceptually, or whether instances of C could instead have methods that use A.a and B.b internally, without never exposing them directly. For example:

class C:
    def __init__(self):
        self.a = A()
        self.b = B()

    def do_foo(self):
        return self.a.a + self.b.b

Alternatively, if all you need is the data held by A or B, but none of their functionality, you can simply relay the data over to C upon construction. This can be done either by passing in A and B instances to C's __init__:

class C:
    def __init__(self, a_instance, b_instance):
        self.a = a_instance.a
        self.b = b_instance.b

a = A()
b = B()
c = C(a_instance=a, b_instance=b)

Or passing in the values directly:

class C:
    def __init__(self, a, b):
        self.a = a
        self.b = b
a = A()
b = B()
c = C(a=a.a, b=b.b)

This is often the simplest solution, because it keeps A and B isolated from C, while only relaying the necessary data. But whether this works for you or not depends on your specific use case.

jfaccioni
  • 496
  • 2
  • 8
  • In my case the solution that works the best is the first one. I avoid writing too much boilerplate code by using a property factory (in my actual code I have around 10 properties to link): https://stackoverflow.com/questions/36580931/python-property-factory-or-descriptor-class-for-wrapping-an-external-library – Daniel Arteaga Dec 01 '21 at 17:46