tbickford
tbickford

Reputation: 41

How can I programmatically decide what class to create an object as?

I want to write code to communicate with encoders of different positioning styles (absolute vs. relative). The idea I have is to create a base class that has things common to all encoders. Then there will be two child classes, one for absolute and one for relative. There will then be child classes of that for each brand. Each class will have unique functions to communicate with.

Sketch of hierarchy:

Sketch of hierarchy

Supposing I had a way to identify each motor by brand and type, how would I programmatically decide what class object to create?

Upvotes: 0

Views: 125

Answers (1)

BitsAreNumbersToo
BitsAreNumbersToo

Reputation: 921

This sounds like an excellent situation for inheritance, as you already hinted, and can be solved pretty quickly that way. I have put together some sample code for you that shows how you could accomplish this.

There is one point you make that I would like to contend:

You mention that each class will have unique functions. I actually recommend exactly the opposite, given every class the same core functions, and then after they are initialized, you never need to worry about which one you actually got, because they all adhere to the same interface. This can greatly simplify your future code as you will not need to ever check which class you are using, and your code will be reusable between different motors without any rewriting, just by changing the motor brand and name.

To answer this question how would I programmatically decide what class object to create? : I recommend a dictionary with brand name and model name tuple as the key, and the appropriately built class definition as the value. If two separate models use the same interface, you can simply map both to the same class definition. I have an example implementation of this below named MOTOR_ENCODER_LOOKUP.

import enum
import typing


class PositioningType(enum.Enum):
    relative = 'relative'
    absolute = 'absolute'


class EncoderBase:
    def __init__(
            self,
            pos_type: PositioningType,
            motor_brand: str,
            motor_model: str,
            address: str
    ):
        self.pos_type = pos_type
        self.motor_brand = motor_brand
        self.motor_model = motor_model
        self.address = address
        self._setup_hardware_interface()
        self._last_commanded_state = None
        self._last_commanded_state = self.get_position()

    def move_relative(self, relative_change: float) -> bool:
        raise NotImplementedError

    def move_absolute(self, new_position: float) -> bool:
        raise NotImplementedError

    def get_position(self) -> float:
        # Do something to get the position from the hardware
        # May want to override this in the motor-specific classes
        return self._last_commanded_state or 0.0

    def _setup_hardware_interface(self) -> bool:
        # Whatever specific setup is necessary for each encoder can be
        #   performed in the derived classes
        #   i.e., open a COM port, setup UDP, etc
        raise NotImplementedError

    def _send_command(self, commandvalue: float) -> bool:
        raise NotImplementedError

    def __repr__(self):
        outstr = ''
        outstr += f'<{type(self).__name__}\n'
        outstr += f'  positioning type: {self.pos_type}\n'
        outstr += f'  motor brand:      {self.motor_brand}\n'
        outstr += f'  motor model:      {self.motor_model}\n'
        outstr += f'  hardware address: {self.address}\n'
        outstr += f'  last position:    {self._last_commanded_state}\n'
        outstr += f'/>'
        return outstr

    @property
    def pos_type(self) -> PositioningType:
        return self._pos_type

    @pos_type.setter
    def pos_type(self, invalue: PositioningType):
        self._pos_type = invalue

    @property
    def motor_brand(self) -> str:
        return self._motor_brand

    @motor_brand.setter
    def motor_brand(self, invalue: str):
        self._motor_brand = invalue

    @property
    def motor_model(self) -> str:
        return self._motor_model

    @motor_model.setter
    def motor_model(self, invalue: str):
        self._motor_model = invalue

    @property
    def address(self) -> str:
        return self._address

    @address.setter
    def address(self, invalue: str):
        self._address = invalue


class AbsoluteEncoder(EncoderBase):
    def __init__(self, motor_brand: str, motor_model: str, address: str):
        super().__init__(PositioningType.absolute, motor_brand, motor_model, address)

    def move_relative(self, relative_change: float) -> bool:
        # This is inherently an absolute encoder, so calculate the new absolute
        #   position and send that
        new_position = self.get_position() + relative_change
        return self.move_absolute(new_position)

    def move_absolute(self, new_position: float) -> bool:
        # This is already an absolute encoder, so send the command as-is
        success = self._send_command(new_position)
        if success:
            self._last_commanded_state = new_position
        return success


