作為一名程式設計師,這些年我遇到了無數的項目,我繼承、管理、升級、開發、移交。其中許多涉及義大利麵代碼,或也稱為“大泥球”。此問題通常會影響基於某些框架建構的項目,其中程式碼的組織方式與框架文件中的範例類似。
不幸的是,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中文網其他相關文章!