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
File details
Details for the file suitable_class_finder-0.1.0.tar.gz
.
File metadata
- Download URL: suitable_class_finder-0.1.0.tar.gz
- Upload date:
- Size: 5.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.10.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a9e82203e6770ccb7dd3854a9eafae94456af5d126fbb853167d853ba8a3e4fb |
|
MD5 | d1ef2d767f84f725a74134f1355d6fb6 |
|
BLAKE2b-256 | 320e135951db26869bac7ad27dcabb7e697a1ffbf5f683cd1eea84d896624b7d |
File details
Details for the file suitable_class_finder-0.1.0-py3-none-any.whl
.
File metadata
- Download URL: suitable_class_finder-0.1.0-py3-none-any.whl
- Upload date:
- Size: 5.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.10.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | cff842d403f5156b86eaa5be407d4add16aec806ce50e6768cdd09a1b61d599f |
|
MD5 | 6218742238473fa87a320da00271075a |
|
BLAKE2b-256 | 4a89a9c0e34ca9ff9f51c3051139cd0e6603058dc6570e990243bb15df3fa052 |