Design Patterns trong PHP: Từng bước giải bài toán tính khoảng cách
Tính khoảng cách giữa các điểm địa lý là một yêu cầu phổ biến trong nhiều ứng dụng. Trong bài viết này, chúng ta sẽ từng bước cải thiện một hệ thống tính khoảng cách đơn giản bằng cách áp dụng các design pattern phổ biến, biến nó thành một giải pháp linh hoạt và dễ bảo trì.
Vấn đề
Chúng ta cần tính khoảng cách giữa hai điểm địa lý với các yêu cầu sau:
- Hỗ trợ nhiều phương thức tính toán (đường chim bay, bằng ô tô, bằng xe đạp)
- Lưu cache kết quả để tránh tính toán lặp lại
- Xử lý lỗi tính toán một cách nhẹ nhàng
- Áp dụng khoảng cách tối thiểu
- Dễ dàng mở rộng hệ thống
Phiên bản 1: Giải pháp đơn giản nhất
Chúng ta bắt đầu với cách tiếp cận đơn giản nhất: một class duy nhất thực hiện tất cả công việc:
class SimpleDistanceCalculator
{
public function getDistance(float $lat1, float $long1, float $lat2, float $long2): float
{
// Tính khoảng cách đường chim bay
// (công thức Haversine hoặc công thức Vincenty)
return 10.0; // Đây là kết quả giả định
}
}
// Sử dụng
$calculator = new SimpleDistanceCalculator();
$distance = $calculator->getDistance(19.2, 34.35, 33.2, 343.35);
echo "Khoảng cách: " . $distance;
Phiên bản này hoạt động, nhưng có nhiều hạn chế:
- Chỉ hỗ trợ một phương thức tính khoảng cách (đường chim bay)
- Không có cơ chế lưu cache
- Không có xử lý lỗi
- Khó mở rộng thêm chức năng mới
Phiên bản 2: Áp dụng OOP + Dependency Injection (DI)
Trong phiên bản này, chúng ta sẽ áp dụng OOP + Dependency Injection để giảm sự phụ thuộc giữa các thành phần. Đầu tiên, tạo interface và các class:
// Tạo class Point để đóng gói thông tin về điểm
class Point
{
public function __construct(
private readonly float $lat,
private readonly float $long,
) {}
public function getLat(): float
{
return $this->lat;
}
public function getLong(): float
{
return $this->long;
}
}
// Tạo interface để định nghĩa contract cho các calculator
interface DistanceCalculator
{
public function getDistance(Point $point1, Point $point2): float;
}
// Triển khai cụ thể của calculator
class BirdFlyDistanceCalculator implements DistanceCalculator
{
public function getDistance(Point $point1, Point $point2): float
{
// Tính khoảng cách đường chim bay
return 10.0;
}
}
// Class context sử dụng calculator thông qua dependency injection
class GetDistanceCommand
{
// Inject DistanceCalculator thông qua constructor
public function __construct(
private readonly DistanceCalculator $calculator
) {}
public function execute(Point $point1, Point $point2): float
{
return $this->calculator->getDistance($point1, $point2);
}
}
Dependency Injection trong Laravel
Laravel có một hệ thống DI container rất mạnh mẽ. Hãy xem cách đăng ký và sử dụng các dependency trong Laravel:
1. Đăng ký binding trong Service Provider
Tạo một Service Provider để đăng ký các binding cho hệ thống tính khoảng cách:
<?php
namespace App\Providers;
use App\Console\Commands\DistanceCalculator\BirdFlyDistanceCalculator;
use App\Console\Commands\DistanceCalculator\DistanceCalculator;
use Illuminate\Support\ServiceProvider;
class DistanceCalculatorServiceProvider extends ServiceProvider
{
public function register()
{
// Đăng ký binding để interface DistanceCalculator
// được map tới concrete class BirdFlyDistanceCalculator
$this->app->bind(DistanceCalculator::class, BirdFlyDistanceCalculator::class);
// Hoặc nếu muốn singleton (cùng một instance sẽ được dùng lại)
// $this->app->singleton(DistanceCalculator::class, BirdFlyDistanceCalculator::class);
// Có thể đăng ký với tên tùy chọn
// $this->app->bind('distance.calculator', BirdFlyDistanceCalculator::class);
// Hoặc đăng ký với closure nếu cần logic phức tạp hơn
// $this->app->bind(DistanceCalculator::class, function ($app) {
// return new BirdFlyDistanceCalculator();
// });
}
}
Sau đó thêm provider này vào mảng providers
trong config/app.php
:
'providers' => [
// Other providers...
App\Providers\DistanceCalculatorServiceProvider::class,
],
2. Dependency Injection trong Controller
<?php
namespace App\Http\Controllers;
use App\Console\Commands\DistanceCalculator\DistanceCalculator;
use App\Console\Commands\DistanceCalculator\Point;
use Illuminate\Http\Request;
class DistanceController extends Controller
{
// Laravel sẽ tự động inject DistanceCalculator vào constructor
public function __construct(
private readonly DistanceCalculator $calculator
) {}
public function calculate(Request $request)
{
$point1 = new Point($request->input('lat1'), $request->input('long1'));
$point2 = new Point($request->input('lat2'), $request->input('long2'));
$distance = $this->calculator->getDistance($point1, $point2);
return response()->json(['distance' => $distance]);
}
}
3. Dependency Injection trong Console Command
Như trong code mẫu GetDistanceCommand.php
đã cung cấp:
<?php
namespace App\Console\Commands\DistanceCalculator;
use Illuminate\Console\Command;
class GetDistanceCommand extends Command
{
protected $signature = 'distance:get';
// Laravel sẽ tự động inject DistanceCalculator vào handle method
public function handle(DistanceCalculator $calculator): void
{
$this->info('start');
$point1 = new Point(19.2, 34.35);
$point2 = new Point(33.2, 343.35);
$distance = $calculator->getDistance($point1, $point2);
$this->info('Result: ' . $distance);
}
}
4. Dependency Injection thông qua Container API
Nếu bạn cần lấy một instance bên ngoài Controller hoặc Command:
// Lấy instance thông qua container
$calculator = app()->make(DistanceCalculator::class);
// Hoặc dùng helper function
$calculator = app(DistanceCalculator::class);
// Hoặc dùng resolve method
$calculator = resolve(DistanceCalculator::class);
Lợi ích của Dependency Injection
Khi áp dụng DI trong phiên bản 2, chúng ta đã đạt được những lợi ích sau:
- Tách biệt các thành phần: Code không còn phụ thuộc vào một implementation cụ thể mà chỉ phụ thuộc vào interface.
- Dễ dàng thay đổi implementation: Chỉ cần thay đổi binding trong Service Provider, toàn bộ ứng dụng sẽ sử dụng implementation mới.
- Dễ kiểm thử: Có thể dễ dàng mock interface để test các thành phần độc lập:
// Trong test
public function testDistanceCommand()
{
// Tạo mock object
$calculatorMock = $this->createMock(DistanceCalculator::class);
// Thiết lập behavior của mock
$calculatorMock->expects($this->once())
->method('getDistance')
->willReturn(150.0);
// Gán mock vào container
$this->app->instance(DistanceCalculator::class, $calculatorMock);
// Thực thi command
$this->artisan('distance:get')
->expectsOutput('Result: 150.0')
->assertExitCode(0);
}
- Quản lý lifecycle của object: Laravel container cho phép kiểm soát vòng đời của object (singleton, instance mới mỗi lần resolve, v.v).
- Tự động resolve các dependency lồng nhau: Nếu
BirdFlyDistanceCalculator
phụ thuộc vào các service khác, Laravel container sẽ tự động resolve chúng.
Bằng cách áp dụng DI trong Laravel, code của chúng ta trở nên linh hoạt và dễ bảo trì hơn rất nhiều so với phương pháp truyền thống.
Phiên bản 3: Áp dụng Strategy Pattern
Tiếp theo, chúng ta áp dụng Strategy Pattern để hỗ trợ nhiều phương pháp tính khoảng cách:
interface DistanceCalculator
{
public function getDistance(Point $point1, Point $point2): float;
}
class DistanceByBirdFlyCalculator implements DistanceCalculator
{
public function getDistance(Point $point1, Point $point2): float
{
// Tính khoảng cách đường chim bay
return 10.0;
}
}
class DistanceByCarCalculator implements DistanceCalculator
{
public function getDistance(Point $point1, Point $point2): float
{
// Gọi API Google Maps với phương tiện là ô tô
return 12.0;
}
}
class DistanceByBikeCalculator implements DistanceCalculator
{
public function getDistance(Point $point1, Point $point2): float
{
// Gọi API Google Maps với phương tiện là xe đạp
return 11.0;
}
}
// Sử dụng
$point1 = new Point(19.2, 34.35);
$point2 = new Point(33.2, 343.35);
// Chọn phương thức tính toán phù hợp
$calculator = new DistanceByCarCalculator();
$distance = $calculator->getDistance($point1, $point2);
Lợi ích:
- Dễ dàng thêm mới phương pháp tính khoảng cách
- Có thể chọn thuật toán thích hợp tại thời điểm chạy
- Tuân thủ nguyên tắc Open/Closed: mở rộng mà không sửa đổi code hiện có
Phiên bản 4: Áp dụng Chain of Responsibility Pattern
Giờ chúng ta thêm khả năng dự phòng (fallback) khi một phương pháp tính toán thất bại:
class DistanceByCarCalculator
{
public function __construct(
protected readonly ?DistanceCalculator $fallback = null,
) {
}
public function getDistance(Point $point1, Point $point2): float
{
try {
return $this->exec($point1, $point2);
} catch (\Throwable $exception) {
if ($this->fallback !== null) {
return $this->fallback->getDistance($point1, $point2);
}
throw new \RuntimeException($exception->getMessage());
}
}
private function exec(Point $point1, Point $point2): float
{
// call API sang gmap, tham so dau vao la car
return 12.0;
}
}
class DistanceByBikeCalculator
{
public function __construct(
protected readonly ?DistanceCalculator $fallback = null,
) {
}
public function getDistance(Point $point1, Point $point2): float
{
try {
return $this->exec($point1, $point2);
} catch (\Throwable $exception) {
if ($this->fallback !== null) {
return $this->fallback->getDistance($point1, $point2);
}
throw new \RuntimeException($exception->getMessage());
}
}
protected function exec(Point $point1, Point $point2): float
{
// Gọi API Google Maps với phương tiện là xe đạp
return 11.0;
}
}
// Sử dụng làm fallback
$calculator = new DistanceByCarCalculator(
new DistanceByBikeCalculator(
new DistanceByBirdFlyCalculator()
)
);
$distance = $calculator->getDistance($point1, $point2);
Lợi ích:
- Xử lý lỗi một cách ưu nhã
- Tự động chuyển sang phương pháp dự phòng khi cần
- Giảm thiểu khả năng thất bại hoàn toàn
Phiên bản 5: Áp dụng Template Method Pattern
abstract class AbstractDistanceCalculator implements DistanceCalculator
{
public function __construct(
protected readonly ?DistanceCalculator $fallback = null,
) {}
public function getDistance(Point $point1, Point $point2): float
{
try {
return $this->exec($point1, $point2);
} catch (\Throwable $exception) {
if ($this->fallback !== null) {
return $this->fallback->getDistance($point1, $point2);
}
throw new \RuntimeException($exception->getMessage());
}
}
abstract protected function exec(Point $point1, Point $point2): float;
}
class DistanceByCarCalculator extends AbstractDistanceCalculator
{
protected function exec(Point $point1, Point $point2): float
{
// Gọi API Google Maps với phương tiện là ô tô
return 12.0;
}
}
class DistanceByBikeCalculator extends AbstractDistanceCalculator
{
protected function exec(Point $point1, Point $point2): float
{
// Gọi API Google Maps với phương tiện là xe đạp
return 11.0;
}
}
class DistanceByBirdFlyCalculator extends AbstractDistanceCalculator
{
protected function exec(Point $point1, Point $point2): float
{
// Tính khoảng cách đường chim bay
return 10.0;
}
}
// Sử dụng
$calculator = new DistanceByCarCalculator(
new DistanceByBikeCalculator(
new DistanceByBirdFlyCalculator()
)
);
$distance = $calculator->getDistance($point1, $point2);
Chúng ta đã áp dụng Template Method Pattern trong với AbstractDistanceCalculator
. Class này định nghĩa khung xử lý (template) cho việc tính khoảng cách:
- Gọi phương thức
exec()
để tính toán - Xử lý ngoại lệ nếu có
- Sử dụng fallback nếu cần
Các lớp con chỉ cần triển khai phương thức exec()
, không cần lo về cấu trúc tổng thể.
Lợi ích:
- Tái sử dụng code cho xử lý lỗi và cơ chế fallback
- Đảm bảo tất cả các lớp con đều theo một quy trình xử lý chuẩn
- Tránh lặp lại code
Phiên bản 6: Áp dụng Decorator Pattern
Cuối cùng, chúng ta thêm các chức năng phụ như caching và đảm bảo khoảng cách tối thiểu bằng Decorator Pattern:
class CachingDistanceCalculator implements DistanceCalculator
{
public function __construct(
private readonly DistanceCalculator $calculator,
) {}
public function getDistance(Point $point1, Point $point2): float
{
$keyCache = $this->getKeyCache($point1, $point2);
$distance = Cache::get($keyCache);
if ($distance > 0) {
return $distance;
}
$distance = $this->calculator->getDistance($point1, $point2);
Cache::put($keyCache, $distance, Carbon::now()->addWeek());
return $distance;
}
private function getKeyCache(Point $point1, Point $point2): string
{
return $point1->getLat(). '-' . $point1->getLong() . ':' . $point2->getLat() . '-' . $point2->getLong();
}
}
class MinDistanceCalculator implements DistanceCalculator
{
public function __construct(
private readonly DistanceCalculator $calculator,
) {}
public function getDistance(Point $point1, Point $point2): float
{
$distance = $this->calculator->getDistance($point1, $point2);
if ($distance < 200) {
return 200;
}
return $distance;
}
}
// Sử dụng tất cả các pattern cùng nhau
$baseCalculator = new DistanceByCarCalculator(
new DistanceByBikeCalculator(
new DistanceByBirdFlyCalculator()
)
);
// Thêm decorators
$cachedCalculator = new CachingDistanceCalculator($baseCalculator);
$finalCalculator = new MinDistanceCalculator($cachedCalculator);
// Sử dụng
$point1 = new Point(19.2, 34.35);
$point2 = new Point(33.2, 343.35);
$distance = $finalCalculator->getDistance($point1, $point2);
Lợi ích:
- Thêm chức năng mới mà không cần sửa đổi code cũ
- Có thể kết hợp các decorator theo nhu cầu
- Đảm bảo nguyên tắc Single Responsibility: mỗi class chỉ làm một việc
Phân tích lợi ích khi áp dụng các Design Pattern
1. Khả năng mở rộng
- Thêm phương pháp tính khoảng cách mới: Chỉ cần tạo class mới kế thừa từ
AbstractDistanceCalculator
- Thêm chức năng mới: Tạo decorator mới implement
DistanceCalculator
- Hỗ trợ dịch vụ mới: Có thể dễ dàng tích hợp với các API khác ngoài Google Maps
// Ví dụ: thêm tính năng log
class LoggingDistanceCalculator implements DistanceCalculator
{
public function __construct(
private readonly DistanceCalculator $calculator,
private readonly LoggerInterface $logger
) {}
public function getDistance(Point $point1, Point $point2): float
{
$this->logger->info("Tính khoảng cách từ ({$point1->getLat()},{$point1->getLong()}) đến ({$point2->getLat()},{$point2->getLong()})");
$distance = $this->calculator->getDistance($point1, $point2);
$this->logger->info("Kết quả: {$distance}");
return $distance;
}
}
2. Khả năng kiểm thử
Mỗi component có thể được kiểm thử riêng biệt:
- Test từng phương pháp tính khoảng cách
- Test cơ chế cache
- Test logic khoảng cách tối thiểu
- Test chuỗi fallback
3. Dễ bảo trì
- Các thành phần độc lập nên dễ dàng sửa chữa
- Không cần lo lắng về tác động đến các phần khác khi sửa một phần
- Code có tổ chức rõ ràng, dễ đọc và hiểu
Áp dụng vào các bài toán khác
Cách tiếp cận này có thể áp dụng cho nhiều bài toán khác:
Xử lý thanh toán
- Strategy: Các phương thức thanh toán (thẻ tín dụng, ví điện tử, chuyển khoản)
- Chain of Responsibility: Thử các cổng thanh toán khác nhau nếu một cổng thất bại
- Decorator: Thêm log, thông báo, tính phí giao dịch
Hệ thống xuất dữ liệu
- Strategy: Các định dạng xuất (CSV, Excel, PDF)
- Chain of Responsibility: Thử các phương pháp xuất thay thế
- Decorator: Thêm nén, mã hóa, ký số
Hệ thống lọc nội dung
- Strategy: Các thuật toán lọc khác nhau
- Chain of Responsibility: Áp dụng nhiều bộ lọc liên tiếp
- Decorator: Thêm caching, logging, báo cáo
Kết luận
Qua bài viết này, chúng ta đã thấy cách cải thiện một hệ thống đơn giản thành một giải pháp linh hoạt, dễ mở rộng bằng cách áp dụng các design pattern:
- Dependency Injection: Giảm sự phụ thuộc giữa các thành phần
- Strategy Pattern: Hỗ trợ nhiều thuật toán tính khoảng cách
- Chain of Responsibility: Xử lý lỗi và cung cấp cơ chế fallback
- Template Method: Chuẩn hóa quy trình xử lý
- Decorator Pattern: Thêm chức năng mà không cần sửa đổi code cũ
Việc áp dụng từng bước các design pattern giúp chúng ta hiểu rõ lợi ích của mỗi pattern và cách chúng kết hợp với nhau để tạo ra một giải pháp tổng thể mạnh mẽ.
Hãy nhớ rằng design pattern không chỉ là khái niệm học thuật mà là công cụ thực tế để giải quyết các vấn đề lập trình thường gặp. Khi bạn đối mặt với một vấn đề phức tạp, hãy xem xét các pattern này có thể giúp đơn giản hóa giải pháp của bạn như thế nào.
Chúc bạn coding vui vẻ!
All rights reserved