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
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 thesuitable_method
and its arguments are thesuitable_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
- The different
can_handle
cases should be disjoint. If there are many subclasses that suits to one case, it will raise an exception. - Subclasses should cover all possible cases. If there is a case that doesn't match with any subclass, then an exception will be thrown.
- You can use a different method than
can_handle
. Just replace the desired method in thesuitable_method
argument ofsuitable_for
function. This could be useful when you have a complexsuitable_object
and you want to be more explicit with the name of the method. - 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 ofsuitable_for
method. It's disabled by default, as mentioned at the second item.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for suitable_class_finder-0.1.0.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | a9e82203e6770ccb7dd3854a9eafae94456af5d126fbb853167d853ba8a3e4fb |
|
MD5 | d1ef2d767f84f725a74134f1355d6fb6 |
|
BLAKE2b-256 | 320e135951db26869bac7ad27dcabb7e697a1ffbf5f683cd1eea84d896624b7d |
Hashes for suitable_class_finder-0.1.0-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | cff842d403f5156b86eaa5be407d4add16aec806ce50e6768cdd09a1b61d599f |
|
MD5 | 6218742238473fa87a320da00271075a |
|
BLAKE2b-256 | 4a89a9c0e34ca9ff9f51c3051139cd0e6603058dc6570e990243bb15df3fa052 |