Skip to main content

Given the hierarchy of an abstract class, it detects the appropriate concrete subclass (deterministically) that satisfies certain attributes obtained as a parameter. Useful for implementing the Strategy design pattern.

Project description

SuitableClassFinder

PyPI GitHub release (latest by date) GitHub License Package Status CircleCI CodeFactor Grade Codecov

Given the hierarchy of an abstract class, it detects the appropriate concrete subclass (deterministically) that satisfies certain attributes obtained as a parameter. Useful for implementing the Strategy design pattern.

[!NOTE] This is a Python port from a useful tool which I used in my times as a Smalltalk developer and I miss a lot. It's part from a set of snippets called Smalltools-st.

Example

Let's imagine that we have the following hierarchy:

from abc import ABC

class Vehicle(ABC):
    def __init__(self, brand, color):
        self.brand = brand
        self.color = color

class Car(Vehicle):
    def __init__(self, doors_amount, *args):
        self.doors_amount = doors_amount
        super().__init__(*args)

class Bike(Vehicle):
    pass

class Motorbike(Vehicle):
    pass

And we are consuming some silly API. The response could be something like:

vehicles = [
    {'type':'car', 'doors':5, 'motor':1400, 'brand':'renault', 'color':'red'},
    {'type':'bike', 'doors':0, 'motor':0, 'brand':'trek', 'color':'orange'},
    {'type':'motorbike', 'doors':0, 'motor':250, 'brand':'yamaha', 'color':'black'},
    {'type':'car', 'doors':3, 'motor':1200, 'brand':'volkswagen', 'color':'white'},
    ...
]

Adding just this snippet to Vehicle:

    @classmethod
    def can_handle(cls, vehicle_type):
        return cls.__name__.lower() == vehicle_type

...we can get the right subclass for each json, just passing the type string attribute to the suitable_for method:

from smalltools.behavior.suitable_class_finder import SuitableClassFinder

SuitableClassFinder(Vehicle).suitable_for(vehicles[0]['type']) # Returns Car

[!TIP] The can_handle method is what we called the suitable_method and its arguments are the suitable_object.

But, what if the API response is not so easy?

vehicles = [
    {'doors':5, 'motor':1400, 'brand':'renault', 'color':'red'},
    {'doors':0, 'motor':0, 'brand':'trek', 'color':'orange'},
    {'doors':0, 'motor':250, 'brand':'yamaha', 'color':'black'},
    {'doors':3, 'motor':1200, 'brand':'volkswagen', 'color':'white'},
    ...
]

Don't worry. We can do something like this:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, brand, color):
        self.brand = brand
        self.color = color

    @classmethod
    @abstractmethod
    def can_handle(cls, doors, motor):
        pass

class Car(Vehicle):
    def __init__(self, doors_amount, *args):
        self.doors_amount = doors_amount
        super().__init__(*args)

    @classmethod
    def can_handle(cls, doors, motor):
        return doors > 0 and motor > 0

class Bike(Vehicle):
    @classmethod
    def can_handle(cls, doors, motor):
        return doors == 0 and motor == 0

class Motorbike(Vehicle):
    @classmethod
    def can_handle(cls, doors, motor):
        return doors == 0 and motor > 0

Check that you can pass multiple arguments to the suitable_method. So we have to do the next lines:

from smalltools.behavior.suitable_class_finder import SuitableClassFinder

vehicle = vehicles[0]
SuitableClassFinder(Vehicle).suitable_for(vehicle['doors'], vehicle['motor']) # Returns Car

Okey, and if you have objects with different "shapes"?

vehicles = [
    {'doors':5, 'motor':1400, 'brand':'renault', 'color':'red'},
    {'brand':'trek', 'color':'orange'},
    {'motor':250, 'brand':'yamaha', 'color':'black'},
    {'doors':3, 'motor':1200, 'brand':'volkswagen', 'color':'white'},
    ...
]

Then, you can pass the entire json and process it:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, brand, color):
        self.brand = brand
        self.color = color

    @classmethod
    @abstractmethod
    def can_handle(cls, raw_json):
        pass

class Car(Vehicle):
    def __init__(self, doors_amount, *args):
        self.doors_amount = doors_amount
        super().__init__(*args)

    @classmethod
    def can_handle(cls, raw_json):
        return 'doors' in raw_json and raw_json['doors'] > 0

class Bike(Vehicle):
    @classmethod
    def can_handle(cls, raw_json):
        return 'doors' not in raw_json and 'motor' not in raw_json

class Motorbike(Vehicle):
    @classmethod
    def can_handle(cls, raw_json):
        return 'doors' not in raw_json and 'motor' in raw_json and raw_json['motor'] > 0

As simple as that!

from smalltools.behavior.suitable_class_finder import SuitableClassFinder

SuitableClassFinder(Vehicle).suitable_for(vehicles[0]) # Returns Car

The sky is the limit!

Notes

  1. The different can_handle cases should be disjoint. If there are many subclasses that suits to one case, it will raise an exception.
  2. Subclasses should cover all possible cases. If there is a case that doesn't match with any subclass, then an exception will be thrown.
  3. You can use a different method than can_handle. Just replace the desired method in the suitable_method argument of suitable_for function. This could be useful when you have a complex suitable_object and you want to be more explicit with the name of the method.
  4. Sometimes, it could be good to return a default class when no result is found (instead of raising an exception). You can do this with the default_subclass argument of suitable_for method. It's disabled by default, as mentioned at the second item.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

suitable_class_finder-0.1.4.tar.gz (5.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

suitable_class_finder-0.1.4-py3-none-any.whl (5.7 kB view details)

Uploaded Python 3

File details

Details for the file suitable_class_finder-0.1.4.tar.gz.

File metadata

  • Download URL: suitable_class_finder-0.1.4.tar.gz
  • Upload date:
  • Size: 5.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.2

File hashes

Hashes for suitable_class_finder-0.1.4.tar.gz
Algorithm Hash digest
SHA256 c0ed044899af1cd2f6d76b3d72895e012b8f381eb775492dc470d45adb721942
MD5 b02cde4e519d3a24e8724f15a5862a0a
BLAKE2b-256 c3e620d5a2bc0852cef318e0bf3ffe0a23f3485dedb9da45f84de58a9a77452d

See more details on using hashes here.

File details

Details for the file suitable_class_finder-0.1.4-py3-none-any.whl.

File metadata

File hashes

Hashes for suitable_class_finder-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 68083560c961f7d965703d0d7e374d14765118923c1dbfba40b8e47f47e4baae
MD5 ce43681dad7f4c7d9d54b9b7e5d716b7
BLAKE2b-256 b5950ea33180a83fb67eedc8f67756c9f3ad70ad42d4b941210d62e31515da8c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page