处理复杂的 MVC 应用程序 - 如何扩展并避免控制器混乱
介绍
文章封面图片由undabot.com提供
本文使用Laravel作为代码片段,但该范例可以轻松适应其他所有 MVC 框架。
为了让事情变得更有趣,我们将通过发布两位专业人士之间的假想对话来布局这篇文章:
斯坦(Stan ) 是一位经验丰富的开发人员,他犯过许多架构错误(但值得庆幸的是,他似乎正在从中吸取教训),并且
Ollie是一名新手开发人员,刚刚开始深入研究严肃的编程世界,并且有一些简单的应用程序。
对话如下:
介绍
[Stan]嗨,Ollie!今天我想跟你聊聊MVC。MVC是一种流行的架构模式,用于构建健壮的应用程序。
[Ollie]等等等等……MVC?那是什么?
[Stan] MVC 代表模型 - 视图 - 控制器。它本质上定义了一种健壮的代码设计策略。它将应用程序分解成不同的部分,以便在应用程序模块之间实现更高程度的关注点分离。
模块化代码在添加更多功能或维护代码时非常有用。
我们来看下面的图:
图片取自维基百科
[Stan]因此,用户与视图交互,在每次交互时,都会调用相关的控制器方法,然后负责更新/获取适当的数据返回给用户(再次使用视图层)。
[Ollie]明白了!我已经在很多项目中用过 MVC 了。我对此非常了解!
典型代码示例
[Stan] Ollie,别急,还有很多内容要讲。
现在,我们来看看一个我们经常遇到的 Laravel 控制器方法:
<?php
namespace App\Http\Controllers;
class HomeController extends Controller {
public function simpleMethod() {
$books = Books:all();
foreach($books as $book) {
// some business logic here...
}
return view('home.home')->with(['books' => $books]);
}
}
[Ollie]好的,这段代码看起来很简单!
[斯坦]如果我们把它分解成单独的组件,效果会怎样?
[Ollie]我们到底为什么要这么做?我喜欢那段代码;简单又能完成任务。它有什么特别之处吗?
[Stan]我的小徒弟,这里面有一点很特别!它叫做“复杂应用程序”。
你看,在一个相对较小的项目中,把所有东西都放在一个控制器方法里完全没问题。
[Ollie]等等等等……你说的一切是什么意思……?
层来救援
[Stan ] 我感觉你没有理解大多数应用程序是如何工作的……在典型的 MVC 应用程序中,我们有以下层:
-
验证层
-
业务逻辑层
-
数据库/存储库层
-
错误处理层
-
成功/错误显示层
说实话,MVC 应用程序可以进一步细分为更多层,但这些是最基本的层。大多数教程忽略它们也没关系。他们的工作不是解释如何对应用程序进行分层,而是解释本教程的主题。
[Ollie]好的……那我为什么要拆分我的应用程序呢?如果我想添加一些功能,我可以在我的 Controller 方法中实现。
随着时间的推移,代码变得腐烂
[Stan]当然你可以继续这样做,但在某种程度上它会导致代码重复、极大的控制器方法和类,以及总体上腐烂的代码。
这就是为什么我说在复杂的应用程序中分层效果更好。
如果您不打算扩展您的项目,那么您可能可以将所有内容保留在您的控制器方法中。
随着应用程序的增长,维护难度也随之增加。随着复杂性的增加,可复用模块的价值也随之提升。我们知道,我们必须采取措施,以免承担技术债务的风险,因为技术债务反过来又会导致维护和后续开发的难度加大。
让我们看一下前面示例中的代码,经过一段时间添加更多功能之后;
<?php
namespace App\Http\Controllers;
class BooksController extends Controller {
public function complexMethod(HttpRequest $request) {
$authorId = $request->author_id;
if(!Author::find($authorId))
// wrong data
return back()->with('error','Wrong data');
try {
// this DB query needs to be duplicated if we want
// to use it in another part of the code
$books = Books::with('autor')
->with('reviews')->where(['author_id' => $authorId])
->get();
foreach($books as $book) {
// some business logic here...
// this code snippet can turn out to be huge,
// since it grows with the application complexity.
}
return view('home.home')->with(['books' => $books]);
} catch (Exception $e) {
return back()->with('error', $e->getMessage());
}
}
}
[Ollie]哎呀,你说得对!我希望我的应用程序能够扩展!我该如何将我的代码拆分成更多层呢?
[Stan]我也是这么想的。本质上,控制器的工作是与视图层(MVC 中的 V)交互。这意味着它应该处理用户输入,并返回要显示给用户的数据。
不多不少。
每一层由谁负责?
话虽如此,让我们修改一下这些层,看看哪些应该保留在控制器中,哪些应该分开:
-
验证层:
此层负责验证用户输入的数据。在 MVC 图中,它位于视图层附近,因此应该在控制器类中实现。 -
业务逻辑层:
这一层通常会随着时间的推移变得过于复杂。它定义了应用程序的业务规则,与视图无关。因此,我们需要将其与控制器解耦,并将其打包到业务逻辑层的另一个类中。 -
数据库/存储库层
此层包含应用程序的数据库查询。在许多数据密集型的复杂应用程序中(例如实时系统),此层本身也可能是一个不同的应用程序。因此,它不应该在控制器中实现,而应该在数据库/存储库层中的另一个类中实现。 -
错误处理层:
当异常抛出时,我们应该怎么做?这要视情况而定。我们可能想将异常记录到日志通道中,并采取一些特殊的处理措施。
在大多数 MVC 应用程序中,我们希望将错误告知用户,因此该层应该在业务逻辑层和控制器层中实现。 -
成功/错误显示层:
此层与上一层紧密相连。当操作成功或抛出异常时,及时通知用户至关重要。此层定义在控制器 (Controller) 和视图 (View) 之间,可在控制器类中实现。
[Ollie]哇,我学到了这么多!但我还是有点困惑;我的控制器现在应该是什么样子?
分层代码
[Stan]好问题!请看下面的例子:
<?php
namespace App\Http\Controllers;
class BooksController extends Controller {
protected $bookManager;
function __construct() {
$this->bookManager = new BookManager();
}
public function complexMethod(HttpRequest $request) {
// VALIDATION LAYER
// having all rules in a separate validationRules method
// allows reusage
$validator = Validator::make($request->all(), $this->validationRules($request));
if ($validator->fails()) {
return back()->with('error','Wrong data');
}
try {
// BUSINESS LOGIC LAYER
$books = $this->bookManager->getAllBooksForAuthor($request->author_id);
// SUCCESS DISPLAY LAYER
return view('home.home')->with(['books' => $books]);
} catch (Exception $e) {
//ERROR DISPLAY LAYER
return back()->with('error', $e->getMessage());
}
}
}
[Ollie]但是业务逻辑方法和数据库/存储库层方法在哪里?
[Stan]当然是在其他类中定义!面向对象编程的一大优点就是能够将不同的模块打包成单独的类,并通过创建这些类的实例来使用它们。
(参见最后一个例子中的构造函数)。
让我们看看我们的BookManager class
:
<?php
namespace App\BusinessLogicLayer\;
use App\StorageLayer\BookRepository;
class BookManager {
protected $bookRepository;
const PUBLISHED_BOOK_STATE = 1;
public function __construct() {
$this->bookRepository = new BookRepository();
}
public function getAllBooksForAuthor($authorId) {
// here we can add all the business logic:
// for example, we can check whether the author has any
// books that are in a DRAFT state, or we can check if the
// author is the same user as the logged in user, in order
// to display more data.
$books = $this->booksRepository->getAllBooks([
'state' => self::PUBLISHED_BOOK_STATE,
'author_id' => $authorId
]);
foreach($books as $book) {
// business logic here
}
return $books;
}
}
[Ollie]但是我在这里也没有看到任何 DB 查询。
[Stan]没错!数据库查询是在 Repository/Storage 层,记得吗?看看下面的类:
<?php
namespace app\StorageLayer;
use app\models\Book;
class BookRepository {
public function getAllBooks($attributesArray) {
return Book::where(attributesArray);
}
}
展望未来
[Ollie]这个类是不是太小了?我们不能直接省略它,把数据库查询放到业务逻辑层吗?
[Stan]如果我们想要正确扩展的话,那就不行。记住,随着项目的增长,我们可能需要在我们的类中添加复杂的数据库查询,甚至需要将 Repository 层迁移到一个完全独立的项目中(即使在不同的服务器上)。
[Ollie]哇,我真的没想到这么多!你说得对。这段代码看起来简洁又可扩展。我想这应该能让添加更多功能变得更容易。
[Stan]没错。在复杂的应用中,遵循开放-封闭原则至关重要:
“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”
因此,即使您很久以前就编写了一个 Controller 方法,并且想要编写另一个方法来重用其他类中定义的一些层,您也可以简单地在新的 Controller 方法中调用相关方法。
[Ollie]太酷了!我感觉现在就可以攀登了!
鏂囩珷鏉ユ簮锛�https://dev.to/pavlosisaris/handling-complex-mvc-applications----how-to-scale-and-avoid-controller-chaos-lb9