class RelativeEncoder(EncoderBase):
    def __init__(self, motor_brand: str, motor_model: str, address: str):
        super().__init__(PositioningType.relative, motor_brand, motor_model, address)

    def move_relative(self, relative_change: float) -> bool:
        # This is already a relative encoder, so send the command as-is
        success = self._send_command(relative_change)
        if success:
            self._last_commanded_state += relative_change
        return success

    def move_absolute(self, new_position: float) -> bool:
        # This is inherently a relative encoder, so calculate the relative change
        #   and send the relative command
        relative_change = new_position - self.get_position()
        return self.move_relative(relative_change)


class EncoderAlphaOne(AbsoluteEncoder):
    def _send_command(self, commandvalue: float) -> bool:
        # do something to send the command
        return True

    def _setup_hardware_interface(self) -> bool:
        return True

    def get_position(self) -> float:
        # Ask the hardware for its current position since AbsoluteEncoders probably
        #   have that feature
        return self._last_commanded_state or 0.0


class EncoderAlphaTwo(RelativeEncoder):
    def _send_command(self, commandvalue: float) -> bool:
        # do something to send the command
        return True

    def _setup_hardware_interface(self) -> bool:
        return True


class EncoderBetaOne(AbsoluteEncoder):
    def _send_command(self, commandvalue: float) -> bool:
        # do something to send the command
        return True

    def _setup_hardware_interface(self) -> bool:
        return True

    def get_position(self) -> float:
        # Ask the hardware for its current position since AbsoluteEncoders probably
        #   have that feature
        return self._last_commanded_state or 0.0


# Add all your various make/model of encoders here with appropriate classes
#   and encoder_factory will automatically grab it
# Each encoder needs to have a class definition written, as shown above
#   but most of the work is done in one of the parent classes, so it should be quick
MOTOR_ENCODER_LOOKUP = {
    ('AlphaCompany', 'Model1'): EncoderAlphaOne,
    ('AlphaCompany', 'Model2'): EncoderAlphaTwo,
    ('BetaCompany', 'FirstModel'): EncoderBetaOne
}


# A factory function to go grab the correct class definition and initialize it
def encoder_factory(motor_brand: str, motor_model: str, address: str):
    return MOTOR_ENCODER_LOOKUP[(motor_brand, motor_model)](
        motor_brand,
        motor_model,
        address
    )


def _main():
    # Demonstrate that the functionality basically works
    # Use three separate types of encoder without fussing about which one you have
    e1 = encoder_factory('AlphaCompany', 'Model1', 'COM1')
    e2 = encoder_factory('AlphaCompany', 'Model2', 'COM3')
    e3 = encoder_factory('BetaCompany', 'FirstModel', 'COM4')
    e1.move_relative(25.0)
    e1.move_relative(-5.0)
    e2.move_relative(10.0)
    e2.move_absolute(45.0)
    e3.move_absolute(60.0)
    e3.move_relative(10.0)
    print(e1)
    print(e2)
    print(e3)


if __name__ == '__main__':
    _main()

Running the above yields:

<EncoderAlphaOne
  positioning type: PositioningType.absolute
  motor brand:      AlphaCompany
  motor model:      Model1
  hardware address: COM1
  last position:    20.0
/>
<EncoderAlphaTwo
  positioning type: PositioningType.relative
  motor brand:      AlphaCompany
  motor model:      Model2
  hardware address: COM3
  last position:    45.0
/>
<EncoderBetaOne
  positioning type: PositioningType.absolute
  motor brand:      BetaCompany
  motor model:      FirstModel
  hardware address: COM4
  last position:    70.0
/>

Let me know if you have any follow up questions.

Upvotes: 0

Related Questions