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 (例外代码) | 定义例外代码,可以统一管控当例外发生错误时,回传的错误代码 |
架构存取限制
- 不能跨 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>