Photo 1627915863310 649f5c62ea01

週末小挑戰:後端工程師的日期範圍生成器實作日

在這個放鬆的週末裡,我和幾位同事決定抽空來點不一樣的活動,目標是為了提升我們後端開發的實戰能力。這次我們挑選的題目是「日期範圍生成器」,一個看似簡單但實則藏著不少挑戰的任務。作為團隊裡資歷稍深的一員,我負責引領這次的實作旅程。首先,我產出了一些基礎的程式碼,為這個項目打下了初步的框架。這部分還算是水到渠成,畢竟是些基本的設定,沒什麼大問題。

一開始初步構想php 日期列印動作 ,例如 04/01, 04/02, 04/03, 04/04…. 04/30 的列印格式,於是使用DateTimeDateInterval類。以下是一個示例代碼,展示如何列印出4月份的每一天,格式為 04/01, 04/02, ..., 04/30

PHP
<?php

// 設定起始日期為4月1日
$start = new DateTime('2024-04-01');
// 設定結束日期為4月30日
$end = new DateTime('2024-04-30');

// DateInterval設定為1天,P1D表示每次增加1天
$interval = new DateInterval('P1D');
// DatePeriod將會使用上述的起始日期、間隔和結束日期來創建一個範圍
$period = new DatePeriod($start, $interval, $end->modify('+1 day')); // 在結束日期加一天是為了包含4月30日

foreach ($period as $date) {
    // 格式化日期並列印,格式為 04/01, 04/02, ..., 04/30
    echo $date->format("m/d") . PHP_EOL;
}

隨著基礎結構完成,我們開始著手進行結構上的調整,這是為了讓整個程式碼更加模組化、更易於維護。不過,這個階段我們遇到了不少挑戰。為了提升程式碼的靈活性和重用性,我決定引入物件導向的概念。這對於一些同事們來說是個稍微陌生的領域,我們在實作過程中不斷地討論、嘗試,甚至是爭論。這個過程雖然充滿挑戰,但也讓團隊的合作更加緊密,大家的技能也得到了提升。

於是有了第二段的結構,這邊開始把功能分別置放。

PHP
<?php

class DatePrinter {
    private $startDate;
    private $endDate;

    // 構造函數初始化起始和結束日期
    public function __construct($start, $end) {
        $this->startDate = new DateTime($start);
        $this->endDate = new DateTime($end);
    }

    // 生成並列印日期範圍
    public function printDates() {
        $interval = new DateInterval('P1D'); // 設置間隔為1天
        $end = $this->endDate->modify('+1 day'); // 包括結束日期
        $period = new DatePeriod($this->startDate, $interval, $end);

        foreach ($period as $date) {
            echo $date->format("m/d") . PHP_EOL;
        }
    }
}

// 創建DatePrinter對象並列印4月份的日期
$datePrinter = new DatePrinter('2024-04-01', '2024-04-30');
$datePrinter->printDates();

我定義了一個名為 DatePrinter 的類別,目的是要處理跟列印日期範圍有關的功能。在這個類別裡,我設定了兩個私有屬性,$startDate 和 $endDate,用來存放起始日期和結束日期。

透過構造函數 __construct,當我建立 DatePrinter 類別的實例時,可以傳入起始和結束日期,並將這兩個日期轉換為 PHP 的 DateTime 物件,這樣方便後續的日期處理。

接著,我實作了一個名為 printDates 的方法。這個方法首先建立了一個每次增加一天的日期間隔 DateInterval 物件,並將結束日期擴展一天(這麼做是為了包含結束日期在內的日期範圍)。然後,我使用了 DatePeriod 來根據起始日期、間隔,以及修改過的結束日期來生成一個日期範圍。

最後,我使用 foreach 迴圈來遍歷這個日期範圍,並透過 echo 與 PHP_EOL (代表換行符號)來列印出每一天的日期,格式是月/日。

雖然這樣可以重複使用DatePrinter物件,但這樣還不夠好….,而且「日期範圍生成器」這題目,如果只是列印「日期」那好像也僅此而已。

在這情境下探討設計模式

當物件的結構逐步成型之後,我提出了進一步提升程式的可擴充性。我們想讓這個「日期範圍生成器」不僅僅適用於目前的需求,還能夠輕鬆地應對未來可能增加的新功能。然而,這一提議雖好,實施起來卻不是那麼容易。我們在實作中遇到了不少思維上的瓶頸,有時甚至覺得這個目標似乎有些過於理想化。但透過不斷地回顧程式碼、優化架構,甚至是重新設計某些模組,我們終於找到了解決方案。

這邊我先分兩個部分「資料參數」與「業務邏輯」

  • 資料參數:就是我們的「日期範圍」
  • 業務邏輯:就是我們的「列印」

所以這時候我們必須吧參數與邏輯先分開,要將列印細節從DatePrinter類中分離出來,我們可以考慮使用策略模式來實現。這樣,DatePrinter類專注於生成指定範圍內的日期,而將日期列印的責任委託給另一個類別來處理。這樣做不僅使DatePrinter類更加專注於它的主要職責,也提高了代碼的可重用性和可擴展性。

以下是根據這個思路重構後的代碼示例:

首先,我們定義一個列印策略的介面:

PHP
<?php

interface DatePrintStrategy {
    public function print(array $dates);
}

接下來,實現一個簡單的列印策略,用於列印日期列表:

PHP
<?php

class SimpleDatePrint implements DatePrintStrategy {
    public function print(array $dates) {
        foreach ($dates as $date) {
            echo $date->format("m/d") . PHP_EOL;
        }
    }
}

然後是重構後的DatePrinter類,它現在接收一個列印策略物件,並在生成日期後使用該策略進行列印:

PHP
<?php

