2

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.

1
  • 1
    "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" - same goes for the omniscient factory - if it tries to handle all subtypes, and if it enforces a specific constructor signature, you'll either have parameter creep in the factory (to set the properties after construction), or in the constructors of the concrete strategies themselves), or otherwise you'll have to deal with incomplete objects, and downcasting, and whatnot. Aug 10, 2022 at 8:10

1 Answer 1

8

"As you can see above, each Strategy is optionally configurable through its constructor."

This is perfectly fine/normal, and is in fact one of the cornerstones that makes polymorphic types flexible: you enter the parameters that vary across the hierarchy via the constructor, and you then pass the resulting object to clients that only rely on the abstract interface (dependency injection).

"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)."

The internet is full of oversimplified toy examples that don't address the more subtle points.

The original Design Patterns book also gives a toy example, but the authors give 3 strategies that have disparate constructors:

// They have an abstract Compositor class (which plays the role of the Strategy)
// and 3 derivatives with the following constructors: 

SimpleCompositor();
TeXCompositor();
ArrayCompositor(int interval);

// Some code then creates one of these, based on its own 
// needs, and injects the instance into the Context object:
 
class Composition {
public:
    Composition(Compositor*);    // note the ctor parameter
    void Repair();
private:
    // ...
};

// It can then use the Composition instance itself, or pass the 
// preconfigured instance to something else that needs it as a dependency

"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."

If you impose this restriction on yourself, then there should be a good reason for that. You might not need any sort of factory. And even when it looks like you do, ask yourself if you really need a single factory that creates all these objects, along with the unnecessary coupling this brings? Such a factory would have to be aware of all the subtypes, and you'd have to update it (and possibly code that uses it) whenever you add a new derivative.

A better, more flexible use of factories is in circumstances where some of the constructor parameters are not known when a dependent object needs to be created. So, the dependent object gets injected with a factory that produces the dependency at a later time (in some sense, you can think of it as partial construction). You wrap the dependency's constructor in a factory or a lambda that allows the missing parameters to be plugged in later on, when they become available. These factories are much more subtype-specific, with a narrower responsibility, so they don't impose a particular constructor signature across the hierarchy.

If you don't have such a situation, you can just create the concrete strategy by directly calling the constructor, and inject it into the dependent object manually. Adding a factory without having a good reason does not magically make the code more decoupled.

Another situation where this might be a concern is when you are using some sort of a framework that insists that your objects have the default constructor, or a constructor with a particular signature. But again, that puts an artificial limitation on you - and then it becomes a matter of a cost/benefit analysis. I.e., that framework better be worth it.

Finally, what about DI containers? I'm not particularly familiar with what's available for Python, but if you're using dependency injection with a DI container, most containers don't require a specific constructor signature.

2
  • 2
    "The internet if full of oversimplified toy examples that don't address the more subtle points." - cannot agree more!
    – Doc Brown
    Aug 10, 2022 at 9:04
  • 1
    Thank you for your insightful answer!
    – Dev-iL
    Aug 10, 2022 at 11:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.