Are you sure you are writing object oriented code?

We PHP developers are proud to write in OOP language (you can easily replace PHP here with C #, Java or another OOP language). Each vacancy contains requirements for knowledge of OOP. In every interview, they ask something about SOLID or the three whales of OOP. But when it comes down to it - we just get classes filled with procedures. OOP is rare, usually in library code.



A typical web application is ORM entity classes that contain data from a row in the database and controllers (or services - it doesn't matter) containing procedures for working with this data. Object Oriented Programming is about objects that own their own data, and do not provide it for processing by other code. A great illustration of this is a question that was asked in one chat: "How can I improve this code?"



private function getWorkingTimeIntervals(CarbonPeriod $businessDaysPeriod, array $timeRanges): array
{
    $workingTimeIntervals = [];
    foreach ($businessDaysPeriod as $date) {
        foreach ($timeRanges as $time) {
            $workingTimeIntervals[] = [
                'start' => Carbon::create($date->format('Y-m-d') . ' ' . $time['start']),
                'end' => Carbon::create($date->format('Y-m-d') . ' ' . $time['end'])
            ];
        }
    }

    return $workingTimeIntervals;
}

/**
 *    
 *
 * @param array $workingTimeIntervals
 * @param array $events
 * @return array
 */
private function removeEventsFromWorkingTime(array $workingTimeIntervals, array $events): array
{
    foreach ($workingTimeIntervals as $n => &$interval) {
        foreach ($events as $event) {
            $period = CarbonPeriod::create($interval['start'], $interval['end']);
            if ($period->overlaps($event['start_date'], $event['end_date'])) {
                if ($interval['start'] <= $event['start_date'] && $interval['end'] <= $event['end_date']) {
                    $interval['end'] = $event['start_date'];
                } elseif ($interval['start'] >= $event['start_date'] && $interval['end'] >= $event['end_date']) {
                    $interval['start'] = $event['end_date'];
                } elseif ($interval['start'] <= $event['start_date'] && $interval['end'] >= $event['end_date']) {
                    $interval['start'] = $event['start_date'];
                    $interval['end'] = $event['end_date'];
                } else {
                    unset($workingTimeIntervals[$n]);
                }
            }
        }
    }

    return $workingTimeIntervals;
}


. () (), . , . — ( ) , . , , . , , .



unit- . . ( ). .



class Interval
{
    //    PHP 7.4
    public DateTimeImmutable $start;
    public DateTimeImmutable $end;
}


DateTimeImmutable .



— .

, , , — .



unit-. . :



class Interval
{
    public DateTimeImmutable $start;
    public DateTimeImmutable $end;

    public function __construct(DateTimeImmutable $start, DateTimeImmutable $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}


PHPUnit- :



use App\Interval;
use PHPUnit\Framework\TestCase;

class IntervalTest extends TestCase
{
    private DateTimeImmutable $today;
    private DateTimeImmutable $yesterday;
    private DateTimeImmutable $tomorrow;

    protected function setUp(): void
    {
        $this->today = new DateTimeImmutable();
        $this->yesterday = $this->today->add(\DateInterval::createFromDateString("-1 day"));
        $this->tomorrow = $this->today->add(\DateInterval::createFromDateString("1 day"));

        parent::setUp();
    }

    public function testValidDates()
    {
        $interval = new Interval($this->yesterday, $this->today);

        $this->assertEquals($this->yesterday, $interval->start);
        $this->assertEquals($this->today, $interval->end);
    }

    public function testInvalidDates()
    {
        $this->expectException(\InvalidArgumentException::class);

        new Interval($this->today, $this->yesterday);
    }
}


, . , testValidDates, . , testInvalidDates, . , :



Failed asserting that exception of type "InvalidArgumentException" is thrown.


:



class Interval
{
    public DateTimeImmutable $start;
    public DateTimeImmutable $end;

    public function __construct(DateTimeImmutable $start, DateTimeImmutable $end)
    {
        if ($start > $end) {
            throw new \InvalidArgumentException("Invalid date interval");
        }

        $this->start = $start;
        $this->end = $end;
    }
}


. PHP, null . . , , . Interval . ? unit- . , . . , , isEmpty .



class Interval
{
    public DateTimeImmutable $start;
    public DateTimeImmutable $end;

    public function __construct(DateTimeImmutable $start, DateTimeImmutable $end)
    {
        if ($start > $end) {
            throw new \InvalidArgumentException("Invalid date interval");
        }

        $this->start = $start;
        $this->end = $end;
    }

    public function isEmpty(): bool
    {
        return $this->start->getTimestamp() == $this->end->getTimestamp();
    }
}

class IntervalTest extends TestCase
{
    //...

    public function testNonEmpty()
    {
        $interval = new Interval($this->yesterday, $this->today);

        $this->assertFalse($interval->isEmpty());
    }

    public function testEmpty()
    {
        $interval = new Interval($this->today, $this->today);

        $this->assertTrue($interval->isEmpty());
    }
}


. ['start'=>,'end'=>], . ! , . , :



-  08:00 - 12:00
-  13:00 - 17:00
  08:00 - 12:00
  13:00 - 17:00
...


, :



 :
-  08:00 - 09:00
-  16:00 - 17:00
  13:00 - 17:00

:
-  09:00 - 12:00
-  13:00 - 16:00
  08:00 - 12:00
...


Interval:



$period = CarbonPeriod::create($interval['start'], $interval['end']);
if ($period->overlaps($event['start_date'], $event['end_date'])) {
    if ($interval['start'] <= $event['start_date'] && $interval['end'] <= $event['end_date']) {
        $interval['end'] = $event['start_date'];
    } elseif ($interval['start'] >= $event['start_date'] && $interval['end'] >= $event['end_date']) {
        $interval['start'] = $event['end_date'];
    } elseif ($interval['start'] <= $event['start_date'] && $interval['end'] >= $event['end_date']) {
        $interval['start'] = $event['start_date'];
        $interval['end'] = $event['end_date'];
    } else {
        unset($workingTimeIntervals[$n]);
    }
}


Interval: remove(Interval $other) , . :



private function removeEventsFromWorkingTime($workingTimeIntervals, $events): array
{
    foreach ($workingTimeIntervals as $n => $interval) {
        foreach ($events as $event) {
            $interval->remove($event);

            if ($interval->isEmpty()) {
                unset($workingTimeIntervals[$n]);
            }
        }
    }

    return $workingTimeIntervals;
}


. . , , ! , . . , .



, $other .





class IntervalRemoveTest extends TestCase
{
    private DateTimeImmutable $minus10Days;
    private DateTimeImmutable $today;
    private DateTimeImmutable $yesterday;
    private DateTimeImmutable $tomorrow;
    private DateTimeImmutable $plus10Days;

    protected function setUp(): void
    {
        $this->today = new DateTimeImmutable();
        $this->yesterday = $this->today->sub(\DateInterval::createFromDateString("1 day"));
        $this->tomorrow = $this->today->add(\DateInterval::createFromDateString("1 day"));

        $this->minus10Days = $this->today->sub(\DateInterval::createFromDateString("10 day"));
        $this->plus10Days = $this->today->add(\DateInterval::createFromDateString("10 day"));

        parent::setUp();
    }

    public function testDifferent()
    {
        $interval = new Interval($this->minus10Days, $this->yesterday);

        $interval->remove(new Interval($this->tomorrow, $this->plus10Days));

        $this->assertEquals($this->minus10Days, $interval->start);
        $this->assertEquals($this->yesterday, $interval->end);
    }
}


, , .





class IntervalRemoveTest extends TestCase
{
    public function testFullyCovered()
    {
        $interval = new Interval($this->yesterday, $this->tomorrow);

        $interval->remove(new Interval($this->minus10Days, $this->plus10Days));

        $this->assertTrue($interval->isEmpty());
    }

    public function testFullyCoveredWithCommonStart()
    {
        $interval = new Interval($this->yesterday, $this->tomorrow);

        $interval->remove(new Interval($this->yesterday, $this->plus10Days));

        $this->assertTrue($interval->isEmpty());
    }

    // and testFullyCoveredWithCommonEnd()
}


, :





?





?! ! remove ! , :



} elseif ($interval['start'] <= $event['start_date'] && $interval['end'] >= $event['end_date']) {
        $interval['start'] = $event['start_date'];
        $interval['end'] = $event['end_date'];


.



, . , , . . , . , , . , , , . , , ! . .



IntervalCollection, :



class Interval
{
    public DateTimeImmutable $start;
    public DateTimeImmutable $end;

    public function __construct(DateTimeImmutable $start, 
                                DateTimeImmutable $end)
    {
        if ($start > $end) {
            throw new \InvalidArgumentException(
                                "Invalid date interval");
        }

        $this->start = $start;
        $this->end = $end;
    }

    public function isEmpty(): bool
    {
        return $this->start === $this->end;
    }

    /**
     * @param Interval $other
     * @return Interval[]
     */
    public function remove(Interval $other)
    {
        if ($this->start >= $other->end 
                || $this->end <= $other->start) return [$this];

        if ($this->start >= $other->start 
                && $this->end <= $other->end) return [];

        if ($this->start < $other->start 
                && $this->end > $other->end) return [
            new Interval($this->start, $other->start),
            new Interval($other->end, $this->end),
        ];

        if ($this->start === $other->start) {
            return [new Interval($other->end, $this->end)];
        }

        return [new Interval($this->start, $other->start)];
    }
}

/** @mixin Interval[] */
class IntervalCollection extends \ArrayIterator
{
    public function diff(IntervalCollection $other)
            : IntervalCollection
    {
        /** @var Interval[] $items */
        $items = $this->getArrayCopy();
        foreach ($other as $interval) {
            $newItems = [];
            foreach ($items as $ourInterval) {
                array_push($newItems, 
                ...$ourInterval->remove($interval));
            }
            $items = $newItems;
        }

        return new self($items);
    }
}


IntervalCollection — , . Interval, , .



https://github.com/adelf/intervals-example. , IntervalCollection::diff . , . . unit-.



, - coupling ( ), . private:



class Interval
{
    private DateTimeImmutable $start;
    private DateTimeImmutable $end;

    // methods
}


This can be done by adding style methods print()that will help us pull out the interval data in the desired format, but will completely close the ability to work with the interval data from the outside. But this is definitely a topic for another article.




All Articles