I'm designing an algorithm that matches entries based on some notion of "proximity" (for the sake of discussion, assume we're matching floats). Furthermore:
- The input is a scalar and a vector, and the output should the "closest" element of the vector to the scalar.
- I'm considering three matching techniques: Forward, Backward and Bidirectional.
- Matching is performed within a tolerance. That is, if the closest value is outside an allowed tolerance, a warning is issued.
I decided that the Strategy design pattern should be suitable here, and ended up with the following:
class MatchingStrategy(ABC):
@abstractmethod
def match(self, query: float, candidates: Iterable[float]) -> float:
"""The main interface that outputs the closest candidate to the query."""
class LookbackMatchingStrategy(MatchingStrategy):
def __init__(self, tolerance: float = 0.2, **kwargs) -> None:
self.__lookback_tol: float = tolerance
def match(self, query: float, candidates: Iterable[float]) -> float:
# <implementation>
class LookaheadMatchingStrategy(MatchingStrategy):
def __init__(self, tolerance: float = 0.2, **kwargs) -> None:
self.__lookahead_tol: float = tolerance
def match(self, query: float, candidates: Iterable[float]) -> float:
# <implementation>
class BidirectionalMatchingStrategy(MatchingStrategy):
def __init__(self, backward_tolerance: float = 0.1, forward_tolerance: float = 0.1, **kwargs) -> None:
self.__lookback_tol: float = backward_tolerance
self.__lookahead_tol: float = forward_tolerance
def match(self, query: float, candidates: Iterable[float]) -> float:
# <implementation>
As you can see above, each Strategy is optionally configurable through its constructor. This is different from all the examples of the Strategy patterns I saw, where concrete sub-Strategies only ever implemented the "main interface" (in the example above, the match
function). While I personally can't find any obvious flaws in my approach, some of my colleagues are uneasy, because supposedly it would be harder to construct these objects using a factory since their constructors aren't exactly the same.
Which brings me to my question - are configurable Strategies, as in the example above, allowable / a good idea / the right tool for the job?
One obvious alternative is encapsulating the configurations in a {data}class and having the Strategy constructors expect it (so basically a wrapper over kwargs
). I don't like this approach because such a class will keep getting bigger as more strategies are added over time, and would contain an increasingly large number of parameters that are irrelevant to most Strategies.