Apakah Anda yakin Anda menulis kode berorientasi objek?

Kami pengembang PHP dengan bangga menulis dalam bahasa OOP (Anda dapat dengan mudah mengganti PHP di sini dengan C #, Java atau bahasa OOP lainnya). Setiap lowongan berisi persyaratan pengetahuan tentang OOP. Dalam setiap wawancara, mereka menanyakan sesuatu tentang SOLID atau tiga paus OOP. Tetapi ketika sampai pada itu - kami hanya mengisi kelas dengan prosedur. OOP jarang terjadi, biasanya dalam kode perpustakaan.



Aplikasi web tipikal adalah kelas entitas ORM yang berisi data dari baris dalam database dan pengontrol (atau layanan - tidak masalah) yang berisi prosedur untuk bekerja dengan data ini. Object Oriented Programming adalah tentang objek yang memiliki data mereka sendiri, dan tidak memberikan itu untuk memproses oleh kode lain. Ilustrasi yang bagus tentang ini adalah pertanyaan yang diajukan dalam satu obrolan: "Bagaimana cara meningkatkan kode ini?"



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
}


Hal ini dapat dilakukan dengan menambahkan metode gaya print()yang akan membantu kami mengeluarkan data interval dalam format yang diinginkan, tetapi akan sepenuhnya menutup kemampuan untuk bekerja dengan data interval dari luar. Tapi ini pasti topik untuk artikel lain.




All Articles