H

Handling complex MVC applications - How to scale and avoid Controller chaos Introduction

2025-06-10

处理复杂的 MVC 应用程序 - 如何扩展并避免控制器混乱

介绍

文章封面图片由undabot.com提供

本文使用Laravel作为代码片段,但该范例可以轻松适应其他所有 MVC 框架。

为了让事情变得更有趣,我们将通过发布两位专业人士之间的假想对话来布局这篇文章:

斯坦(Stan ) 是一位经验丰富的开发人员,他犯过许多架构错误(但值得庆幸的是,他似乎正在从中吸取教训),并且

Ollie是一名新手开发人员,刚刚开始深入研究严肃的编程世界,并且有一些简单的应用程序。

对话如下:

介绍

[Stan]嗨,Ollie!今天我想跟你聊聊MVC。MVC是一种流行的架构模式,用于构建健壮的应用程序。

[Ollie]等等等等……MVC?那是什么?

[Stan] MVC 代表模型 - 视图 - 控制器。它本质上定义了一种健壮的代码设计策略。它将应用程序分解成不同的部分,以便在应用程序模块之间实现更高程度的关注点分离。
模块化代码在添加更多功能或维护代码时非常有用。
我们来看下面的图:

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]);  
  }  
}

Enter fullscreen mode Exit fullscreen mode

[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());
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

[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());
      }

  }

}
Enter fullscreen mode Exit fullscreen mode

[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;
    }
}
Enter fullscreen mode Exit fullscreen mode

[Ollie]但是我在这里也没有看到任何 DB 查询。

[Stan]没错!数据库查询是在 Repository/Storage 层,记得吗?看看下面的类:


<?php 

namespace app\StorageLayer;
use app\models\Book;

class BookRepository {

    public function getAllBooks($attributesArray) {  
        return Book::where(attributesArray);  
    }

}
Enter fullscreen mode Exit fullscreen mode

展望未来

[Ollie]这个类是不是太小了?我们不能直接省略它,把数据库查询放到业务逻辑层吗?

[Stan]如果我们想要正确扩展的话,那就不行。记住,随着项目的增长,我们可能需要在我们的类中添加复杂的数据库查询,甚至需要将 Repository 层迁移到一个完全独立的项目中(即使在不同的服务器上)。

[Ollie]哇,我真的没想到这么多!你说得对。这段代码看起来简洁又可扩展。我想这应该能让添加更多功能变得更容易。

[Stan]没错。在复杂的应用中,遵循开放-封闭原则至关重要

“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”

因此,即使您很久以前就编写了一个 Controller 方法,并且想要编写另一个方法来重用其他类中定义的一些层,您也可以简单地在新的 Controller 方法中调用相关方法。

[Ollie]太酷了!我感觉现在就可以攀登了!

规模项目模因

在此处输入图片描述

鏂囩珷鏉ユ簮锛�https://dev.to/pavlosisaris/handling-complex-mvc-applications----how-to-scale-and-avoid-controller-chaos-lb9
PREV
PHP 8 来了!朝着正确方向迈出的一步?新功能介绍
NEXT
使用 Terraform 管理基础设施资源