Combination & Cache 架構設計準則(2019 年版本)

傳統的 MVC(Model, View, Controller) 框架,當 Controller 收到請求之後,我們會在 Controller 內直接透過 Model 去撈取資料庫的資料,並在 Controller 做資料驗證、資料整合、快取、商業邏輯判斷…等等的工作。

當系統越來越大,會發現很多類似的商業邏輯的程式都散在各地,沒有辦法重複再利用,當程式需要異動或修改的時候,就要去搜尋所有程式碼,把許多相同商業邏輯的程式碼去做異動,但需要修改的地方若太多,往往會東漏西漏,導致系統出現錯誤,並造成往後開發的時間成本增加。

所以我們會想要做到 減少重複的程式碼提高維護開發的效率,所以將程式碼依照分類分層抽出獨立控管,讓不同類型的程式專心處理自己相關的商業邏輯,讓開發維護更容易。

隨著程式架構的演進會發展出更多不同的架構,所以這個設計架構準則也是會隨著時間做演進的。

資料處理邏輯分層

架構圖

架構圖

架構說明

A. 資料控制結構

* Controller (控制器:控制資料流程)
    * ServiceManager (服務整合管理:組合管理不同 Service 的商業邏輯)
        * Service (服務:處理商業邏輯)
            * Repository (資源庫:資料表資料撈取邏輯)
                * Model (資料庫模型:資料表設定)
                    * Presenter (資料呈現:資料表資料格式呈現轉換)
            * Combination(資料整合:整理 Repository 資料成資訊)
    * CombinationManager(複合資料整合管理:整理多個 Service 的資料成資訊)
* Checker (檢查器:根據 Controller 所需商業邏輯,驗證不同資料表欄位資料)
    * Validator (驗證器:資料表欄位資料驗證)
結構名稱 說明
Controller (控制器) 控制資料流程,控制要使用哪些 Service 或 ServiceManager 的商業邏輯,去組合出使用者請求需要的資料,並做資料的資料交易控制 (transaction) ,並使用 Checker 去檢查任何使用者傳進來的資料,確保資料的正確性
ServiceManager (服務整合管理) 協助 Controller 組合不同 Service 的資料成商業邏輯
Service (服務) 處理商業邏輯,組合不同的 Repository 資料成商業邏輯,提供 Controller 或 ServiceManager 存取
Repository (資源庫) 資料表資料撈取邏輯,撈取屬於自己 Model 不同條件下的資料,提供 Service 存取
Model (資料庫模型) 資料庫模型,資料表存取相關設定
Presenter (資料呈現) 資料呈現,協助 Model 做資料呈現轉換
Checker (檢查器) 協助 Controller 做資料驗證,在資料進入到程式邏輯前,都需要經過 Checker 將資料格式做驗證
Validator (驗證器) 協助 Checker 做資料驗證,Validator 只能驗證單一 Model 資料
CombinationManager (複合資料整合管理) 協助整理不同 Service 的複合式資料,若有資料的邏輯判斷需要不同的資料來源,則由 CombinationManager 負責整合處理
Combination (資料整合) 協助整理 Repository 資料成資訊

B.獨立結構

* CacheManager (快取:管理資源快取鍵值及清除快取)
* Constant (常數:定義資料狀態名稱)
* Support (支援:協助處理獨立邏輯資料處理)
* ExceptionCode (例外代碼:例外錯誤代碼定義)
結構名稱 說明
CacheManager (快取) 協助專案資料做快取資料的控制,可以在任何程式邏輯複雜的地方做快取存取控制,並統一清除快取
Constant (常數) 定義並命名所有資料狀態,確保資料值做異動時,不會影響程式邏輯
Support (支援) 協助處理獨立程式邏輯,邏輯沒有被其他任何的函式綁定,可以獨立完成
ExceptionCode (例外代碼) 定義例外代碼,可以統一管控當例外發生錯誤時,回傳的錯誤代碼

