Các mẫu thiết kế trong XOOPS
2.5.x ✅ 4.0.x ✅
Các mẫu thiết kế là các giải pháp có thể tái sử dụng cho các vấn đề thiết kế phần mềm phổ biến. XOOPS sử dụng một số mẫu được thiết lập tốt giúp duy trì chất lượng mã, cải thiện khả năng kiểm tra và nâng cao tính linh hoạt của hệ thống.
Tổng quan
Phần tiêu đề “Tổng quan”Hiểu và triển khai đúng các mẫu thiết kế là rất quan trọng để tạo ra XOOPS modules có thể bảo trì được. Hướng dẫn này bao gồm các mẫu được sử dụng phổ biến nhất trong quá trình phát triển XOOPS.
| Mẫu | Mục đích | Các trường hợp sử dụng phổ biến |
|---|---|---|
| MVC | Tách biệt mối quan tâm | Cấu trúc mô-đun |
| Singleton | Bảo đảm một trường hợp | Kết nối cơ sở dữ liệu |
| Nhà máy | Trừu tượng tạo đối tượng | Trình xử lý, cơ sở dữ liệu |
| Người quan sát | Thông báo sự kiện | Tải trước, thông báo |
| Người trang trí | Tiện ích mở rộng hành vi động | Các phần tử biểu mẫu, bộ lọc |
| Chiến lược | Trao đổi thuật toán | Xác thực, xác thực |
| Bộ chuyển đổi | Khả năng tương thích giao diện | Tích hợp mã kế thừa |
| Kho lưu trữ | Trừu tượng truy cập dữ liệu | Kiên trì dữ liệu |
Model-View-Controller (MVC)
Phần tiêu đề “Model-View-Controller (MVC)”Mẫu MVC tách ứng dụng thành ba thành phần được kết nối với nhau, làm cho cơ sở mã có tổ chức và dễ kiểm tra hơn.
Kiến trúc
Phần tiêu đề “Kiến trúc”flowchart TB subgraph MVC["MVC Pattern in XOOPS"] Controller["🎮 Controller<br/>(index.php, admin/index.php)"] Model["📦 Model<br/>(Handlers)"] View["🎨 View<br/>(Templates)"]
Controller --> Model Controller --> View Model <--> View end
style Controller fill:#e3f2fd,stroke:#1976d2 style Model fill:#fff3e0,stroke:#f57c00 style View fill:#e8f5e9,stroke:#388e3cMô hình (Lớp dữ liệu)
Phần tiêu đề “Mô hình (Lớp dữ liệu)”<?phpnamespace XoopsModules\MyModule;
class Article extends \XoopsObject{ public function __construct() { $this->initVar('article_id', XOBJ_DTYPE_INT, null, false); $this->initVar('title', XOBJ_DTYPE_TXTBOX, '', true, 255); $this->initVar('content', XOBJ_DTYPE_TXTAREA, '', true); $this->initVar('author_id', XOBJ_DTYPE_INT, 0, true); $this->initVar('status', XOBJ_DTYPE_INT, 1, false); $this->initVar('created', XOBJ_DTYPE_INT, time(), false); $this->initVar('modified', XOBJ_DTYPE_INT, time(), false); }
public function isPublished(): bool { return $this->getVar('status') === 1; }
public function getFormattedDate(): string { return formatTimestamp($this->getVar('created')); }}
class ArticleHandler extends \XoopsPersistableObjectHandler{ public function __construct(\XoopsDatabase $db) { parent::__construct($db, 'mymodule_articles', Article::class, 'article_id', 'title'); }
public function getPublishedArticles(int $limit = 10): array { $criteria = new \CriteriaCompo(); $criteria->add(new \Criteria('status', 1)); $criteria->setSort('created'); $criteria->setOrder('DESC'); $criteria->setLimit($limit);
return $this->getObjects($criteria); }}Xem (Lớp trình bày)
Phần tiêu đề “Xem (Lớp trình bày)”{* templates/article_list.tpl *}<div class="article-list"> <h2>{$smarty.const._MD_MYMODULE_ARTICLES}</h2>
{foreach from=$articles item=article} <article class="article-item"> <h3> <a href="{$xoops_url}/modules/mymodule/article.php?id={$article.article_id}"> {$article.title|escape} </a> </h3> <p class="meta"> {$smarty.const._MD_MYMODULE_POSTED}: {$article.formatted_date} </p> <div class="content"> {$article.content|truncate:200} </div> </article> {/foreach}</div>Bộ điều khiển (Lớp logic)
Phần tiêu đề “Bộ điều khiển (Lớp logic)”<?phprequire_once dirname(__DIR__, 2) . '/mainfile.php';
use XoopsModules\MyModule\Helper;
$helper = Helper::getInstance();$articleHandler = $helper->getHandler('Article');
// Get action from request$op = \Xmf\Request::getString('op', 'list');
switch ($op) { case 'view': $articleId = \Xmf\Request::getInt('id', 0); $article = $articleHandler->get($articleId);
if (!$article) { redirect_header(XOOPS_URL, 3, _MD_MYMODULE_NOT_FOUND); }
$GLOBALS['xoopsOption']['template_main'] = 'mymodule_article_view.tpl'; require_once XOOPS_ROOT_PATH . '/header.php';
$xoopsTpl->assign('article', $article->toArray()); break;
case 'list': default: $articles = $articleHandler->getPublishedArticles(10);
$GLOBALS['xoopsOption']['template_main'] = 'mymodule_article_list.tpl'; require_once XOOPS_ROOT_PATH . '/header.php';
$xoopsTpl->assign('articles', array_map(fn($a) => $a->toArray(), $articles)); break;}
require_once XOOPS_ROOT_PATH . '/footer.php';Mẫu đơn
Phần tiêu đề “Mẫu đơn”Mẫu Singleton đảm bảo class chỉ có một phiên bản và cung cấp quyền truy cập toàn cầu vào phiên bản đó.
Khi nào nên sử dụng
Phần tiêu đề “Khi nào nên sử dụng”- Kết nối cơ sở dữ liệu
- Trình quản lý cấu hình
- Trường hợp logger
- Trình quản lý bộ đệm
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule;
class ConfigurationManager{ private static ?self $instance = null; private array $config = [];
private function __construct() { // Load configuration $this->loadConfiguration(); }
// Prevent cloning private function __clone() {}
// Prevent unserialization public function __wakeup() { throw new \Exception("Cannot unserialize singleton"); }
public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); }
return self::$instance; }
private function loadConfiguration(): void { $helper = Helper::getInstance(); $this->config = [ 'items_per_page' => $helper->getConfig('items_per_page', 10), 'allow_comments' => $helper->getConfig('allow_comments', true), 'date_format' => $helper->getConfig('date_format', 'Y-m-d'), ]; }
public function get(string $key, mixed $default = null): mixed { return $this->config[$key] ?? $default; }}
// Usage$config = ConfigurationManager::getInstance();$itemsPerPage = $config->get('items_per_page');Ví dụ cốt lõi về XOOPS
Phần tiêu đề “Ví dụ cốt lõi về XOOPS”<?php// XoopsDatabaseFactory uses Singleton pattern$db = XoopsDatabaseFactory::getDatabaseConnection();
// XMF Module Helper uses Singleton$helper = \Xmf\Module\Helper::getHelper('mymodule');
// Xoops main instance$xoops = \Xoops::getInstance();Mẫu nhà máy
Phần tiêu đề “Mẫu nhà máy”Mẫu Factory tạo các đối tượng mà không chỉ định chính xác class của chúng, cho phép tạo đối tượng linh hoạt.
Khi nào nên sử dụng
Phần tiêu đề “Khi nào nên sử dụng”- Tạo trình xử lý động
- Kết nối cơ sở dữ liệu cho các cơ sở dữ liệu khác nhau
- Nhà cung cấp xác thực
- Tạo phần tử biểu mẫu
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule;
interface ContentInterface{ public function render(): string;}
class ArticleContent implements ContentInterface{ private array $data;
public function __construct(array $data) { $this->data = $data; }
public function render(): string { return "<article><h2>{$this->data['title']}</h2><p>{$this->data['body']}</p></article>"; }}
class NewsContent implements ContentInterface{ private array $data;
public function __construct(array $data) { $this->data = $data; }
public function render(): string { return "<div class='news'><h3>{$this->data['headline']}</h3><p>{$this->data['summary']}</p></div>"; }}
class ContentFactory{ public static function create(string $type, array $data): ContentInterface { return match ($type) { 'article' => new ArticleContent($data), 'news' => new NewsContent($data), default => throw new \InvalidArgumentException("Unknown content type: $type"), }; }}
// Usage$article = ContentFactory::create('article', ['title' => 'Hello', 'body' => 'World']);echo $article->render();Nhà máy cơ sở dữ liệu XOOPS
Phần tiêu đề “Nhà máy cơ sở dữ liệu XOOPS”<?phpclass XoopsDatabaseFactory{ public static function getDatabaseConnection() { static $instance;
if (!isset($instance)) { $dbType = XOOPS_DB_TYPE ?? 'mysql'; $className = 'XoopsDatabase' . ucfirst($dbType);
if (!class_exists($className)) { $file = XOOPS_ROOT_PATH . '/class/database/' . strtolower($dbType) . '.php'; if (file_exists($file)) { require_once $file; } }
$instance = new $className();
if (!$instance->connect()) { trigger_error('Unable to connect to database', E_USER_ERROR); } }
return $instance; }}Mẫu người quan sát
Phần tiêu đề “Mẫu người quan sát”Mẫu Người quan sát cho phép các đối tượng được thông báo về những thay đổi đối với trạng thái của chủ thể, cho phép thực hiện hành vi theo hướng sự kiện.
Khi nào nên sử dụng
Phần tiêu đề “Khi nào nên sử dụng”- Xử lý sự kiện
- Hệ thống thông báo
- Kiến trúc plugin
- Ghi nhật ký và kiểm tra
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule;
interface ObserverInterface{ public function update(string $event, array $data): void;}
class EventDispatcher{ private array $observers = [];
public function attach(string $event, ObserverInterface $observer): void { if (!isset($this->observers[$event])) { $this->observers[$event] = []; }
$this->observers[$event][] = $observer; }
public function detach(string $event, ObserverInterface $observer): void { if (isset($this->observers[$event])) { $key = array_search($observer, $this->observers[$event], true); if ($key !== false) { unset($this->observers[$event][$key]); } } }
public function notify(string $event, array $data = []): void { if (isset($this->observers[$event])) { foreach ($this->observers[$event] as $observer) { $observer->update($event, $data); } } }}
class EmailNotifier implements ObserverInterface{ public function update(string $event, array $data): void { if ($event === 'article.published') { // Send email notification $this->sendEmail($data['article']); } }
private function sendEmail($article): void { $xoopsMailer = xoops_getMailer(); $xoopsMailer->setSubject('New Article Published: ' . $article->getVar('title')); $xoopsMailer->setBody('A new article has been published.'); $xoopsMailer->send(); }}
// Usage$dispatcher = new EventDispatcher();$dispatcher->attach('article.published', new EmailNotifier());
// When article is published$dispatcher->notify('article.published', ['article' => $article]);Tải trước XOOPS (Triển khai máy quan sát)
Phần tiêu đề “Tải trước XOOPS (Triển khai máy quan sát)”<?phpclass MymoduleCorePreload extends XoopsPreloadItem{ public static function eventCoreIncludeCommonEnd($args) { // React to core common include completing $GLOBALS['xoopsLogger']->addExtra('MyModule', 'Initialized'); }
public static function eventCoreHeaderEnd($args) { // Add custom headers $GLOBALS['xoTheme']->addStylesheet('modules/mymodule/assets/css/custom.css'); }
public static function eventCoreFooterStart($args) { // Execute before footer renders }}Mẫu trang trí
Phần tiêu đề “Mẫu trang trí”Mẫu Decorator bổ sung hành vi cho các đối tượng một cách linh hoạt mà không ảnh hưởng đến các đối tượng khác của cùng class.
Khi nào nên sử dụng
Phần tiêu đề “Khi nào nên sử dụng”- Tùy chỉnh phần tử biểu mẫu
- Định dạng đầu ra
- Kiểm tra quyền
- Lớp bộ nhớ đệm
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule;
interface FormElementInterface{ public function render(): string;}
class TextInput implements FormElementInterface{ private string $name; private string $value;
public function __construct(string $name, string $value = '') { $this->name = $name; $this->value = $value; }
public function render(): string { return sprintf( '<input type="text" name="%s" value="%s">', htmlspecialchars($this->name), htmlspecialchars($this->value) ); }}
abstract class FormElementDecorator implements FormElementInterface{ protected FormElementInterface $element;
public function __construct(FormElementInterface $element) { $this->element = $element; }
public function render(): string { return $this->element->render(); }}
class RequiredDecorator extends FormElementDecorator{ public function render(): string { return $this->element->render() . '<span class="required">*</span>'; }}
class LabelDecorator extends FormElementDecorator{ private string $label;
public function __construct(FormElementInterface $element, string $label) { parent::__construct($element); $this->label = $label; }
public function render(): string { return sprintf( '<label>%s</label>%s', htmlspecialchars($this->label), $this->element->render() ); }}
class HelpTextDecorator extends FormElementDecorator{ private string $helpText;
public function __construct(FormElementInterface $element, string $helpText) { parent::__construct($element); $this->helpText = $helpText; }
public function render(): string { return $this->element->render() . sprintf( '<small class="help-text">%s</small>', htmlspecialchars($this->helpText) ); }}
// Usage - decorators can be stacked$input = new TextInput('username');$input = new RequiredDecorator($input);$input = new LabelDecorator($input, 'Username');$input = new HelpTextDecorator($input, 'Enter your username');
echo $input->render();// Output: <label>Username</label><input type="text" name="username" value=""><span class="required">*</span><small class="help-text">Enter your username</small>Mẫu chiến lược
Phần tiêu đề “Mẫu chiến lược”Mẫu Chiến lược xác định một nhóm thuật toán, đóng gói từng thuật toán và làm cho chúng có thể hoán đổi cho nhau.
Khi nào nên sử dụng- Nhiều phương thức xác thực
Phần tiêu đề “Khi nào nên sử dụng- Nhiều phương thức xác thực”- Các thuật toán sắp xếp khác nhau
- Định dạng xuất khác nhau
- Quy tắc xác nhận linh hoạt
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule;
interface AuthStrategyInterface{ public function authenticate(string $username, string $password): bool;}
class DatabaseAuthStrategy implements AuthStrategyInterface{ public function authenticate(string $username, string $password): bool { $memberHandler = xoops_getHandler('member'); $user = $memberHandler->loginUser($username, $password);
return $user !== false; }}
class LdapAuthStrategy implements AuthStrategyInterface{ private string $ldapHost; private int $ldapPort;
public function __construct(string $host, int $port = 389) { $this->ldapHost = $host; $this->ldapPort = $port; }
public function authenticate(string $username, string $password): bool { $ldap = ldap_connect($this->ldapHost, $this->ldapPort);
if (!$ldap) { return false; }
$bind = @ldap_bind($ldap, "uid=$username,ou=users,dc=example,dc=com", $password);
ldap_close($ldap);
return $bind; }}
class AuthService{ private AuthStrategyInterface $strategy;
public function __construct(AuthStrategyInterface $strategy) { $this->strategy = $strategy; }
public function setStrategy(AuthStrategyInterface $strategy): void { $this->strategy = $strategy; }
public function login(string $username, string $password): bool { return $this->strategy->authenticate($username, $password); }}
// Usage$authService = new AuthService(new DatabaseAuthStrategy());
// Can switch strategies at runtimeif ($useLdap) { $authService->setStrategy(new LdapAuthStrategy('ldap.example.com'));}
$authenticated = $authService->login($username, $password);Mẫu kho lưu trữ
Phần tiêu đề “Mẫu kho lưu trữ”Mẫu Kho lưu trữ cung cấp một lớp trừu tượng giữa logic truy cập dữ liệu và logic nghiệp vụ.
Khi nào nên sử dụng
Phần tiêu đề “Khi nào nên sử dụng”- Yêu cầu truy cập dữ liệu phức tạp
- Nhiều nguồn dữ liệu
- Các lớp dữ liệu có thể kiểm tra được
- Thiết kế hướng tên miền
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule\Repository;
use XoopsModules\MyModule\Entity\Article;
interface ArticleRepositoryInterface{ public function find(int $id): ?Article; public function findBySlug(string $slug): ?Article; public function findPublished(int $limit = 10, int $offset = 0): array; public function save(Article $article): bool; public function delete(Article $article): bool;}
class ArticleRepository implements ArticleRepositoryInterface{ private \XoopsPersistableObjectHandler $handler;
public function __construct(\XoopsPersistableObjectHandler $handler) { $this->handler = $handler; }
public function find(int $id): ?Article { $obj = $this->handler->get($id); return $obj ?: null; }
public function findBySlug(string $slug): ?Article { $criteria = new \Criteria('slug', $slug); $objects = $this->handler->getObjects($criteria);
return !empty($objects) ? $objects[0] : null; }
public function findPublished(int $limit = 10, int $offset = 0): array { $criteria = new \CriteriaCompo(); $criteria->add(new \Criteria('status', 'published')); $criteria->setSort('published_at'); $criteria->setOrder('DESC'); $criteria->setLimit($limit); $criteria->setStart($offset);
return $this->handler->getObjects($criteria); }
public function save(Article $article): bool { return $this->handler->insert($article); }
public function delete(Article $article): bool { return $this->handler->delete($article); }}Tiêm phụ thuộc
Phần tiêu đề “Tiêm phụ thuộc”Tính năng Tiêm phụ thuộc (DI) cho phép các đối tượng được xây dựng bằng các phần phụ thuộc của chúng thay vì tạo chúng bên trong.
Lợi ích
Phần tiêu đề “Lợi ích”- Cải thiện khả năng kiểm tra
- Khớp nối lỏng lẻo
- Cấu hình linh hoạt
- Tổ chức mã tốt hơn
Triển khai
Phần tiêu đề “Triển khai”<?phpnamespace XoopsModules\MyModule;
class ArticleService{ private Repository\ArticleRepositoryInterface $repository; private CacheInterface $cache; private LoggerInterface $logger;
public function __construct( Repository\ArticleRepositoryInterface $repository, CacheInterface $cache, LoggerInterface $logger ) { $this->repository = $repository; $this->cache = $cache; $this->logger = $logger; }
public function getArticle(int $id): ?Entity\Article { $cacheKey = "article_{$id}";
// Try cache first if ($this->cache->has($cacheKey)) { $this->logger->debug("Article {$id} loaded from cache"); return $this->cache->get($cacheKey); }
// Load from repository $article = $this->repository->find($id);
if ($article) { $this->cache->set($cacheKey, $article, 3600); $this->logger->debug("Article {$id} loaded from database"); }
return $article; }}
// Service container setup$container = new DependencyContainer();
$container->register('db', fn() => XoopsDatabaseFactory::getDatabaseConnection());
$container->register('articleHandler', fn($c) => new ArticleHandler($c->resolve('db')));
$container->register('articleRepository', fn($c) => new Repository\ArticleRepository($c->resolve('articleHandler')));
$container->register('cache', fn() => new FileCache(XOOPS_VAR_PATH . '/caches'));
$container->register('logger', fn() => new XoopsLogger());
$container->register('articleService', fn($c) => new ArticleService( $c->resolve('articleRepository'), $c->resolve('cache'), $c->resolve('logger') ));
// Usage$articleService = $container->resolve('articleService');$article = $articleService->getArticle(1);Các phương pháp hay nhất
Phần tiêu đề “Các phương pháp hay nhất”Nguyên tắc lựa chọn mẫu
Phần tiêu đề “Nguyên tắc lựa chọn mẫu”- Chọn mẫu dựa trên nhu cầu thực tế, không phải mẫu dự kiến
- Giữ việc triển khai đơn giản - đừng thiết kế quá kỹ
- Cách sử dụng mẫu tài liệu để nhóm hiểu
- Kết hợp các mẫu khi thích hợp (ví dụ: Factory + Singleton)
- Xem xét khả năng kiểm tra khi chọn mẫu
Các kiểu chống đối phổ biến cần tránh
Phần tiêu đề “Các kiểu chống đối phổ biến cần tránh”| Chống Mẫu | Vấn đề | Giải pháp |
|---|---|---|
| Đối Tượng Thần | Lớp làm quá nhiều | Trách nhiệm duy nhất |
| Mã Spaghetti | Không có cấu trúc rõ ràng | Sử dụng mẫu MVC |
| Sao chép-Dán | Sao chép mã | Trích xuất mã chung |
| Những con số kỳ diệu | Hằng số không rõ ràng | Sử dụng các hằng số có tên |
| Khớp nối chặt chẽ | Khó kiểm tra/bảo trì | Sử dụng tính năng tiêm phụ thuộc |
Mẫu thử nghiệm
Phần tiêu đề “Mẫu thử nghiệm”<?php// Unit testing with dependency injectionclass ArticleServiceTest extends \PHPUnit\Framework\TestCase{ private $repository; private $cache; private $logger; private $service;
protected function setUp(): void { $this->repository = $this->createMock(ArticleRepositoryInterface::class); $this->cache = $this->createMock(CacheInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->service = new ArticleService( $this->repository, $this->cache, $this->logger ); }
public function testGetArticleFromCache(): void { $article = new Article(); $article->setVar('article_id', 1);
$this->cache->expects($this->once()) ->method('has') ->with('article_1') ->willReturn(true);
$this->cache->expects($this->once()) ->method('get') ->with('article_1') ->willReturn($article);
$result = $this->service->getArticle(1);
$this->assertSame($article, $result); }}Tài liệu liên quan
Phần tiêu đề “Tài liệu liên quan”- XOOPS-Architecture - Kiến trúc hệ thống tổng thể
- Lớp cơ sở dữ liệu - Mẫu lưu giữ dữ liệu
- Các phương pháp thực hành bảo mật tốt nhất - Triển khai mẫu bảo mật
#xoops #design-patterns #architecture #mvc #singleton #factory #observer