visit
There are many articles explaining how to make a good design and what rules to follow. In this note we will see a concrete example on how to convert a into a better one.
To be able to see, in production, data on the performance of each strategy in real time.
<?
StrategySupervisedHelper::getInstance()->optimize($processId);
class StrategySupervisedHelper extends Singleton {
//..
}
<? private function optimize($processId);
<?
private function optimize($processId){
throw new Exception('Testing coverage');
}
“An inherited system is one that has no tests”
The strategy to refactor an inherited system is to cover the existing functionality before making any changes.
Photo by
on
The possible solution to this deadlock is to write the tests declaratively, thus generating better interfaces.
We will run them manually until the coupling is resolved.function testOptimizationIsGoodEnough(){
$this->assertTrue(false);
}
function testOptimizationBelowThreshold(){
$this->assertTrue(false);
}
...
Let’s think about the responsibilities to choose the name in MAPPER.
<?
class SupervisedLearningAlgorithm extends Singleton {
}
yields a very implemental invocation (coupled to getInstance()) and not very declarative...
<?
SupervisedLearningAlgorithm::getInstance()->optimize($processId);
?>
(new SupervisedLearningAlgorithm())->optimize($processId);
<?
class SupervisedLearningAlgorithm{
}
Do not subclass concrete classes.If the language allows this, we explicitly declare it:
<?
final class SupervisedLearningAlgorithm{
}
This is a code smell suggesting us to check the cohesion between this parameter and the process.
<?
final class SupervisedLearningAlgorithm{
public function calculate($processId){}
private function analize($processId){}
private function executeAndGetData($processId, bool $isUsingFastMethod = null){}
//... etc etc etc
}
Looking at bijection we conclude there can be no algorithm without a process. We don’t want to have a class with setters to mutate it:
Therefore we will pass all the essential attributes during construction.
The way to know if an attribute is essential is to take away all the responsibilities associated with that object. If it can no longer carry out its responsibilities, it is because the attribute belongs to the .
?>
final class SupervisedLearningAlgorithm{
public function __construct($processId){}
}
In this way, the strategy is immutable in its essence, with all the benefits it brings us.
The process, according to bijection, models a real world process. This seems to fit the pattern.
However, we believe that it is closer to a where there is an ordered sequence of executions, modeling the different steps of an algorithm.Photo by
on
?>
final class SupervisedLearningStrategy{
}
There is never a valid reason to use null. Null does not exist in real life.
It violates the principle of bijection and generates coupling between the function caller and the argument. Also, it generates unnecessary ifs as null is not polymorphic with any other object.
<?
private function executeAndGetData($processId, $isUsingFastMethod = null){
}
private function executeAndGetData($processId, bool $isUsingFastMethod = false){
}
The default parameters produce coupling and ripple effect. They are available for the programmer laziness. Since it is a private function the replacement scope is the same class. We make it explicit, replacing all invocations:
<?
private function executeAndGetData($processId, bool $isUsingFastMethod = false){
}
private function executeAndGetData($processId, bool $isUsingFastMethod){
}
<?
final class SupervisedLearningStrategy {
const CONFIDENDE_INTERVAL_THRESHOLD=0.9;
private function executeAndGetData($processId, bool $isUsingFastMethod){
//...
if ($estimatedError <= self::CONFIDENDE_INTERVAL_THRESHOLD) {}
//..
}
}
Remember that the tests have to be in control of the entire environment and the time is global and fragile to match the tests.
From now on, it will be an essential parameter of object creation (Refactoring by is a safe task, which can be done by any modern IDE.The log stores relevant information in production about the executions of the strategy. As usual, using a Singleton as a global reference.
This new bond prevents us from being able to test it. This Singleton is in another module over which we have no control, so we are going to use a wrapping technique.
function logInfo(array $infoToLog) {
SingletonLogger::info($infoToLog);
}
Besides from being a Singleton, the log uses static class messages.
<?
SingletonLogger::info($infoToLog);
The only protocol that a class should contain is the one related to its single responsibility (the S for Solid): creating instances.Since the reference is to a static method, we cannot replace the class call with a polymorphic method. Instead, we will use an anonymous function.
<?
function logInfo(array $infoToLog) {
$loggingFunction = function() use ($infoToLog) {
SingletonLogger::info($infoToLog);
};
$loggingFunction($infoToLog);
}
<?
final class SupervisedLearningAlgorithm{
public function __construct($processId, closure $logging function){
}
}
<?
$loggingFunction = function() use ($infoToLog) {
SingletonLogger::info($infoToLog);
};
new SupervisedLearningAlgorithm{$processId, $loggingFunction);
<?
$loggingFunction = function() use ($infoToLog) {
$this->loggedData[] = $infoToLog;
};
new SupervisedLearningAlgorithm{$processId, $loggingFunction);
$this->assertEquals([...],$this->loggedData);
<?
private function getDataToPersist($runTime, $isClustering) {
return [
'processId' => $this->processId,
'date' => new Timestamp(),
'runTime' => $runTime,
'isClustering' => $isClustering
];
}
?>
final class LearningAlgorithmRunData{
////look for responsabilities related to the cohesive data
}
<?
function testOptimizationIsGoodEnough(){
///
$this->assertEquals($expected, $real);
}
function testOptimizationBelowThreshold(){
///
$this->assertEquals($expected, $real);
}
Photo by
on
Photo by
on