架構存取限制

  1. 不能跨 2 階層以上存取
    • Controller 不能存取 Repository
    • Controller 不能存取 Validator
    • Service 不能存取 Model
  2. 低階層的不能存取高階層的資料
    • Model 不能存取 Repository
    • Repository 不能存取 Service
    • Validator 不能存取 Checker
  3. 同一個資料類型,不能互相呼叫
    • 避免同一類型類別呼叫,造成 new 物件的時候有無窮迴圈
      • PostService 存取 UserService,UserService 存取 PostsService 造成無窮迴圈
    • ServiceManager 不能呼叫 ServiceManager
    • Service 不能呼叫 Service
    • Checker 不能呼叫 Checker
    • Validator 不能呼叫 Validator
    • Repository 不能呼叫 Repository
    • CacheManager 不能呼叫 CacheManager
  4. 獨立結構可以在任何一階層去呼叫

架構設計邏輯範例說明

A. 資料控制結構

Controller (控制器)

項目 說明
用途 控制資料流程
可以存取結構 CheckerServiceManagerServiceDB transaction所有獨立結構
可以被存取結構

處理 HTTP 請求的入口,依照需求呼叫 ServiceManager 或 Service 去做資料的存取,大部分情況呼叫 Service 去組合需要的資料就好,若相同的組合邏輯在不同的 Controller 都有用到,那就使用 ServiceManager 去組合不同的 Service

要確保所有 Service 商業邏輯都正確跑完才允許對資料做異動,並避免 Transaction 在 Controller 及 Service 被重複呼叫,導致無法正確鎖定資料狀態,所以使用 Controller 當作資料交易(Transaction)的控制點

<?php

class PostController extends Controller
{
    public function __construct(
        PostServiceManager $PostServiceManager,
        PostService $PostService,
        CommentService $CommentService,
        PostChecker $PostChecker
    )
    {
        $this->PostServiceManager = $PostServiceManager;
        $this->PostService = $PostService;
        $this->CommentService = $CommentService;
        $this->PostChecker = $PostChecker;
    }

    // 顯示文章
    public function show($post_id) {
        try {
            // 驗證資料
            $input = [
                'post_id' => $post_id
            ];
            $this->PostChecker->checkShow($input);

            // 撈取文章
            $Post = $this->PostServiceManager->findPost($post_id);

            // 撈取文章留言
            $Comment = $this->CommentService->getCommentByPostId(post_id);
        } catch (Exception $exception) {
            throw $exception
        }
    }

    // 更新文章
    public function update($post_id) {
        try {
            // 驗證資料
            $input = request()->all();
            $input['post_id'] = $post_id;
            $this->PostChecker->checkUpdate($input);

            // 交易開始
            DB::beginTransaction();

            // 更新文章
            $Post = $this->PostService->update($post_id, $input);

            // 交易結束
            DB::commit();
        } catch (Exception $exception) {
            // 交易失敗
            DB::rollBack();
            throw $exception
        }
    }
}

ServiceManager (服務整合管理)

項目 說明
用途 組合管理不同 Service 的商業邏輯
可以存取結構 Service所有獨立結構
可以被存取結構 Controller

使用不同 Service 撈取資料,將不同資料組合成商業邏輯,供 Controller 做存取

<?php

class PostServiceManager {
    public function __construct(
        PostService $PostService,
        UserService $UserService
    )
    {
        $this->PostService = $PostService;
        $this->UserService = $UserService;
    }

    // 撈取文章資料
    public function findPost($post_id){
        try {
            // 撈取文章
            $Post = $this->PostService->findPost($post_id);
            // 撈取文章作者資料
            $user_id = $Post->user_id;
            $Post->user = $this->UserService->findUser($user_id);

            return $Post;
        } catch (Exception $exception) {
            throw $exception
        }
    }
}

Service (服務)

項目 說明
用途 處理商業邏輯
可以存取結構 Repository所有獨立結構
可以被存取結構 ControllerServiceManager

使用不同的 Repository 撈取資料,將不同資料組合成商業邏輯

<?php

class PostService {
    public function __construct(
        PostRepository $PostRepository,
        PostTagRepository $PostTagRepository
    )
    {
        $this->PostRepository = $PostRepository;
        $this->PostTagRepository = $PostTagRepository;
    }

