¿Estás seguro de que estás escribiendo código orientado a objetos?

Los desarrolladores de PHP estamos orgullosos de escribir en lenguaje de programación orientada a objetos (aquí puede reemplazar fácilmente PHP con C #, Java u otro lenguaje de programación orientada a objetos). Cada vacante contiene requisitos para el conocimiento de la programación orientada a objetos. En cada entrevista preguntan algo sobre SOLID o las tres ballenas de OOP. Pero cuando se trata de eso, las clases están llenas de procedimientos. La programación orientada a objetos es poco común, generalmente en el código de la biblioteca.



Una aplicación web típica son las clases de entidad ORM que contienen datos de una fila en la base de datos y controladores (o servicios, no importa) que contienen procedimientos para trabajar con estos datos. La Programación Orientada a Objetos trata sobre objetos que poseen sus propios datos y no los proporcionan para ser procesados ​​por otro código. Una gran ilustración de esto es una pregunta que se hizo en un chat: "¿Cómo puedo mejorar este código?"



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
}


Esto se puede hacer agregando métodos de estilo print()que nos ayudarán a extraer los datos del intervalo en el formato deseado, pero cerrarán por completo la capacidad de trabajar con los datos del intervalo desde el exterior. Pero este es definitivamente un tema para otro artículo.




All Articles