Đề bài:

Mô phỏng lại tính năng thuê game theo PAYG (Pay-as-you-go) của Octokit.
Người dùng trả tiền mua thời gian (tính bằng ngày), để tạo game, mỗi game có thể deploy nhiều lần, mỗi lần deploy thì trừ vào quỹ ngày của người dùng.

Trước tiên là Domain User

<?php
//tạo domain exception
class NotEnoughDaysException extends DomainException
{
    public function __construct()
    {
        parent::__construct('NOT_ENOUGH_DAYS');
    }
}

//Entity User 
class User
{
    public function __construct(public int $id)
    {
    }

    public static function make(int $id): static
    {
        return new static($id);
    }
}

enum UserTransactionPaygEnum: string
{
    case TOPUP = 'topup';
    case CONSUME = 'consume';
}


//Entity UserTransaction 
class UserTransaction
{
    public function __construct(
        public string $transactionId, 
        public int $userId,
        public UserTransactionPaygEnum $type, 
        public int $days
    )
    {

    }

    public static function make(
        string $transactionId, 
        int $userId,
        UserTransactionPaygEnum $type, 
        int $days
    ): static
    {
        return new static($transactionId, $userId, $type, $days);
    }

    public static function topup(
        string $transactionId, 
        int $userId, 
        int $days
    ): static
    {
        return static::make(
            $transactionId, 
            $userId, 
            UserTransactionPaygEnum::TOPUP,
            $days
        );
    }

    public static function consume(
        string $transactionId,
        int $userId, 
        int $days
    ): static
    {
        return static::make(
            $transactionId,
            $userId, 
            UserTransactionPaygEnum::CONSUME, 
            $days
        );
    }
}

//Contracts về các Repository 
interface IUserRepository
{
    public function findById(int $userId): ?User;

    public function save(User $user): void;
}

interface IUserTransactionRepository
{
    public function findByTransactionId(string $transactionId): ?UserTransaction;

    public function save(UserTransaction $userTransaction): void;

    /**
     * @return UserTransaction[]
     */
    public function findAllByUserId(int $userId): array;
}

//Domain Service, được phép call nhiều Entity
class UserPaygService
{
    public function __construct(
        public IUserTransactionRepository $userTransactionRepository
    )
    {

    }

    public function countRemainDays(int $userId): int
    {
        $userTransactions = $this->userTransactionRepository->findAllByUserId($userId);
        $total = 0;

        foreach ($userTransactions as $userTransaction) {
            $total += match ($userTransaction) {
                UserTransactionPaygEnum::TOPUP => $userTransaction->days,
                UserTransactionPaygEnum::CONSUME => -$userTransaction->days,
            };
        }

        return $total;
    }
}

Sau đây là Domain Game:


// đầu tiên code Domain Exception cho nghiệp vụ thuê game
class GameExpiredException extends DomainException
{
    public function __construct()
    {
        parent::__construct('GAME_EXPIRED');
    }
}

class GameNotDeployedException extends DomainException
{
    public function __construct()
    {
        parent::__construct('GAME_IS_NOT_DEPLOYED');
    }
}


//sau đó tạo value object thể hiện lịch thuê game
class LaunchGamePeriod
{
    public function __construct(
        public \DateTimeImmutable $from, 
        public \DateTimeImmutable $to
    )
    {
        if ($to->getTimestamp() < $this->from->getTimestamp()) {
            throw new InvalidArgumentException('PERIOD_INVALID');
        }
    }

    public function durationInDays(): int
    {
        return $this->from->diff($this->to, true)->days;
    }
}

//tao entity
class Game
{

    public function __construct(
        public string $gameId,
        public int $userId,
        public string $name
    )
    {
    }

    public static function make(
        string $gameId, 
        int $userId, 
        string $name
    ): static
    {
        return new Game($gameId, $userId, $name);
    }
}

class GameSchedule
{
    public function __construct(
        public string $gameId, 
        public \DateTimeImmutable $launchFrom, 
        public DateTimeImmutable $launchTo, 
        public $isActive = true
    )
    {

    }

    public static function make(
        string $gameId,
        LaunchGamePeriod $period
    ): static
    {
        return new static($gameId, $period->from, $period->to, true);
    }

    public function canPlay(?int $now = null): bool
    {
        if (!$now) {
            $now = time();
        }
        return $this->launchTo->getTimestamp() > $now;
    }

}

interface IGameRepository
{
    public function findById(string $gameId): ?Game;
    public function save(Game $game): void;
}

interface IGameScheduleRepository
{
    public function findActiveByGameId(string $gameId): ?GameSchedule;
    public function saveActive(GameSchedule $gameSchedule): void;
}

Tiếp tục là Tầng Application, chứa các UseCase:


//Application Service ở tầng application, đi theo use case, được phép call nhiều domain với nhau
class DeployGameService
{
    public function __construct(
        public UserPaygService $userPaygService,
        public IGameScheduleRepository $gameScheduleRepository
    )
    {

    }

    public function deploy(
        int $userId, 
        int $gameId,
        LaunchGamePeriod $launchGamePeriod
    ): void
    {
        $remainingDays = $this->userPaygService->countRemainDays($userId);
        if ($launchGamePeriod->durationInDays() > $remainingDays) {
            throw new NotEnoughDaysException();
        }
        $gameSchedule = GameSchedule::make($gameId, $launchGamePeriod);
        $this->gameScheduleRepository->saveActive($gameSchedule);
    }
}

class PlayGameService
{
    public function __construct(
        public IGameRepository $gameRepository,
        public IGameScheduleRepository $gameScheduleRepository
    )
    {

    }
    public function start(int $gameId): void
    {
        $game = $this->gameRepository->findById($gameId);
        $activeSchedule = $this->gameScheduleRepository->findActiveByGameId($gameId);
        if (!$activeSchedule) {
            throw new GameNotDeployedException();
        }

        if (!$activeSchedule->canPlay()) {
            throw new GameExpiredException();
        }

        // các logic khác như log lại....
    }
}

Câu hỏi

  • Câu 1: Đoạn code trên không có Aggregate Root? Căn cứ vào đâu?
  • Câu 2: Bổ sung Aggregate Root và refactor code lại như thế nào?

By admin