程序架构讨论:你的应用就是一个扩展的容器
保持代码的组织性,可读性和可维护性很困难,并且没有一种千篇一律的解决方案。 当要求实现新功能时,很容易在现有类中添加越来越多的逻辑,从而增加了维护成本并导致臭名昭著的 “万能对象”,这些对象担负着太多的功能,以致于很难按其名称来推断其工作。虽然有许多工具可以采用设计模式和最佳实践的形式来缓解这些问题,但我们将重点关注一个简单的想法,将应用程序中的每个功能都视为依赖项。
很少有没有任何依赖关系的现代应用程序-我们大多数应用程序至少都使用了类似 Laravel 的框架。依赖关系自然将相关逻辑封装在其自己的名称空间中,并且通常可以添加到应用程序中而不会与现有代码冲突。
将功能分为明确定义的依赖项可以更轻松地遵循单一职责原则(SRP),从而创建更简单、可维护的代码库。团队可以同时处理应用程序的不同部分,而不会因为合并冲突而陷入困境。定义每个功能的边界甚至可以使以后将它们提取到微服务中变得更加容易。
分解扩展包
最基本的扩展包可能只包含一个文件。可以将其代码导入 (通常使用 Composer autoloading) 到任何应用程序对象中。
Laravel 扩展包通常包含一个附加的服务提供者,通过将已配置的对象绑定到应用程序容器中来将该软件包与框架集成在一起。
案例: 租户功能
让我们来看一个使用财务 SaaS 应用程序的示例。我们希望它支持多个用户,因此我们需要实现多租户。
多租户允许您将应用程序数据的范围划分到单个用户,这样用户就不会看到或编辑彼此的数据。这是一个很大的话题,有很多实现方法。
我们假设 John 和 Jane 都可以访问我们的应用程序。他们的账单存储在 bills
表中,其付款存储在 payments
表中。为了防止 John 看到 Jane 的数据和 Jane 看到 John 的数据,我们将通过在每个表中添加一个user_id
列来实现租期。现在,我们可以通过向查询添加 where
条件来限制对 Jane 身份验证用户的bills
和payments
表上的数据的访问。
例如, i如果 Jane 的用户ID为 5
,则用于查询Jane的付款的查询如下所示:
SELECT * from payments WHERE user_id = 5;
要获得有关多租户的深入说明和其他方法,请访问 multitenantlaravel.com 查看汤姆·史立克(Tom Schlick)关于该主题的出色的 Laracon 2017演讲
既然我们已经决定了租赁实施,我们的代码示例将假定以下内容:
- 每个代表用户数据的模型在其表上都会有一个
user_id
列。 - 租期仅在对用户进行身份验证时才需要应用,因此我们讨论的路由将应用
auth
中间件。
快速开始
在此示例中,我们将重点关注 Bill
和 Payment
模型。我们需要做的第一件事是为每个实体构建控制器。每个控制器将获取属于已认证用户的数据,并使其可用于视图。
BillsController.php
class BillsController extends Controller
{
public function index()
{
$bills = Bill::where('user_id', auth()->user()->id)->get();
return view('bills.index', compact('bills'));
}
}
PaymentsController.php
class PaymentsController extends Controller
{
public function index()
{
$payments = Payment::where('user_id', auth()->user()->id)->get();
return view('payments.index', compact('payments'));
}
}
尽管此选择可行,但我们已将自己置于必须记住将此 where
条件应用于我们对此模型进行的任何查询的位置。我们还将这些方法耦合到 user_id
列和 auth()
助手函数的基础实例AuthManager
上。 我们可以通过在每个模型中提取 local scope 来解决部分问题::
// In Payment & Bill models
public function scopeCurrentTenant($query)
{
$query->where('user_id', auth()->user()->id);
}
然后,我们可以在控制器中更改查询语句:
$payments = Payment::currentTenant()->get();
$bills = Bill::currentTenant()->get();
不幸的是,我们所做的只是将有问题的代码移出控制器并移入模型。 它仍然容易出错,需要我们将范围应用于每个查询。我们可以通过实现自动应用的 global scope 进一步改进它:
// In Payment & Bill models
public static function boot()
{
static::addGlobalScope('user_id', function (Builder $builder) {
$builder->where('user_id', auth()->user()->id);
});
}
现在我们的控制器可以简单地调用:
$payments = Payment::all();
$bills = Bill::all();
这种方法可以完成工作,但是您可能会认为我们仍然存在重复,并且,如果我们要更改租赁范围,则需要在两个地方进行。你是绝对正确的。我们还将租约应用于我们的实体的逻辑,并与我们自己的代码混合在一起。
将 scope 提取到专用的 Scope class 修复了重复问题:
class TenancyScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('user_id', auth()->user()->id);
}
}
但是,我们仍然必须记住在每个模型中注册 scope。
让我们考虑一下,如果我们创建一个扩展包,我们将如何解决这个问题。最简单方法是使用 trait-类似于HasTenancy
,向需要租赁的任何模型添加一组相关逻辑。
现在,让我们做一下封装! 我们将以App\Tenancy
为我们的命名空间,并创建 HasTenancy
traits 添加我们的全局作用域(scope):
HasTenancy.php
namespace App\Tenancy;
trait HasTenancy
{
public static function bootHasTenancy()
{
static::addGlobalScope(new TenancyScope);
}
}
注意:
bootHasTenancy
是 Laravel 提供的,可以用来直接将您的扩展挂载到相应的 Model 上的boot()
方法中的方法,更多信息请查阅:Caleb Porzio's blog post.
现在我们在模型中使用 HasTenancy
:
class Payment extends Model
{
use HasTenancy;
}
class Bill extends Model
{
use HasTenancy;
}
以上更改,我们主要完成了以下几点:
- 通过 HasTenancy traris 注册全局作用域(scope),实现扩展与应用的解耦。
- 使用单独的命名空间
App\Tenancy
来组织我们的逻辑代码,将影响范围限制在App\Tenancy
之下。
这段代码越来越好,但是我们的租约 scope 取决于 Auth
实例 —在没有通过身份验证的情况下使用租约查询模型将引发异常。 此外,我们的 scope 与 User
对象紧密耦合;如果我们想以其他任何方式确定租期,则必须更改 scope。让我们看看如何在包名称空间内实现 Tenant
对象和服务提供者,并利用Laravel 的应用程序容器解决此问题。
首先,我们将创建一个Tenant
类,以在一个地方包含与 User
模型相关的所有逻辑:
namespace App\Tenancy;
class Tenant
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function column()
{
return 'user_id';
}
public function id()
{
return $this->user->id;
}
}
接下来,我们将Tenant
绑定到服务提供商中的Laravel应用程序容器中,并向其注入经过身份验证的用户:
namespace App\Tenancy;
class TenancyProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Tenant::class, function() {
return new Tenant(Auth::user());
});
}
}
最后,我们将更新 scope 使用新的Tenant
类:
namespace App\Tenancy;
class TenancyScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$tenant = app(Tenant::class);
$builder->where($tenant->column(), $tenant->id());
}
}
现在,“租约” 功能独立存在。我们在其中注册租户的服务提供商将成为该软件包进入应用程序的入口点;与租用有关的任何其他配置都将放在此处,用户可以在这里轻松覆盖它。
如果我们要制作扩展包,我们将使用 Laravel的 package discovery 功能自动注册我们的服务提供商,并且所有用户都将拥有安装软件包后要做的就是使用 HasTenancy
trait.
结论
即使您最终并没有真正提取新扩展包或拆分微服务,也可以以 "feature = package" 的思想处理代码通常会为您提供更好的坚持 SRP 的信心,更重要的是,可以帮助您创建SRP。代码库具有组织良好的组件,一目了然,易于维护和理解。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
好文
很好的关于解耦的
好文,最近刚好想做多租户架构,感觉可以参考。