/**  
 * Class DatePrinter  
 * @package Rewrite\Practice\DataRange  
 * @version 1.0  
 */
class DatePrinter {
    private $startDate;
    private $endDate;
    private $printStrategy;

    // 在構造函數中接收一個列印策略對象
    public function __construct($start, $end, DatePrintStrategy $printStrategy) {
        $this->startDate = new DateTime($start);
        $this->endDate = new DateTime($end);
        $this->printStrategy = $printStrategy;
    }

    public function generateDates() {
        $dates = [];
        $interval = new DateInterval('P1D');
        $end = $this->endDate->modify('+1 day');
        $period = new DatePeriod($this->startDate, $interval, $end);

        foreach ($period as $date) {
            $dates[] = $date;
        }

        return $dates;
    }

    // 使用策略列印日期
    public function printDates() {
        $dates = $this->generateDates();
        $this->printStrategy->print($dates);
    }
}

使用端:

PHP
<?php

$printStrategy = new SimpleDatePrint();
$datePrinter = new DatePrinter('2024-03-01', '2024-03-31', $printStrategy);
$datePrinter->printDates();

到這裡也接近中午吃飯了,但大夥們這時似乎意猶未盡,而且由於思路上的改變,我們很快的就生出了第二版。

這時的物件也分離的很清楚:

DatePrinter類專注於執行列印動作

DateRange類別專注於生成指定範圍內的日期

DatePrintStrategy 介面決定執行的方法

SimpleDatePrint擁有DatePrintStrategy 介面的實際「列印」業務邏輯

好了~那我們就不多廢話,上code !!!

執行列印類別

PHP
<?php

namespace Rewrite\Practice\DataRange;  
  
/**  
 * Class DatePrinter
 * @package Rewrite\Practice\DataRange  
 * @version 2.0  
 */
class DatePrinter  
{  
    /** @var DateRange */  
    private $dateRange;  
  
    /** @var DatePrintStrategy */  
    private $printStrategy;  
  
    /**  
     * @param DateRange $dateRange  
     * @param DatePrintStrategy $printStrategy  
     */  
    public function __construct(DateRange $dateRange, DatePrintStrategy $printStrategy) {  
        $this->dateRange = $dateRange;  
        $this->printStrategy = $printStrategy;  
    }  
  
    /**  
     * Print the date range
     */
	public function print() {  
        $this->printStrategy->print($this->dateRange);  
    }  
}

生成日期範圍類別

PHP
<?php
  
namespace Rewrite\Practice\DataRange;  
 
/**
 * Class DateRange
 * @package Rewrite\Practice\DataRange
 * @version 2.0
 */  
class DateRange
{  
    /** @var \DateTime */  
    private $startDate;  
  
    /** @var \DateTime */  
    private $endDate;  
  
    /**  
     * DateRange constructor.     
     * @param $year  
     * @param $month  
     */  
    public function __construct(int $year, int $month) {  
        // 確保月份始終是兩位數  
        $monthPadded = str_pad($month, 2, '0', STR_PAD_LEFT);  
  
        // 設置月份的第一天為起始日期  
        $start = "{$year}-{$monthPadded}-01";  
        $this->startDate = new \DateTime($start);  
  
        // 設置月份的最後一天為結束日期  
        $this->endDate = new \DateTime($start);  
        $this->endDate->modify('last day of this month');  
    }  

	/**  
	 * 生成日期範圍  
	 *   
     * @return array  
	 */
    public function generateDates() {  
        $dates = [];  
        $interval = new \DateInterval('P1D');  
        $end = $this->endDate->modify('+1 day'); // 包含月份的最後一天  
        $period = new \DatePeriod($this->startDate, $interval, $end);  
  
        foreach ($period as $date) {  
            $dates[] = $date;  
        }  
  
        return $dates;  
    }  
}

介面

PHP
<?php

namespace Rewrite\Practice\DataRange;  
  
interface DatePrintStrategy {  
    public function print(DateRange $dateGenerator);  
}

「基礎列印」業務邏輯

PHP
<?php

namespace Rewrite\Practice\DataRange\Method;  
  
use Rewrite\Practice\DataRange\DatePrintStrategy;  
use Rewrite\Practice\DataRange\DateRange;  
  
class SimpleDatePrint implements DatePrintStrategy  
{  
    /** @var string  */  
    const DATE_FORMAT = "Y-m-d";  
  
    /** @var string  */  
    private $format;  
  
    public function __construct($format = self::DATE_FORMAT)  
    {  
        $this->format = $format;  
    }  
  
    public function print(DateRange $dateGenerator)  
    {  
        $dates = $dateGenerator->generateDates();  
        foreach ($dates as $date) {  
            echo $date->format($this->format) . PHP_EOL;  
        }  
    }  
}
4b35f3ac 38a7 4dd4 8c41 77f9ccfe64fb

使用端

PHP
<?php

$datePrinter = new \Rewrite\Practice\DataRange\DatePrinter(  
    new \Rewrite\Practice\DataRange\DateRange(2024, 4),  
    new \Rewrite\Practice\DataRange\Method\SimpleDatePrint("Y/m/d")  
);  
$datePrinter->print();
Resize

經過一番努力,我們成功地克服了所有的挑戰,「日期範圍生成器」不僅能夠滿足我們當下的需求,還具備了良好的擴充性,能夠迎接未來的挑戰。這次的實作不僅僅提升了團隊的技術能力,更重要的是增強了我們之間的合作與溝通。作為團隊中的一員,我深感欣慰,也期待著我們下一次的技術挑戰。

就準備帶著燒盡的腦袋吃下午茶….

我:嘿~各位,如果這時候需要你們開發一個「月曆」格式的列印功能,你們需要多久….啊~~~(無情的會議室門就這樣關上了…

Resize 1