Combination & Cache 架构设计准则(2019)

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>