How to use mapping method of base class for derived class?

77 views Asked by At

Imagine I have the following in python (actual language does not really matter, since I have the problem in other languagues like php too. A base class "CarModel" and a derived class "TruckModel" from "CarModel".

class CarModel:
    def __init__(self):
        self.__wheel_count: int = 0

    def set_wheel_count(self, value: int) -> None:
        self.__wheel_count = value

class TruckModel(CarModel):
    def __init__(self):
        super().__init__()
        self.__load_in_kg: int = 0

    def set_load_in_kg(self, value: int) -> None:
        self.__load_in_kg= value

If I now have a mapping class, which should convert e.g. a Dict to my Model, how can I reuse the mapping-method for my derived class? Especially if I have a base class with a lot of setter-methods, I have to repeat the code, which I don't like ("dry").

class VehicleMapper:
    def map_car_dict_to_car_model(dict: Dict) -> CarModel:
        model: CarModel = CarModel()
        model.set_wheel_count(dict['wheelCount'])
        return model

    def map_truck_dict_to_truck_model(dict: Dict) -> TruckModel:
        model: TruckModel= TruckModel()
        model.set_load_in_kg(dict['loadInKg'])
 
        model.set_wheel_count(dict['wheelCount']) #  ??? How can I re-use the map-method for the base class here ???
        
        return model

I could move the mapping methods to the model classes, then this would work. But I got teached, that model classes should only "hold" data and shouldn't do anything. That's why there are mapper classes, right?

2

There are 2 answers

1
RomanPerekhrest On BEST ANSWER

A more cleaner and solid way is to use separate mappers/factories.
And it's even more justified as in your case you also need a configurable mapping from dict keys to respective model property name.

Consider the following pattern:

class CarModel:
    def __init__(self):
        self.__wheel_count: int = 0

    def wheel_count(self, value: int) -> None:
        self.__wheel_count = value

    wheel_count = property(None, wheel_count)

class TruckModel(CarModel):
    def __init__(self):
        super().__init__()
        self.__load_in_kg: int = 0

    def load_in_kg(self, value: int) -> None:
        self.__load_in_kg= value

    load_in_kg = property(None, load_in_kg)


class VehicleFactory:
    """Vehicle base factory"""
    __model__ = None
    __map_keys__ = None

    @classmethod
    def create(cls, data):
        model = cls.__model__()
        for k, attr in cls.__map_keys__.items():
            setattr(model, attr, data[k])
        return model

class CarFactory(VehicleFactory):
    __model__ = CarModel
    __map_keys__ = {'wheelCount': 'wheel_count'}

class TruckFactory(VehicleFactory):
    __model__ = TruckModel
    __map_keys__ = {'wheelCount': 'wheel_count',
                    'loadInKg': 'load_in_kg'}

Usage:

car = CarFactory.create({'wheelCount': 4})
print(vars(car))    # {'_CarModel__wheel_count': 4}

truck = TruckFactory.create({'wheelCount': 4, 'loadInKg': 500})
print(vars(truck))  # {'_CarModel__wheel_count': 4, '_TruckModel__load_in_kg': 500}
0
blhsing On

A cleaner approach would be to use a decorator function to annotate each setter method with the key that would map to it, and store the key-to-setter mapping as a class attribute, so that keys and setters for CarModel can be resued for TruckModel, and no names of keys or setters are repeated when building the mapping, better adhering to the DRY principle:

class FromDictable:
    key_to_setter = {}

    @classmethod
    def bind_key(cls, key):
        def decorator(setter):
            cls.key_to_setter[key] = setter
        return decorator

    @classmethod
    def from_dict(cls, d: dict):
        instance = cls()
        for key, value in d.items():
            cls.key_to_setter[key](instance, value)
        return instance

bind_key = FromDictable.bind_key

class CarModel(FromDictable):
    def __init__(self):
        self.__wheel_count: int = 0

    @bind_key('wheelCount')
    def set_wheel_count(self, value: int) -> None:
        self.__wheel_count = value

class TruckModel(CarModel):
    def __init__(self):
        super().__init__()
        self.__load_in_kg: int = 0

    @bind_key('loadInKg')
    def set_load_in_kg(self, value: int) -> None:
        self.__load_in_kg= value

so that:

print(vars(CarModel.from_dict({'wheelCount': 4})))
print(vars(TruckModel.from_dict({'wheelCount': 6, 'loadInKg': 8000})))

outputs:

{'_CarModel__wheel_count': 4}
{'_CarModel__wheel_count': 6, '_TruckModel__load_in_kg': 8000}

Demo here