    // 撈取文章
    public function findPost($post_id) {
        try {
            // 撈取文章
            $Post = $this->PostRepository->find($post_id);
            // 撈取文章標籤
            $Tag = $this->PostTagRepository->getByPostId($post_id);

            return [$Post, $Tag];
        } catch (Exception $exception) {
            throw $exception
        }
    }
}

Repository (資源庫)

項目 說明
用途 資料表資料撈取邏輯
可以存取結構 Model所有獨立結構
可以被存取結構 Service

撈取特定 Model 資料,像 PostRepository 可以存取 Post Model (模型) 的 基本資料,並使用不同條件撈取 Model 的資料,供 Service 做存取

也可以使用 PostRecommendRepository 存取 Post Model (模型) 的 推薦資料

同一個 Model (模型) 可以用不同的 Repository 去呼叫,但同一 Repository 只能有一個 Model (模型)

<?php

class PostRepository {
    public function __construct(
        Post $Post
    )
    {
        $this->Post = $Post;
    }

    public function find($post_id) {
        try {

            // 撈取資料庫文章資料
            $Post = $this->Post->find($post_id);

            return $Post;
        } catch (Exception $exception) {
            throw $exception
        }
    }

    public function findLatestPost() {
        try {
            // 撈取資料庫文章資料
            $Post = $this->Post
                ->order('created_at', 'desc')
                ->first();

            return $Post;
        } catch (Exception $exception) {
            throw $exception
        }
    }
}

Model (資料庫模型)

項目 說明
用途 資料表設定
可以存取結構 所有獨立結構
可以被存取結構 Repository

Eloquent 存取資料表相關設定,使用 Eloquent 直接存取資料表資料

<?php

class Post extends Model
{
    protected $table = 'post';

    protected $fillable = [];

    protected $primaryKey = 'id';

    protected $dates = ['created_at', 'updated_at'];

    protected $presenter = PostPresenter::class;
}

Presenter (資料呈現)

項目 說明
用途 資料表資料格式呈現轉換
可以存取結構 所有獨立結構
可以被存取結構 Model

提供 Model 的資料用其他方式呈現

<?php

class PostPresenter extends Presenter
{
    public function created_at_human_time()
    {
        return $this->created_at->diffForHumans();
    }
}

Checker (檢查器)

項目 說明
用途 根據 Controller 所需商業邏輯,驗證不同資料表欄位資料
可以存取結構 Validator所有獨立結構
可以被存取結構 Controller

協助 Controller 驗證不同資料表資料的正確性,若驗證錯誤則丟處例外,Controller 根據例外代碼去做處理

<?php

class PostValidator {
    public function checkFindPost($input){
        // 驗證文章資料
        $this->PostValidator->validatePostId($input);
        $this->PostValidator->validatePostContent($input);

        // 驗證會員資料
        $this->MemberValidator->validateMemberId($input);
    }
}

Validator (驗證器)

項目 說明
用途 資料表欄位資料驗證
可以存取結構 所有獨立結構
可以被存取結構 Checker

協助 Checker 驗證資料的正確性,若驗證錯誤則丟處例外,Checker 根據例外代碼去做處理

<?php

class PostValidator {
    public function validatePostId($input){
        // 設定驗證規則
        $rules = [
            'post_id' => [
                'required',
                'max:20',
            ],
        ];

        // 開始驗證
        $this->validator = Validator::make($input, $rules);

        if ($this->validator->fails()) {
            throw new Exception(
                '文章編號格式錯誤',
                PostExceptionCode::POST_ID_FORMAT_ERROR
            );
        }
    }
}

Combination(資料整合)

項目 說明
用途 整理 Repository 資料成資訊
可以存取結構 所有獨立結構
可以被存取結構 SerivceCombinationManager

當 Service 從 Repository 取得資料後,協助整理判斷 Repository 資料的屬性狀態,像是可以從 文章編號 取得 文章網址

<?php

class PostsCombination {
    // 設定整合資訊
    public function setCombinationInfo(&$Posts)
    {
        if (!($Posts instanceof Posts)) {
            return false;
        }

        // 文章網址
        $url = url("article/{$Posts->id}")

        $Posts->info->url = $url;
    }
}

