Strategy Pattern¶
Usage and Purpose¶
The Strategy Pattern is a fundamental design pattern in software engineering, primarily used to enable an object to change its behavior dynamically. It’s particularly useful in scenarios where an object must be able to switch between different algorithms or strategies at runtime. The essence of this pattern is to define a family of algorithms, encapsulate each one of them, and make them interchangeable. This allows for algorithm variations independently from clients that use them.
Limitations¶
The greatest shortcoming of the traditional strategy pattern is that it does not allow for passing parameters to strategies. This is necessary in many real-world applications, where algorithms require different parameters to run.
Common Suboptimal Solutions¶
In practice, several approaches have been used to handle the issue of passing parameters to strategies, each with its shortcomings:
Passing set of parameters as Keyword Arguments (kwargs): This dynamic approach reduces code readability and can lead to silent contract breaches, causing potential issues that are not immediately apparent.
Generic Parameters Interface: Creating a generic Parameters container class to encompass all parameters required by different algorithms leads to significant coupling, which can hinder maintenance and scalability.
Dedicated Parameters Classes for Each Concrete Strategy: While this reduces coupling compared to a generic interface, it can lead to bloated code, reduced readability, and synchronization challenges between strategies and their parameters.
Defining Parameters as Properties: A commonly effective solution involves defining different parameters as properties of concrete strategy classes. These are then initialized as needed, maintaining separation of concerns and adhering to modularity principles.
Standard Strategy Pattern UML Diagram¶
classDiagram
class Context {
-Strategy strategy
+Context(Strategy strategy)
+executeStrategy()
}
class Strategy {
<<interface>>
+executeAlgorithm()
}
class ConcreteStrategyA {
+executeAlgorithm()
}
class ConcreteStrategyB {
+executeAlgorithm()
}
Context "1" --> "1" Strategy : has-a
Strategy <|-- ConcreteStrategyA : implements
Strategy <|-- ConcreteStrategyB : implements
Parameterized Strategy Pattern¶
The Parameterized Strategy Pattern[1] presents an innovative solution to the limitations of the traditional strategy pattern. This approach involves creating an abstract Parameter class, which is then extended by concrete parameter implementations for each specific parameter type (ex int, float, boolean). Each of the concrete strategy classes then contains a list of these concrete Parameter classes, which are initialized in the constructor. This approach allows for the passing of parameters to strategies, while maintaining separation of concerns and adhering to modularity principles. Besides, it allows for the automatic detection of parameters by the client, which is particularly useful for creating dynamic graphical user interfaces (GUIs) where the available controls and inputs adjust according to the chosen strategy’s parameters. This method allows each concrete strategy to define its own set of parameters, leading to more flexible, dynamic, and user-friendly applications.
[1] Sobajic, O., et al. (2010). Parameterized strategy pattern. Proceedings of the 17th Conference on Pattern Languages of Programs. Reno, Nevada, USA, Association for Computing Machinery: Article 9. - Link to Paper - Link to PDF
Relation to Other Design Patterns¶
Strategy Pattern: This approach extends the traditional strategy pattern by incorporating parameter flexibility.
Adapter Pattern: Each concrete algorithm can be seen as an adapter, transforming the required interface to a specific algorithm interface using parameter classes.
Factory Pattern: The client can be seen as a factory, creating concrete algorithms and their parameters.
UML Diagram for Parameterized Strategy Pattern¶
The key agents in this pattern and their responsibilities are as follows:
Client: Responsible for selecting algorithms and handling parameter instances, including presenting them in the GUI for user interaction.
Concrete Algorithm: Contains the specific algorithm routine with a unique set of parameters.
Parameter Classes: Abstract and concrete parameter classes handle specific types and constraints of parameters.
classDiagram
class Client {
+selectAlgorithm()
+executeAlgorithm()
}
class AbstractAlgorithm {
<<interface>>
+execute()
+getParameters()
}
class ConcreteAlgorithmA {
+execute()
+getParameters()
}
class ConcreteAlgorithmB {
+execute()
+getParameters()
}
class AbstractParameter {
<<interface>>
+getValue()
+setValue()
}
class ConcreteParameter1
class ConcreteParameter2
Client --> AbstractAlgorithm : uses
AbstractAlgorithm <|-- ConcreteAlgorithmA
AbstractAlgorithm <|-- ConcreteAlgorithmB
AbstractAlgorithm "1" --> "*" AbstractParameter : has
AbstractParameter <|-- ConcreteParameter1
AbstractParameter <|-- ConcreteParameter2
Sequence Diagram for Parameterized Strategy Pattern¶
sequenceDiagram;
participant User
participant Client
participant Algorithm
participant Parameter1
participant Parameter2
User->>+Client: Choose Algorithm
Client->>+Algorithm: Instantiate
Algorithm->>-Client: Algorithm Instance
Client->>+Algorithm: getParameters()
Algorithm->>-Client: Parameter1, Parameter2
Client->>User: Display Parameters (Parameter1, Parameter2)
User->>Client: Modify Parameter Values
Client->>+Parameter1: SetValue(newValue1)
Parameter1->>-Client: Value Set
Client->>+Parameter2: SetValue(newValue2)
Parameter2->>-Client: Value Set
User->>+Client: Execute Algorithm
Client->>+Algorithm: execute()
Algorithm->>+Parameter1: GetValue()
Parameter1->>-Algorithm: Value1
Algorithm->>+Parameter2: GetValue()
Parameter2->>-Algorithm: Value2
Algorithm->>Algorithm: Run Algorithm (Value1, Value2)
Algorithm->>-Client: Execution Complete
Client->>-User: Results/Output
Implemented Parameter types¶
Parameter Classes¶
The package provides a collection of concrete parameter classes designed to wrap Python’s nominal types (int, float, str, and bool) and an extension to support UFloat types from the uncertainties package. These parameter classes are intended to serve as robust building blocks for systems implementing a parameterized version of the Strategy pattern. The purpose is to facilitate the validation and use of configurable parameters across different algorithms and strategies.
The parameter classes included offer features such as:
Initialization to None by default, unless a value is explicitly provided.
Methods to safely change the parameter values while maintaining internal consistency.
Extensive validation to reduce the chance of errors when using the parameters.
The guaranteed type of value, boundary checking (for numeric types), and accepted values checking (for string types).
- Classes:
IntParameter: Wraps an integer value with an optional min and max boundary. FloatParameter: Wraps a float value with an optional min and max boundary. StrParameter: Wraps a string value, with optional accepted values. BoolParameter: Wraps a boolean value. UFloatParameter: Wraps a UFloat (uncertain float) value, encapsulating numbers with uncertainties, whilst allowing optional min and max boundaries based on the nominal value.
Every parameter class have the ‘name’ attribute, which is used to identify the parameter. The ‘value’ attribute is used to store the value of the parameter. The optional default attribute is used to set the default value of the parameter. Each parameter class possess different properties, depending on the type of the parameter. They all possess a param_type attribute containing the type the parameter wraps.
The IntParameter, FloatParameter, and UFloatParameter classes have the optional ‘min’ and ‘max’ attributes, which are used to set the boundaries of the parameter. The StrParameter class has the optional accepted_values attribute, which is used to set the accepted values of the parameter.
Both the value. the defualt, and the other parameter attributes can be set set upos instantiation of the parameter class. However, only the value of the parameter should be changed after instantiation, using the set_value() method.
Class Diagram¶
classDiagram
class Parameter {
<<abstract>>
+String name
+get_value()
+set_value(value)
+param_type (Property)
}
class IntParameter {
-Optional[int] default
-Optional[int] min
-Optional[int] max
+set_value(value: Optional[int])
}
class FloatParameter {
-Optional[float] default
-Optional[float] min
-Optional[float] max
+set_value(value: Optional[float])
}
class StrParameter {
-Optional[str] default
-Optional[List[str]] accepted_values
+set_value(value: Optional[str])
}
class BoolParameter {
-Optional[bool] default
+set_value(value: Optional[bool])
}
class UFloatParameter {
-Optional[UFloat] default
-Optional[UFloat] min
-Optional[UFloat] max
+set_value(value: Optional[UFloat])
}
class IterableParameter {
-Optional[Iterable] default
-Optional[Iterable] accepted_values
+set_value(value: Optional[Iterable])
}
Parameter <|-- IntParameter
Parameter <|-- FloatParameter
Parameter <|-- StrParameter
Parameter <|-- BoolParameter
Parameter <|-- UFloatParameter
Parameter <|-- IterableParameter
Example Usage:
>>> from pumas.architecture.parameters import IntParameter
>>> int_param = IntParameter(name="IntParam", default=10, min=0, max=20)
>>> int_param.value
10
>>> int_param.set_value(15)
>>> int_param.value
15
>>> from uncertainties import ufloat
>>> from pumas.architecture.parameters import UFloatParameter
>>> ufloat_param = UFloatParameter(name="UFloatParam", default=ufloat(1, 0.1), min=ufloat(0, 0.1), max=ufloat(2, 0.1))
>>> ufloat_param.value.nominal_value
1.0
>>> ufloat_param.set_value(ufloat(1.5, 0.1))
>>> ufloat_param.value.nominal_value
1.5
The use of these parameter classes helps to enforce a consistent approach to handling parameters across different parts of an application.
Note
Instantiation of any parameter with a type that doesn’t match the expected will result in InvalidParameterTypeError.
Attempting to set values outside the range for IntParameter, FloatParameter, and UFloatParameter will result in InvalidBoundaryError.
Passing non-strings for a parameter name will result in InvalidParameterNameError.
Setting an unrecognized string value for StrParameter when accepted_values is defined will result in InvalidAcceptedValuesError.
Parameter Manager¶
The ParameterManager class is responsible for generatign Parameter objects from a function using its type hints. In addition to that the ParameterManager class is also responsible for updating the parameters attributes. This is necessary because, in our implementation the attributes of the parameters, are not supposed to be changed, with the nociable exception of the value attribute, that can be changed by the Parametert set_value() method. The ParameterManager class is thus responsible for updating the parameters attributes, by creating a new instance of the parameter class, initialized with the new attributes.
Example Usage:
>>> from pumas.architecture.parameters import ParameterManager
Initialize the ParameterManager with parameter definitions.
>>> param_defs = {
... "q": {"type": "int", "default": 5}
... }
>>> pm = ParameterManager(param_defs)
Check the initial state of the parameters managed by the ParameterManager. The expected output should show a dictionary containing representation of an IntParameter instance for ‘x’. In this case, the default value of ‘q’ is 5, and no boundaries are defined.
>>> pm.parameters_map
{'q': IntParameter(name='q', default=5, min=None, max=None)}
Check the current value of the parameter: it defaults to 5.
>>> pm.parameters_map['q'].value
5
>>> id_1 = id(pm.parameters_map['q'])
>>> pm.set_parameter_value('q', 10) # Set a new value for the parameter.
>>> pm.parameters_map['q'].value # Check the new value of the parameter.
10
>>> id_2 = id(pm.parameters_map['q'])
Changing the value of the parameter should not change the parameter instance. >>> id_1 == id_2 True