Search code examples
phprefactoringphp-closuresphp-generators

Refactoring comparision/operators blocks to DRY up and reduce C.R.A.P level


I set out to make a small project around a bounch of classes that return generators (php 5.5).

The main motivation for the small project was to expand on my TDD journey, fiddle with generators and have a package I could throw on packagist for later use.

The current state of the whole "project" can be found at Github

All tests are green, the methods does what I want. Now I want to refactor as I there is lots of dublication.

    /**
    *   Returns a Generator with a even range.
    *
    *   getEven(10); // 10,12,14,16,18,20,22 ...
    *   getEven(null, 10); // 10,8,6,4,2,0,-2,-4 ...
    *   getEven(10, null, 2); // 10,6,2, -2 ...
    *   getEven(10,20); // 10,12,14,16,18,20
    *   getEven(20,10); // 20,18,16,14,12,10
    *   getEven(10,20,2); // 10,14,18
    *
    *   @param int|null $start
    *   @param int|null $end
    *   @param int $step
    *   @throws InvalidArgumentException|LogicException
    *   @return Generator
    */
    public function getEven( $start = null, $end = null, $step = 1 )
    {
        // Throws LogicException
        $this->throwExceptionIfAllNulls( [$start, $end] );
        $this->throwExceptionIfInvalidStep($step);

        // Throws InvalidArgumentException
        $this->throwExceptionIfNotNullOrInt( [$start, $end] );

        // infinite increase range
        if(is_int($start) && is_null($end))
        {
            // throw LogicException
            $this->throwExceptionIfOdd($start);

            $Generator = function() use ($start, $step)
            {
                for($i = $start; true; $i += $step * 2)
                {
                    yield $i;
                }
            };
        }
        // infinite decrease range
        elseif(is_int($end) && is_null($start))
        {
            // throws LogicException
            $this->throwExceptionIfUneven($end);

            $Generator =  function() use ($end, $step)
            {
                for($i = $end; true; $i -= $step * 2)
                {
                    yield $i;
                }
            };
        }
        // predetermined range
        else 
        {
            // throws LogicException
            $this->throwExceptionIfUneven($start);
            $this->throwExceptionIfUneven($end);

            // decrease
            if($start >= $end)
            {
                $Generator = function() use ($start, $end, $step)
                {
                    for($i = $start; $i >= $end; $i -= $step * 2)
                    {
                        yield $i;
                    }
                };
            }
            // increase
            else
            {
                $Generator = function() use ($start, $end, $step)
                {
                    for($i = $start; $i <= $end; $i += $step * 2)
                    {
                        yield $i;
                    }
                };
            }
        }

        return $Generator();
    }

The class also has a method named getOdd (and yes it looks alot like it ;) )

The main dublication is the closures $Generator = function() ... and the difference is mostly operators such as + - * / and arguments in the for loop. This is mainly the same in the rest of th class.

I read Dynamic Comparison Operators in PHP and come to the conclusion that there is no native method like compare(...)

Should I make a private/protected method for comparison. If so should I make a new class/function for this? I do not think it belongs in the current class.

Is it something else I am missing, I am unsure on how to DRY this up, in a proper way?

Btw. iknow a getEven, getOdd is kinda silly when i got a getRange With step function, but it is a more general refactoring / pattern question.

Update @github the getEven and getOdd are now removed...


Solution

  • The code below has not been tested or verified to work, but I have faith in it and at least it shows one possible way of removing the multiple generator functions.

    As you state yourself, the duplication you are trying to remove is mainly in the generator function. If you look into this you can see that every generator function you have can be written as this:

    function createGenerator($index, $limit, $step) {
        return function() use($index, $limit, $step) {
            $incrementing = $step > 0;
            for ($i = $index; true; $i += 2 * $step) {
                if (($incrementing && $i <= $limit) || (!$incrementing && $i >= $limit)) {
                    yield $i;
                }else {
                    break;
                }
            }
        };
    }
    

    In order to utilize this you need to do some magic with the input arguments and it helps (at least makes it pretty) to define some constants. PHP allready got a PHP_INT_MAX constant holding the greatest value possible for an integer, however it does not got a PHP_INT_MIN. So I would define that as a constant of its own.

    define('PHP_INT_MIN', ~PHP_INT_MAX);
    

    Now lets take a look at the four cases in your function.

    1) Infinite increase range

    Infinte is a rather bold claim here, if we change it to "greatest value possible given the constraints of an int" we get a finite range from $index to PHP_INT_MAX, hence by setting $limit = PHP_INT_MAX; the above mentioned generator function will still be the same.

    //$limit = PHP_INT_MAX;
    createGenerator($index, PHP_INT_MAX, $step);
    

    2) Infinite decrease range

    The same argument as above can again be used here, but with a negativ $step and swapping $index and $limit;

    //$index = $limit;
    //$limit = PHP_INT_MIN;
    //$step *= -1;
    createGenerator($limit, PHP_INT_MIN, -1 * $step);
    

    3) Predetermined decreasing range

    Swap and negate once again.

    //$temp = $index;
    //$index = $limit;
    //$limit = $temp;
    //$step *= -1;
    createGenerator($limit, $index, -1 * $step);
    

    4) Predetermined increasing range

    Well this is just the default case, where all arguments are given. And nothing needs to change.

    createGenerator($index, $limit, $step);
    

    The revised code

    public function getEven($index = null, $limit = null, $step = 1) {
        // Throws LogicException
        $this->throwExceptionIfAllNulls([$index, $limit]);
        $this->throwExceptionIfInvalidStep($step);
    
        // Throws InvalidArgumentException
        $this->throwExceptionIfNotNullOrInt([$index, $limit]);
    
        //Generator function
        function createGenerator($index, $limit, $step) {
            return function() use($index, $limit, $step) {
                $incrementing = $step > 0;
                for ($i = $index; true; $i += 2 * $step) {
                    if (($incrementing && $i <= $limit) || (!$incrementing && $i >= $limit)) {
                        yield $i;
                    }else {
                        break;
                    }
                }
            };
        }
        // infinite increase range
        if (is_int($index) && is_null($limit)) {
            // throw LogicException
            $this->throwExceptionIfodd($index);
            return createGenerator($index, PHP_INT_MAX, $step);
        }
        // infinite decrease range
        elseif (is_int($limit) && is_null($index)) {
            // throws LogicException
            $this->throwExceptionIfodd($limit);
            return createGenerator($limit, PHP_INT_MIN, -1*$step);
        }
        // predetermined range
        else {
            // throws LogicException
            $this->throwExceptionIfodd($index);
            $this->throwExceptionIfodd($limit);
    
            // decrease
            if ($index >= $limit) {
                return createGenerator($limit, $index, -1 * $step);
            }
            return createGenerator($index, $limit, $step);
        }
    }