CombinationManager(複合資料整合管理)

項目 說明
用途 整理多個 Service 的資料成資訊
可以存取結構 CombinationSerivce所有獨立結構
可以被存取結構 ServiceManagerController

當整合的資料需要經過不同的資料來源去判斷要產生什麼複合資訊,CombinationManager 協助整理不同來源的資料去做資料整合,目前會從 ServiceManager 去取得不同 Service 的資訊,所以將 CombinationManager 放在這一階層去進行呼叫

<?php

class PostsCombinationManager {
    protected $UserService;
    public function __construct(
        UserCombination $UserCombination,
        ProjectService $ProjectService
    ) {
        // 服務
        $this->UserCombination = $UserCombination;
    }

    public function setCombinationInfo(&$combination_data)
    {
        $Posts = array_get($combination_data, 'Posts');
        if ($Posts instanceof Posts) {
            // 設定文章關聯作者資訊
            $this->UserCombination->setCombinationInfo($Posts->User);

            // 是專題文章
            if ($Posts->type == PostsConstant::TYPE_PROJECT) {
                $Project = $this->ProjectService->findProjectByPostId($Posts->id);
                $url = url("project/{$Project->slug}/{$Posts->id}")
                $Posts->info->url = $url;
            };
        }
    }
}

B.獨立結構

CacheManager (快取)

項目 說明
用途 管理資源快取鍵值及清除快取
可以存取結構 x
可以被存取結構 無限制

複雜 的資料庫查詢(Repository)或是商業邏輯(Service、ServiceManager),想要在一定時間內不要再重複的進行複雜的運算,可以透過快取將運算的結果快取起來

PostsCacheManager 文章資源庫

class PostRepository {
    public function __construct(
        Post $Post,
        PostsCacheManager $PostsCacheManager
    )
    {
        $this->Post = $Post;
        $this->PostsCacheManager = $PostsCacheManager;
    }

    public function find($post_id) {
        try {
            $cache_key = $this->PostsCacheManager->getPostIdCacheKey($post_id);
            $Posts = $this->PostsCacheManager->getCache($cache_key);
            if (!is_null($Posts)) {
                return $Posts;
            }

            // 撈取資料庫文章資料
            $Posts = $this->Post->find($post_id);

            if (!is_null($Posts)) {
                // 有該資料,將資料存入快取
                $this->PostsCacheManager->putCache($Posts, $cache_key);
            }

            return $Posts;
        } catch (Exception $exception) {
            throw $exception
        }
    }

    public function findLatestPost() {
        try {
            // 撈取資料庫文章資料
            $Post = $this->Post
                ->order('created_at', 'desc')
                ->first();

            return $Post;
        } catch (Exception $exception) {
            throw $exception
        }
    }
}

PostsCacheManager 文章快取

class PostsCacheManager {
    protected $cache_key = [
        // 文章快取
        'post_id' => '[PostById][post_id:{post_id}]',
        // 已發布文章快取
        'published_post_id' => '[PublishedPostById][post_id:{post_id}]',
    ];

    /**
     * 文章快取
     */
    public function getPostIdCacheKey($post_id)
    {
        $search = [
            '{post_id}',
        ];
        $replace = [
            $post_id,
        ];
        $cache_key = str_replace($search, $replace, $this->cache_key['post_id']);

        return $cache_key;
    }

    /**
     * 已發布文章快取
     */
    public function getPublishedPostIdCacheKey($post_id)
    {
        $search = [
            '{post_id}',
        ];
        $replace = [
            $post_id,
        ];
        $cache_key = str_replace($search, $replace, $this->cache_key['published_post_id']);

        return $cache_key;
    }

    /**
     * 文章快取
     */
    public function getPostIdCacheKey($post_id)
    {
        $search = [
            '{post_id}',
        ];
        $replace = [
            $post_id,
        ];
        $cache_key = str_replace($search, $replace, $this->cache_key['post_id']);

        return $cache_key;
    }


