作为一名程序员,这些年我遇到了无数的项目,我继承、管理、升级、开发、移交。其中许多涉及意大利面条代码,或者也称为“大泥球”。此问题通常会影响基于某些框架构建的项目,其中代码的组织方式与框架文档中的示例类似。
不幸的是,MVC 框架的文档通常无法警告代码示例主要用于说明功能,并不适合实际应用程序。因此,实际项目通常将所有层集成到处理请求(通常是 HTTP 请求)的控制器或呈现器方法(在 MVP 的情况下)中。如果框架包含组件对象模型(例如 Nette),则组件通常是控制器或演示器的一部分,这会使情况进一步复杂化。
此类项目的代码长度和复杂性都会快速增长。在单个脚本中,数据库操作、数据操作、组件初始化、模板设置和业务逻辑混合在一起。虽然作者偶尔会将部分功能提取到独立服务(通常是单例)中,但这几乎没有多大帮助。这样的项目变得难以阅读且难以维护。
根据我的经验,很少使用标准化设计模式,特别是在较小的项目(5-5万行代码)中,例如寻求简化管理的小型企业的简单 CRUD 应用程序。然而,这些项目可以极大地受益于 CQRS(命令查询职责分离)和 DDD(领域驱动设计)等模式。
我将使用 Nette 堆栈、Contributte(与 Symfony 事件调度程序集成)和 Nextras ORM 演示此方法的外观。
// Command definition final class ItemSaveCommand implements Command { private ItemSaveRequest $request; public function __construct(private Orm $orm) { // } /** @param ItemSaveRequest $request */ public function setRequest(Request $request): void { $this->request = $request; } public function execute(): void { $request = $this->request; if ($request->id) { $entity = $this->orm->items->getById($request->id); } else { $entity = new Item(); $entity->uuid = $request->uuid; } $entity->data = $request->data; $this->orm->persist($entity); } } // Command Factory interface ItemSaveCommandFactory extends FactoryService { public function create(): ItemSaveCommand; } // Request #[RequestCommandFactory(ItemSaveCommandFactory::class)] final class ItemSaveRequest implements Request { public int|null $id = null; public string $uuid; public string $data; } /** * Command execution service * Supports transactions and request logging */ final class CommandExecutionService implements Service { private \DateTimeImmutable $requestedAt; public function __construct( private Orm $orm, private Container $container, private Connection $connection, ) { $this->requestedAt = new \DateTimeImmutable(); } /** @throws \Throwable */ public function execute(AbstractCommandRequest $request, bool $logRequest = true, bool $transaction = true): mixed { $factoryClass = RequestHelper::getCommandFactory($request); $factory = $this->container->getByType($factoryClass); $cmd = $factory->create(); $clonedRequest = clone $request; $cmd->setRequest($request); try { $cmd->execute(); if (!$transaction) { $this->orm->flush(); } if (!$logRequest) { return; } $logEntity = RequestHelper::createRequestLog( $clonedRequest, $this->requestedAt, RequestLogConstants::StateSuccess ); if ($transaction) { $this->orm->persistAndFlush($logEntity); } else { $this->orm->persist($logEntity); } return; } catch (\Throwable $e) { if ($transaction) { $this->connection->rollbackTransaction(); } if (!$logRequest) { throw $e; } $logEntity = RequestHelper::createRequestLog( $clonedRequest, $this->requestedAt, RequestLogConstants::StateFailed ); if ($transaction) { $this->orm->persistAndFlush($logEntity); } else { $this->orm->persist($logEntity); } throw $e; } } } // Listener for executing commands via Event Dispatcher final class RequestExecutionListener implements EventSubscriberInterface { public function __construct( private CommandExecutionService $commandExecutionService ) { // } public static function getSubscribedEvents(): array { return [ RequestExecuteEvent::class => 'onRequest' ]; } /** @param ExecuteRequestEvent<mixed> $ev */ public function onRequest(ExecuteRequestEvent $ev): void { $this->commandExecutionService->execute($ev->request, $ev->logRequest, $ev->transaction); } } // Event definition for command execution final class ExecuteRequestEvent extends Event { public function __construct( public Request $request, public bool $logRequest = true, public bool $transaction = true, ) { // Constructor } } // Event Dispatcher Facade final class EventDispatcherFacade { public static EventDispatcherInterface $dispatcher; public static function set(EventDispatcherInterface $dispatcher): void { self::$dispatcher = $dispatcher; } } // Helper function for simple event dispatching function dispatch(Event $event): object { return EventDispatcherFacade::$dispatcher->dispatch($event); } // Usage in Presenter (e.g., in response to a component event) final class ItemPresenter extends Presenter { public function createComponentItem(): Component { $component = new Component(); $component->onSave[] = function (ItemSaveRequest $request) { dispatch(new ExecuteRequestEvent($request)); }; return $component; } }
该解决方案有几个优点。数据库相关逻辑与MVC/P分离,有助于更好的可读性和更容易维护。请求对象充当数据载体,非常适合登录数据库,例如事件日志。这确保了所有修改数据的用户输入都按照时间顺序保存。如果出现错误,可以在必要时查看并重播这些日志。
这种方法的缺点包括命令不应返回任何数据。因此,如果我需要进一步处理新创建的数据(例如,将其传递给模板),我必须使用其 UUID 检索它,这就是请求和实体都包含它的原因。另一个缺点是对数据库架构的任何更改都需要更新所有请求以匹配新架构,这可能非常耗时。
以上是比框架文档中的示例稍微高级一些的代码。的详细内容。更多信息请关注PHP中文网其他相关文章!