Combination & Cache 架构设计准则(2019)
Categories:
传统的 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 (例外代码) | 定义例外代码,可以统一管控当例外发生错误时,回传的错误代码 | 
架构存取限制
- 不能跨 2 阶层以上存取
- Controller 不能存取 Repository
 - Controller 不能存取 Validator
 - Service 不能存取 Model
 
 - 低阶层的不能存取高阶层的资料
- Model 不能存取 Repository
 - Repository 不能存取 Service
 - Validator 不能存取 Checker
 
 - 同一个资料类型,不能互相呼叫
- 避免同一类型类别呼叫,造成 new 物件的时候有无穷迴圈
- PostService 存取 UserService,UserService 存取 PostsService 造成无穷迴圈
 
 - ServiceManager 不能呼叫 ServiceManager
 - Service 不能呼叫 Service
 - Checker 不能呼叫 Checker
 - Validator 不能呼叫 Validator
 - Repository 不能呼叫 Repository
 - CacheManager 不能呼叫 CacheManager
 
 - 避免同一类型类别呼叫,造成 new 物件的时候有无穷迴圈
 - 独立结构可以在任何一阶层去呼叫
 
架构设计逻辑范例说明
A. 资料控制结构
Controller (控制器)
| 项目 | 说明 | 
|---|---|
| 用途 | 控制资料流程 | 
| 可以存取结构 | Checker、ServiceManager、Service、DB 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、所有独立结构 | 
| 可以被存取结构 | Controller、ServiceManager | 
使用不同的 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 资料成资讯 | 
| 可以存取结构 | 所有独立结构 | 
| 可以被存取结构 | Serivce、CombinationManager | 
当 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 的资料成资讯 | 
| 可以存取结构 | Combination、Serivce、所有独立结构 | 
| 可以被存取结构 | ServiceManager、Controller | 
当整合的资料需要经过不同的资料来源去判断要产生什麽複合资讯,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, foreach 跟 echo 列印 的程式,仅需要将资料呈现在对的 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>