    /**
     * 清除文章快取
     */
    public function forgetPostsCache($cache_data)
    {
        $Posts = array_get($cache_data, 'Posts');


        if (!is_null($Posts) AND ($Posts instanceof Posts)) {
            $cache_key = $this->getPostIdCacheKey($post_id);
            $is_cache_forget = $Cache::forget($cache_key);

            $cache_key = $this->getPublishedPostIdCacheKey($post_id);
            $is_cache_forget = $Cache::forget($cache_key);

            // .... 清除文章其他快取
        }
    }
}

Constant (常數)

項目 說明
用途 定義資料狀態名稱
可以存取結構 x
可以被存取結構 無限制

資料皆為靜態變數,可以供所有資料層級 (e.g. Controller、Service、Repository) 做存取

<?php

class PostConstant {
    const POST_TYPE_PUBLIC = 'P';
    const POST_TYPE_DELETE = 'D';
}

Support (支援)

項目 說明
用途 協助處理獨立邏輯資料處理
可以存取結構 x
可以被存取結構 無限制

方法皆為靜態變數,可以供所有資料層級 (e.g. Controller、Service、Repository) 做存取

若有其他可供全域共用的方法皆寫在 Support 靜態方法供大家存取

<?php

class PostSupport {
    // 撈取所有文章類型
    public static function getAllPostType() {
        $all_post_type = [
            PostConstant::POST_TYPE_PUBLIC,
            PostConstant::POST_TYPE_DELETE,
        ];
        return $all_post_type;
    }
}

ExceptionCode (例外代碼)

項目 說明
用途 例外錯誤代碼定義
可以存取結構 x
可以被存取結構 x

資料皆為靜態變數,可以供所有資料層級 (e.g. Controller、Service、Repository) 做存取

<?php

class PostExceptionCode {
    const POST_ID_FORMAT_ERROR = 10000001;
    const POST_NOT_FOUND = 10000002;
    const POST_TAG_NOT_FOUND = 10000003;
}

View (視圖) 使用限制

View 的職責是負責顯示資料,所有的資料應由 Controller 準備好再傳給 View,所以不要View 內有複雜的程式判斷邏輯,在 View 裡面只有 if, for, foreachecho 列印 的程式,僅需要將資料呈現在對的 HTML 裡面,不要再對資料重複處理過。

像是文章的網址可能會因為類型不同會有不同的網址,像是一般文章網址可能為 http://kejyun.com/post/1,而影音文章網址可能為 http://kejyun.com/video/2,兩者的資料皆為 Post 資料表的資料,在 View 中要顯示網址應為 echo $Post->post_url; 將網址印出,post_url 則是在傳給 View 之前就經過邏輯判斷的資料,而不是在 View 中判斷不同文章類型(PostConstant::POST_TYPE_NORMAL, PostConstant::POST_TYPE_VIDEO)在 View 中顯示不同的網址資料。

之後若文章網址邏輯需要修改,則需要到各個 View 中去修改,很容易漏改道造成系統程式出錯

<a href="{{ $Post->info->post_url }}"> {{ $Post->Title }}</a>

KeJyun 最新新書推薦
- Laravel 5 for beginner 新手道場:優雅運用框架快速開發 PHP 網站
- Laravel框架开发详解:从零基础到运用框架快速开发PHP网站

Laravel 是 PHP 的框架(Framework),提供了很多開發網站或 API 所需的工具及環境,經過簡單的設定就可以完成資料的處理及顯示,使開發者可以很優雅且快速的開發出各個不同的產品。本書適合有 PHP 基礎的人,但不知道要怎麼選擇框架,或者不用框架的人也能夠明白它的好處。

雖然 WordPress 也能夠架站,但如果有客製化需求,要開發各式各樣的網站,或提供 App 使用的 API,如此一來你只能選擇用框架,而 Laravel 是目前最受歡迎的。

本書將解說為什麼要使用框架,以及理解框架的優缺點後,要怎麼選擇框架,並用框架快速建構一個網站。除非必要,否則書中會避免專業技術用語,盡量使用最生活化易懂的例子及語氣,讓大家更容易進入 Laravel 的世界。

Laravel 5 for beginner 新手道場:優雅運用框架快速開發 PHP 網站

購書連結

Laravel框架开发详解:从零基础到运用框架快速开发PHP网站

購書連結

comments powered by Disqus