ありあけこういち’s diary

SaaS企業Webエンジニア|HSK6級|福岡ラバー|木管楽器|ITについて、明快な読み物を目指しています。

2023年3月29日のHKUST DiMBAのSample Class要約

こんにちは!ありあけこういちです。 今回は、2023年3月29日にあったHKUST DiMBAのSample Classについて紹介しておきたいと思います。

Info Session & Masterclass “Marketing Strategy and Price”

HKUST DiMBA とは何ぞや

HKUSTは、Hong Kong University of Science and Technologyで略語でして、香港科技大学[Xiāng Gǎng Kē jì Dà xué, ㄒㄧㄤ ㄍㄤˇ ㄎㄜ ㄐㄧˋ ㄉㄚˋ ㄒㄩㄝ]を指します。
そして、アジアMBAトップ校の一つのHKUSTが、2022年2月にアジアMBAで初となる完全オンラインで学位取得ができるDigital MBAプログラム(DiMBA)をスタートしています。
香港に住まなくても、母国で仕事しながら授業が取れるのが魅力的だと思います。
具体的な内容につきましては、下記の学校の公式ホームページをご参照ください。(英文)

mba.hkust.edu.hk

それがありあけさんと何の関係がありますか?

実は、私はHKUST DiMBAに今年合格しており来年の2月から履修予定です。
今回は、そのHKUST DiMBAで3月にあったSample Classの内容を軽くまとめておきたいと思います。
また後日、受験記をブログに書く予定です。

Sample Classの内容

今回のSample Classは、「Marketing Strategy and Price」についてでした。
内容を軽くまとめると下記になります。

Marketing Strategy and Price

本題に入る前にです。

  1. https://builtamart.com/products/ferrero-rocher-t8-plastic-box-100g
  2. https://builtamart.com/products/ferrero-rocher-t8-plastic-heart-box-100g

上記2つは、同じくチョコレートは8つ入ってますが、値段の違いはございますでしょうか?
正解は、記事投稿日(7月4日)基準で、2の方が圧倒的に高いです。
1は₱279.00、2は₱608.00になっています。ギフトパッケージにするだけで、2倍以上の値段になっています。

Sample Classの中ではその他にもいろんな例を紹介していました。

Price sensitivity own usage vs. gift choice

自分のものを買うときは、値段に敏感になっています。 情報が無い状態でプレゼントを買うと、値段に関して鈍感になってしまうことが分かります。
プレゼントに関する情報が多いほど、値段に関して徐々に敏感になっていきます。

授業で紹介した、The Guardian誌の2018年度の記事によると、イギリスでは10分の1の消費者がクリスマスの後に借金問題に落ちるらしいですが、筆者は当該記事は現時点では見当たりませんでした。 (UK: 1 in 10 consumers had fallen into financial debt after Christmas (The Guardian 2018))

なぜプレゼントを買うとよりお金を支払うことになるのか?

授業中で紹介するには、下記が起因することが多いようです。

  1. 相手に認めてもらいたい承認欲求 → お金をたくさん払うことで、認めてもらう。(You still care about me)
  2. 単純に相手と関係を維持したいから

この現象を踏まえると、販売者側と消費者側で取れる策は違います。 (The art of pricing. How can we use this information?)

販売者側で取れる策(Pricing strategies)

  1. Gift cardを作る
  2. AmazonのようなWishlist機能を持たない
  3. Price segmentation based on purchase goal(Webサイトに「Gift Ideas」のようなタブを作るなど)

消費者側で取れる策(Implications for consumers)

  1. Wishlistを活用する。
  2. プレゼントをもらう人が何を欲しがるか、何が好きなのかを把握しましょう。相手のことが分かればできると思うが、相手のことがあまりわからないのであれば、自分が使うと思って購入する。
  3. Signal value of the gift in different ways (Other than Price)
    → Time
    → Effort (thoughtful gift)

授業では、先生の子供が先生に絵を描いた例を紹介していました。確かにお金はあまりかかりませんが、感動しますよね。

また面白い授業内容があれば、共有させていただきます。

リポジトリについて

はじめに

ドメイン駆動設計におけるリポジトリについてまとめたいと思います。

リポジトリの概要

ドメインオブジェクトが永続化ストレージ(データベースやファイルシステムなど)にアクセスするためのインターフェースを提供するオブジェクトであり、エンティティや値オブジェクト、集約などのドメインオブジェクトの取得や格納を担当します。
なお、リポジトリの一番大きい特徴は永続化の隠蔽です。よって、リポジトリを使用する側がCSVだろうがDBだろうがファイルだろうが知ったこっちゃ無い状態にするべきだと思ってます。

以下は引用です。

オブジェクトを繰り返し利用するには、何らかのデータストアにオブジェクトのデータを永続化(保存)し、再構築(復元)する必要があります。リポジトリはデータを永続化し再構築するといった処理を抽象的に扱うためのオブジェクトです。 -- 成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.144). Kindle 版.

PHPコード例

リポジトリの概念をPHPコードで起こすと下記のようになります。
商品(以下Productオブジェクト)を取得、登録、更新、削除するリポジトリ処理を実装しています。

<?php
declare(strict_types=1);

namespace App\Product;
use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;
use Exception;
use PDO;

class ProductRepository
{
    private const NONE_PRODUCT = 0;
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function findById(ProductId $id): Product
    {
        $sql = "SELECT * FROM products WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => (string)$id]);

        if ($stmt->rowCount() === self::NONE_PRODUCT) {
            throw new Exception('商品が存在していません。');
        }
        $product = $stmt->fetch(PDO::FETCH_ASSOC);        

        $productId = new ProductId($product['id']);
        $productName = new ProductName($product['name']);
        $productPrice = new Price($product['price']);

        return new Product($productId, $productName, $productPrice);
    }

    public function save(Product $product): void
    {
        $sql = "INSERT INTO products (id, name, price) VALUES (:id, :name, :price)
               ON DUPLICATE KEY UPDATE name = :name, price = :price";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'id' => (string)$product->getProductId(),
            'name' => (string)$product->getProductName(),
            'price' => $product->getPrice()->toInt()
            ]);
    }

    public function delete(productId $productId): void
    {
        $sql = "DELETE FROM products WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'id' => (string)$productId
        ]);

        if ($stmt->rowCount() === self::NONE_PRODUCT) {
            throw new Exception('IDが' . (string)$productId . 'の商品を削除できませんでした。');
        }
    }
}

$productId = new ProductId('5819f3ad1c4b6');
$pdo = new PDO($dsn, $user, $password);
$productRepository = new ProductRepository($pdo);

//使用例
//商品取得
$product = $productRepository->findById($productId);

//商品作成
$newProductId = new ProductId('5819f3ad1c0ce');
$newProductName = new ProductName('name2');
$newProductPrice = new Price(120);

$newProduct = new Product($newProductId, $newProductName, $newProductPrice);
$productRepository->save($newProduct);

//商品更新
$updatedProduct = new Product($newProductId, new ProductName('name3'), new Price(200));
$productRepository->save($updatedProduct);

//商品削除
$productRepository->delete($newProductId);
?>

この例では、ProductRepositoryクラスがProductエンティティを永続化するためのメソッドを提供しています。

  • findByIdメソッドでは、指定されたIDのProductオブジェクトをデータベースから取得し、Productオブジェクトを返します。
  • saveメソッドでは、与えられたProductオブジェクトをデータベースに保存します。同じIDのProductオブジェクトが存在している場合は更新します。
  • deleteメソッドでは、指定されたIDのProductオブジェクトをデータベースから削除します。

今回はPDO*1を使用してデータベースにアクセスしていますが、LaravelのElloquentモデルなどでも同様に実装することができます。

ドメイン駆動設計とクリーンアークテクチャでのリポジトリの概念について

筆者はドメイン駆動設計でのリポジトリとクリーンアークテクチャでのリポジトリは、どちらも同様の永続化を担当するオブジェクトなので、コード上でも同じのように書いて差し支えないと考えています。
下記は引用です。

Repository は Interface Adapter レイヤーにある GateWays にあたります。 リポジトリパターンで知られており、特定のモデルのデータ永続化についてを抽象化したオブジェクトです。 今回の「ユーザ登録」ではユーザというモデルの永続化が必要になると思います。 -- 引用元:実装クリーンアーキテクチャ

クリーンアーキテクチャでのリポジトリJavaコード例(引用)

public class UserRepository : IUserRepository {
  public User FindByUserName(string username) {
    using (var con = new MySqlConnection(Config.ConnectionString)) {
      con.Open();
      using (var com = con.CreateCommand()) {
        com.CommandText = "SELECT * FROM t_user WHERE username = @username";
        com.Parameters.Add(new MySqlParameter("@username", username));
        var reader = com.ExecuteReader();
        if (reader.Read()) {
          var id = reader["id"] as string;
          return new User(
            id,
            username
          );
        } else {
          return null;
        }
      }
    }
  }

  public void Save(User user) {
    using (var con = new MySqlConnection(Config.ConnectionString)) {
      con.Open();

      bool isExist;
      using (var com = con.CreateCommand()) {
        com.CommandText = "SELECT * FROM t_user WHERE id = @id";
        com.Parameters.Add(new MySqlParameter("@id", user.Id.Value));
        var reader = com.ExecuteReader();
        isExist = reader.Read();
      }

      using (var command = con.CreateCommand()) {
        command.CommandText = isExist
          ? "UPDATE t_user SET username = @username WHERE id = @id"
          : "INSERT INTO t_user VALUES(@id, @username)";
        command.Parameters.Add(new MySqlParameter("@id", user.Id.Value));
        command.Parameters.Add(new MySqlParameter("@username", user.UserName));
        command.ExecuteNonQuery();
      }
    }
  }
}

-- コードの引用元:実装クリーンアーキテクチャ

引用元のJavaコードではクリーンアーキテクチャでのリポジトリの実装であり、ドメイン駆動設計のリポジトリと機能と同様モデルのデータ永続化を担当しています。
そのため、筆者はドメイン駆動設計でのリポジトリとクリーンアーキテクチャでのリポジトリはコード上では違いがないと考えています。

リポジトリとDAOの違い

筆者は、リポジトリとDAO(Data Access Object)は、データアクセスに関わるロジックをカプセル化している点について似た役割をもっていて、確かコード上ではリポジトリとDAOが同様の実装がされることもあるが、目的の違う異なる概念であると考えています。

筆者の理解をまとめると下記です。

  • リポジトリ:アプリケーションコードが、ドメインオブジェクトを操作するのが目的のオブジェクト。
  • DAO:データアクセスのメカニズムをカプセル化していて、データアクセスに目的のオブジェクトであり、基本的にRDBMSに最適化されたインターフェースを持っている。

以下は引用です。

リポジトリは、データベースを操作するため、従来のDAO(データアクセスオブジェクト)と似ているかもしれません。しかし、DAOがデータ中心指向であるのに対して、リポジトリオブジェクト指向アプローチである点で異なります。 -- 実践DDD本 第12章「リポジトリ」~集約の永続化管理を担当~

DAOというのは リポジトリより抽象度の低い、RDBMSに最適化されたI/F(insert, update, delete, selectなどSQLに近いI/F)を持っているはずです。 -- Scalaコードでわかった気になるDDD

上述のように、リポジトリとDAOは目的が異なります。

参考記事、書籍

*1:PHP Data Objectsで、PHPでデータベースにアクセスするためのデータベースアクセス抽象化層(Database Access Abstraction Layer)のことです。

ユースケースについて

はじめに

IT用語で広く使われている「ユースケース」という用語と、どのようにコード上に起こしていくか、特にドメイン駆動設計とクリーンアーキテクチャユースケースをどう表現するのかをまとめています。

ユースケースの概要

ユースケースとは

あるシステムやサービスが提供する機能を表現する概念で、利用者が行う一連の処理や操作を記述します。システムの全体像を捉えたり、要件定義にも活用されます。
以下は引用です。

サクッと一言で説明すると「システムの活用事例を図とかで何となく表現してみるから、それを見て、どんなシステムになるかイメージしようぜ!」なやり方が「ユースケース」です。 -- 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

ドメイン駆動設計でのユースケースの表現

ドメイン駆動設計では、ユースケースという概念をアプリケーションサービスを利用し実装します。

アプリケーションサービスを端的に表現するならば、ユースケースを実現するオブジェクトです。 -- 成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.185). Kindle 版.

アプリケーションサービスとは

外部からの入力を受け取り、必要な処理をドメイン層に依頼し、ドメイン層のオブジェクトを利用してビジネスロジックを実行し返された結果を加工して外部に返す役割を担います。

アプリケーションサービスについては、下記の記事でもコードの例を併せてまとめていますので、宜しければご確認ください。

jipai1121.hatenablog.com

クリーンアーキテクチャでのユースケースの表現

クリーンアーキテクチャでのユースケースは、Application Business Rules層にて表現します。 アプリケーションの重要なビジネスロジックを担当する層であり、Enterprise Business Rules層に依存します。
外の層であるInterface Adapters層やFrameworks & Drivers(データベース、UI、フレームワーク)の変更の影響は受けないように実装します。
以下は流用です。

Application Business Rules 赤いレイヤーは Application Business Rules です。 このレイヤーは「ソフトウェアが何ができるのか」を表現します。 Enterprise Business Rules に所属するオブジェクトを協調させ、ユースケースを達成します。 ドメイン駆動設計でいうところのアプリケーションサービスなんかはここの住人です。 -- 実装クリーンアーキテクチャ

図の引用元:https://qiita.com/nrslib/items/a5f902c4defc83bd46b8

phpコード例

以下は商品登録のユースケースをクリーンアークテクチャに基づいてphpコードに起こしたものです。
下記はバックエンドのコードでして、Frameworks & Drivers層およびPresenterの実装については省いております。*1
処理の流れをまとめると以下のようになります。

  1. ProductControllerがRegisterProductInputDataとRegisterProductOutputDataのインスタンスを作り、RegisterProductInteractor(ユースケース)に渡す。
  2. RegisterProductInteractorのhandleメソッドで商品の登録処理を行います。入力データから商品名と価格を取得し、商品ファクトリーを使って新しい商品オブジェクトを生成し、商品リポジトリに保存します。また、出力データ(RegisterProductOutputData)に商品の情報を設定します。
  3. ProductControllerでjsonのレスポンス形としてデータ整形を行い、レスポンスを返します。
Enterprise Business Rules(エンティティ層)
<?php
declare(strict_types=1);

namespace App\Product;

use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;

// 商品エンティティ
class Product
{
    private ProductId $productId;
    private ProductName $productName;
    private Price $price;

    public function __construct(
        ProductId $productId,
        ProductName $productName,
        Price $price
    ){
        $this->productId = $productId;
        $this->productName = $productName;
        $this->price = $price;
    }

    /**
     * @return ProductId
     */
    public function getProductId(): ProductId
    {
        return $this->productId;
    }

    /**
     * @return ProductName
     */
    public function getProductName(): ProductName
    {
        return $this->productName;
    }

    /**
     * @param ProductName $productName
     */
    public function setProductName(ProductName $productName): void
    {
        $this->productName = $productName;
    }

    /**
     * @return Price
     */
    public function getPrice(): Price
    {
        return $this->price;
    }

    /**
     * @param Price $price
     */
    public function setPrice(Price $price): void
    {
        $this->price = $price;
    }
}
?>
Application Business Rules(ユースケース層)

Enterprise Business Rules(エンティティ層)に依存します。

<?php
declare(strict_types=1);

namespace App\Product\ApplicationBusinessRules;

use App\Product\Product;
use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;

//Application Business Rules
class ProductFactory implements ProductFactoryInterface
{
    public function create(ProductName $productName, Price $price): Product {
        $productId = new ProductId(uniqid());
        return new Product($productId, $productName, $price);
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\ApplicationBusinessRules;

use App\Product\Product;
use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductName;

//Application Business Rules
interface ProductFactoryInterface
{
    public function create(ProductName $productName, Price $price): Product;
}
?>

<?php
declare(strict_types=1);

namespace App\Product\ApplicationBusinessRules;

use App\Product\Exception\SaveProductException;
use App\Product\InterfaceAdapters\ProductRepositoryInterface;
use App\Product\InterfaceAdapters\RegisterProductInputPort;
use App\Product\InterfaceAdapters\RegisterProductOutputPort;

//Application Business Rules
class RegisterProductInteractor {
    private ProductRepositoryInterface $repository;
    private ProductFactoryInterface $factory;

    public function __construct (
        ProductRepositoryInterface $repository,
        ProductFactoryInterface $factory
    ) {
        $this->repository = $repository;
        $this->factory = $factory;
    }

    /**
     * @param RegisterProductInputPort $input
     * @param RegisterProductOutputPort $output
     * @return void
     */
    public function handle(RegisterProductInputPort $input, RegisterProductOutputPort $output): void {

        try {
            $productName = $input->getProductName();
            $price = $input->getPrice();
            $product = $this->factory->create($productName, $price);
            $this->repository->save($product);

            $output->setProductId($product->getProductId());
            $output->setProductName($product->getProductName());
            $output->setPrice($product->getPrice());
        } catch (SaveProductException $e) {
            // リポジトリでのexception
            throw $e;
        }
    }
}
?>
値オブジェクト
<?php
declare(strict_types=1);

namespace App\Product\ValueObject;

// 値オブジェクト:商品価格
class Price
{
    /**
     * @var int
     */
    private int $value;

    /**
     * @param int $value
     */
    public function __construct(int $value)
    {
        $this->value = $value;
    }

    /**
     * @return int
     */
    public function toInt(): int
    {
        return $this->value;
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\ValueObject;

// 値オブジェクト:商品ID
class ProductId
{
    /**
     * @var string
     */
    private string $value;

    /**
     * @param string $value
     */
    public function __construct(string $value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->value;
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\ValueObject;

// 値オブジェクト:商品名
class ProductName
{
    /**
     * @var string
     */
    private string $value;

    /**
     * @param string $value
     */
    public function __construct(string $value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->value;
    }
}
?>
独自例外
<?php
declare(strict_types=1);

namespace App\Product\Exception;

use RuntimeException;
use Throwable;

class DeleteProductException extends RuntimeException
{
    public function __construct(
        string $message = "商品を削除できませんでした。",
        int $code = 500,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\Exception;

use RuntimeException;
use Throwable;

class FindProductException extends RuntimeException
{
    public function __construct(
        string $message = "商品情報の取得に失敗しました。",
        int $code = 500,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\Exception;

use RuntimeException;
use Throwable;

class NotFoundProductException extends RuntimeException
{
    public function __construct(
        string $message = "商品が存在していません。",
        int $code = 404,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\Exception;

use RuntimeException;
use Throwable;

class SaveProductException extends RuntimeException
{
    public function __construct(
        string $message = "商品の保存に失敗しました。",
        int $code = 500,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
    }
}
?>
Interface Adapters

Application Business Rules(ユースケース層)に依存します。

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;
use App\Product\ApplicationBusinessRules\RegisterProductInteractor;
use App\Product\Exception\SaveProductException;
use InvalidArgumentException;

//Interface Adapters
class ProductController
{
    // ステータスコードの定数を定義
    const HTTP_OK = 200;
    const HTTP_BAD_REQUEST = 400;
    const HTTP_INTERNAL_SERVER_ERROR = 500;

    private RegisterProductInteractor $usecase;
    public function __construct(
        RegisterProductInteractor $usecase
    )
    {
        $this->usecase = $usecase;
    }

    public function register(array $request): string {

        try {
            $this->validation($request);
            $input = new RegisterProductInput($request['name'], $request['price']);
            $output = new RegisterProductOutput();
            $this->usecase->handle($input, $output);

            // 表示するデータを整形して返す
            $response = [
                'id' => $output->getProductId(),
                'name' => $output->getProductName(),
                'price' => $output->getPrice()
            ];
            http_response_code(self::HTTP_OK); // 成功時のステータスコードを設定
            return json_encode($response);
        } catch (InvalidArgumentException $e) {
            $response = [
                'code' => self::HTTP_BAD_REQUEST,
                'message' => $e->getMessage()
            ];
            http_response_code(self::HTTP_BAD_REQUEST);
            return json_encode($response);
        } catch (SaveProductException $e) {
            $response = [
                'code' => $e->getCode(),
                'message' => $e->getMessage()
            ];
            http_response_code(self::HTTP_INTERNAL_SERVER_ERROR);
            return json_encode($response);
        }
    }

    private function validation(array $request): void
    {
        if (empty($request['name'])) {
            throw new InvalidArgumentException('Name is required');
        }

        if (empty($request['price'])) {
            throw new InvalidArgumentException('Price is required');
        }

        if (!is_numeric($request['price'])) {
            throw new InvalidArgumentException('Price must be a number');
        }
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;
use App\Product\Exception\DeleteProductException;
use App\Product\Exception\FindProductException;
use App\Product\Exception\NotFoundProductException;
use App\Product\Exception\SaveProductException;
use App\Product\Product;
use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;
use PDO;
use Throwable;

//Interface Adapters
class ProductRepository implements ProductRepositoryInterface
{
    private const NONE_PRODUCT = 0;
    private PDO $pdo;

    public function __construct() {
        $this->pdo = new PDO('mysql:dbname=test;host=localhost', 'test', 'password');
    }

    public function findById(ProductId $id): Product
    {
        try {
            $sql = "SELECT * FROM products WHERE id = :id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute(['id' => (string)$id]);

            if ($stmt->rowCount() === self::NONE_PRODUCT) {
                throw new NotFoundProductException("商品が存在していません。", 404);
            }
            $product = $stmt->fetch(PDO::FETCH_ASSOC);

            $productId = new ProductId($product['id']);
            $productName = new ProductName($product['name']);
            $productPrice = new Price($product['price']);

            return new Product($productId, $productName, $productPrice);
        } catch (NotFoundProductException $e) {
            //独自例外をそのままエスカレーション
            throw $e;
        } catch (Throwable $e) {
            throw new FindProductException($e->getMessage(), $e->getCode(), $e);
        }

    }

    public function save(Product $product): void
    {
        try {
            $sql = "INSERT INTO products (id, name, price) VALUES (:id, :name, :price)
               ON DUPLICATE KEY UPDATE name = :name, price = :price";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([
                'id' => (string)$product->getProductId(),
                'name' => (string)$product->getProductName(),
                'price' => $product->getPrice()->toInt()
            ]);
        } catch (Throwable $e) {
            throw new SaveProductException($e->getMessage(), $e->getCode(), $e);
        }
    }

    public function delete(ProductId $productId): void
    {
        try {
            $sql = "DELETE FROM products WHERE id = :id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([
                'id' => (string)$productId
            ]);
        } catch (Throwable $e) {
            throw new DeleteProductException($e->getMessage(), $e->getCode(), $e);
        }
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;

use App\Product\Product;
use App\Product\ValueObject\ProductId;

//Interface Adapters
interface ProductRepositoryInterface
{
    public function findById(ProductId $id): Product;
    public function save(Product $product): void;
    public function delete(ProductId $productId): void;
}
?>

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;

use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductName;

//Interface Adapters
class RegisterProductInput implements RegisterProductInputPort
{
    private ProductName $productName;
    private Price $price;

    public function __construct(string $productName, int $price)
    {
        $this->productName = new ProductName($productName);
        $this->price = new Price($price);
    }

    public function getProductName(): ProductName
    {
        return $this->productName;
    }

    public function getPrice(): Price
    {
        return $this->price;
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;

use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductName;

//Application Business Rules
interface RegisterProductInputPort
{
    public function getProductName(): ProductName;
    public function getPrice(): Price;
}
?>

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;

use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;

//Interface Adapters
class RegisterProductOutput implements RegisterProductOutputPort
{
    private string $productId;
    private string $productName;
    private int $price;

    public function getProductId(): string
    {
        return $this->productId;
    }

    public function getProductName(): string
    {
        return $this->productName;
    }

    public function getPrice(): int
    {
        return $this->price;
    }

    public function setProductId(ProductId $productId): void {
        $this->productId = (string)$productId;
    }

    public function setProductName(ProductName $productName): void {
        $this->productName = (string)$productName;
    }

    public function setPrice(Price $price): void {
        $this->price = $price->toInt();
    }
}
?>

<?php
declare(strict_types=1);

namespace App\Product\InterfaceAdapters;

use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;

//Interface Adapters
interface RegisterProductOutputPort
{
    public function getProductId(): string;
    public function getProductName(): string;
    public function getPrice(): int;
    public function setProductId(ProductId $productId): void;
    public function setProductName(ProductName $productName): void;
    public function setPrice(Price $price): void;
}
?>
値段が200の商品名が「じゃがいも」の商品を登録する使用例
<?php
declare(strict_types=1);

require('../backend/vendor/autoload.php');

use App\Product\ApplicationBusinessRules\ProductFactory;
use App\Product\ApplicationBusinessRules\RegisterProductInteractor;
use App\Product\Exception\SaveProductException;
use App\Product\InterfaceAdapters\ProductController;
use App\Product\InterfaceAdapters\ProductRepository;

$registerProductInteractor = new RegisterProductInteractor(new ProductRepository(), new ProductFactory());
$controller = new ProductController($registerProductInteractor);

$request = [
    'name' => 'じゃがいも',
    'price' => 200
];

$response = $controller->register($request);
echo $response;
?>

参考記事、書籍

*1:Presenterを使わない理由は、今回作るコードはバックエンドの実装であり、表示する実装がないため、表示のためのデータ成形を行うPresenterは特にいらないと思っています。

ドメインサービスについて

はじめに

DDDにおけるドメインサービスについてまとめたいと思います。

ドメインサービスの概要

そもそも「サービス」とは何か

サービスは、エンティティ、値オブジェクト、集約といった「ドメインオブジェクト」に記載するのが不自然なビジネスロジックをサービスに実装します。

以下は引用です。

DDDでは、エンティティ、値オブジェクト、集約といった「ドメインオブジェクト」だけではなく、それらの外に記述したほうがよいロジックも存在します。そのようなときに、状態を持たないステートレスな「サービス」を使用できます。 -- 実践DDD本 第7章「ドメインサービス」~複数の物を扱うビジネスルール~

サービスは主に「ドメインサービス」と「アプリケーションサービス」で分類されます。

ドメインサービスとは

ドメイン層において、複数のエンティティや値オブジェクトを利用して、ビジネスロジックを実現するためのオブジェクトです。
主に、複数の集約*1が必要な計算ロジック、もしくは一つの集約で使えるが、複雑な場合に「ドメインサービス」が使えます。
ドメインサービスはステートレスであり、計算に必要な情報は引数として取得します。ドメインの意味がわかるようなユビキタス言語*2でメソッド名とタイプを表現するとよりコードの可読性があがります。

アプリケーションサービスとは

外部からの入力を受け取り、必要な処理をドメイン*3に依頼し、ドメイン層のオブジェクトを利用してビジネスロジックを実行し返された結果を加工して外部に返す役割を担います。
アプリケーションサービス内には基本的にビジネスロジックを直接実装せず、ドメイン層のオブジェクトを組み合わせて、外部からの入力に対する処理を実現します。
このように、ドメインサービスとアプリケーションサービスは、異なる役割を持つオブジェクトであり、それぞれの責務を分離することで、ビジネスロジックの再利用性や柔軟性を高めることができます。

PHPコードの例

下記PHPコード例では、商品管理システムのドメインサービスとアプリケーションサービスを表現しています。
ProductDomainServiceというドメインサービスで、下記のビジネスロジックを表現しています。

  • 商品が存在しているか否か
  • 同じ名前の商品が存在しているか否か

下記のProductApplicationServiceというアプリケーションサービス、外部から引数を受け取り、商品を閲覧、登録、変更、削除するユースケースCRUD処理)を表現していて、ドメインサービスにあるビジネスロジックを確認してから処理しています。

<?php
declare(strict_types=1);

// ドメインサービス
class ProductDomainService
{
    public function isExistProduct(ProductId $productId): bool {

        // 当該商品の存在チェック
        // ...
        return true;
    }

    public function isExistSameProductName(ProductName $productName): bool {

        // 同じ名前の商品がすでに存在しているか調べる
        // ...
        return false;
    }
}

// 商品を管理(閲覧、登録、変更、削除)するアプリケーションサービス
class ProductApplicationService
{
    private ProductDomainService $productDomainService;
    private ProductFactory $productFactory;
    private ProductRepository $productRepository;

    public function __construct(ProductDomainService $productDomainService, ProductFactory $productFactory, ProductRepository $productRepository)
    {
        $this->productDomainService = $productDomainService;
        $this->productFactory = $productFactory;
        $this->productRepository = $productRepository;
    }

    public function readProduct(ProductId $productId): Product {
        if (!$this->productDomainService->isExistProduct($productId))
        {
            throw new Exception("存在しない商品です。");
        }

        // DBから商品情報を取得
        // ...

        return $product;
    }

    public function registerProduct(ProductName $productName, Price $price): void {
        if ($this->productDomainService->isExistSameProductName($productName))
        {
            throw new Exception("同じ商品名の商品が既に存在しています。");
        }
        $product = $this->productFactory->create($productName, $price);
        $this->productRepository->save($product);
    }

    public function changeProductName(Product $product, ProductName $productName): void {
        if (!$this->productDomainService->isExistProduct($product->getProductId()))
        {
            throw new Exception("存在しない商品です。");
        }
        if ($this->productDomainService->isExistSameProductName($productName))
        {
            throw new Exception("同じ商品名の商品が既に存在しています。");
        }
        $product->setProductName($productName);
        $this->productRepository->save($product);
    }

    public function changeProductPrice(Product $product, Price $price): void {
        if (!$this->productDomainService->isExistProduct($product->getProductId()))
        {
            throw new Exception("存在しない商品です。");
        }
        $product->setPrice($price);
        $this->productRepository->save($product);
    }

    public function deleteProduct(Product $product): int {
        $productId = $product->getProductId();
        if (!$this->productDomainService->isExistProduct($product->getProductId()))
        {
            throw new Exception("存在しない商品です。");
        }
        return $this->productRepository->delete($productId);
    }
}

// ファクトリ
class ProductFactory
{
    public function create(ProductName $productName, Price $price): Product {

        $productId = new ProductId(uniqid());
        // ...

        return new Product($productId, $productName, $price);
    }
}

// リポジトリ
class ProductRepository
{
    public function findById(ProductId $id): Product
    {
        // IDでProduct取得処理
        // ...

        return $product;
    }

    public function save(Product $product): void
    {
        // Product保存処理
        // ...
    }

    public function delete(ProductId $productId): int
    {
        // Product削除処理
        // ...

        return $rowCount;
    }
}

// 商品エンティティ
class Product
{
    private ProductId $productId;
    private ProductName $productName;
    private Price $price;

    public function __construct(
        ProductId $productId,
        ProductName $productName,
        Price $price
    ){
        $this->productId = $productId;
        $this->productName = $productName;
        $this->price = $price;
    }

    /**
     * @return ProductId
     */
    public function getProductId(): ProductId
    {
        return $this->productId;
    }

    /**
     * @return ProductName
     */
    public function getProductName(): ProductName
    {
        return $this->productName;
    }

    /**
     * @param ProductName $productName
     */
    public function setProductName(ProductName $productName): void
    {
        $this->productName = $productName;
    }

    /**
     * @return Price
     */
    public function getPrice(): Price
    {
        return $this->price;
    }

    /**
     * @param Price $price
     */
    public function setPrice(Price $price): void
    {
        $this->price = $price;
    }
}

// 値オブジェクト:商品名
class ProductName
{
    /**
     * @var string
     */
    private string $value;

    /**
     * @param string $value
     */
    public function __construct(string $value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->value;
    }
}

// 値オブジェクト:商品ID
class ProductId
{
    /**
     * @var string
     */
    private string $value;

    /**
     * @param string $value
     */
    public function __construct(string $value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->value;
    }
}

// 値オブジェクト:商品価格
class Price
{
    /**
     * @var int
     */
    private int $value;

    /**
     * @param int $value
     */
    public function __construct(int $value)
    {
        $this->value = $value;
    }

    /**
     * @return int
     */
    public function toInt(): int
    {
        return $this->value;
    }
}

// 使用例
$productId = new ProductId("4b3403665fea6");
$productDomainService = new ProductDomainService();
$productFactory = new ProductFactory();
$productRepository = new ProductRepository();
$productApplicationService = new ProductApplicationService($productDomainService, $productFactory, $productRepository);

// 商品登録
$productApplicationService->registerProduct(new ProductName("ハンドクリーム"), new Price(500));

// 商品閲覧
$product = $productApplicationService->readProduct($productId);

// 商品名変更
$potato = new Product(new ProductName("じゃがいも"), new Price(100));
$productApplicationService->changeProductName($potato, new ProductName("ジャガイモ"));

// 商品価格変更
$chicken = new Product(new ProductName("チキン"), new Price(400));
$productApplicationService->changeProductPrice($chicken, new Price(500));

// 商品削除
$chair = new Product(new ProductName("椅子"), new Price(1000));
$productApplicationService->deleteProduct($chair);
?>

参考記事、書籍

*1:集約については、現在の記事では具体的には説明しませんが、軽く触れておくとオブジェクトのまとまりで整合性を保ちながらデータを更新する単位であり、関連エンティティと値オブジェクトを概念的に束ねたものだと筆者は理解しています。一つの集約に属したエンティティは基本的に他の集約には属しません。集約にドメインサービスを渡すのは、アプリケーションサービスの責務になります。

*2:プロジェクトには認識の齟齬や翻訳にコストをかけないためにも共通言語を作ることが求められます。そういったプロジェクトにおける共通言語のことをユビキタス言語といいます。ユビキタスは「いつでもどこでも存在する」といった意味です。つまり、ユビキタス言語には、プロジェクトのいたるところで使われなくてはならないという意味が込められています。ドメインエキスパートとの会話はもちろん、開発者同士の会話においてもユビキタス言語は使われ、そしてコードにもユビキタス言語は現れます。

成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.518). Kindle 版.

*3:ドメイン層には 1. エンティティ 2. 値オブジェクト 3. 集約 4. リポジトリ 5. ドメインサービス があります。引用元: * 최범균, 도메인 주도 개발 시작하기, 한빛미디어, 2022, p80.

MySQLのForeign key(外部キー)についての整理およびPrimary Key、Unique Key との違いについて

 

どうも、ありあけこういちです。
MySQLにはいろんなキー制約があり、何を使えばよいか分からないことがありましたので、MySQLプライマリーキー、外部キー、ユニークキーどう使い分けるかまとめました。

Foreign key(外部キー)

  • 外部キーとは、親テーブルと子テーブル間の関係データベースにおいてデータの整合性を保つための制約(参照整合性制約)であり、子テーブルに付与する。

  • テーブル間の関係性を設定する時に使用。

  • NULLを含めることが可能。

基本的な制約文は下記になります。

CONSTRAINT 外部キー名 FOREIGN KEY カラム名 REFERENCES 親テーブル名 (親テーブルのカラム名)

外部キー制約を削除する

ALTER TABLE テーブル名 DROP CONSTRAINT 外部キー名

親テーブルのレコードを削除する場合に外部キーで紐づいている子テーブルのレコードまで自動的に削除したい場合

ON DELETE CASCADE句を付けます。

CONSTRAINT 制約名 FOREIGN KEY カラム名 REFERENCES 親テーブル名 (親テーブルのカラム名) ON DELETE CASCADE

外部キー制約を一時的に無効化する

外部キー制約を適用しているテーブルにはDROP TABLEができないため、本番ではその挙動で良いものの、開発中にテーブルをまるごと作り直したい時などにこの制約が邪魔になることがあります。
その際に、外部キー制約を一時的に無効化するクエリです。

ALTER TABLE テーブル名 DISABLE CONSTRAINT 外部キー名

再度有効化する時は下記です。

ALTER TABLE テーブル名 ENABLE CONSTRAINT 外部キー名

外部キー制約を全体的に一時的に外すときは「0」を代入します。

SET foreign_key_checks = 0;

再び有効にするには「1」を代入します。

SET foreign_key_checks = 1;

一意(unique)とは?

Unique KeyとPrimary Keyの説明の前に、一意について軽くまとめてみますと、一意とは「重複しない」ことを意味します。
例として、a,b,c,dはそれぞれ重複しないので一意が取れていますが、a,a,b,cはaが重複しているので全体でみると一意とは言えません。

Unique Key

  • 一意キー(UNIQUE 制約が設定されたカラムには重複した値を格納できない。)

  • 一つのテーブルにいくつでも設定することが可能

    • Primary Keyと代表的な違い

    • 複数のカラムに対して UNIQUE 制約を設定することができる。

  • UNIQUE 制約の場合は値として NULL を格納することができ、また複数のカラムに NULL を格納できる。

基本的な制約文(CREATE TABLEの場合)

CREATE TABLE テーブル名 (カラム名 データタイプ UNIQUE, ...)

Primary Key(主キー)

  • 一意キー(重複を禁止)

  • 必ず何かの値を持っている。(NOT NULL)

  • 一つのテーブルに対し一つのみ作成。(Unique Keyと大きな違い。)

  • データの出席番号のようなイメージ
    下記のリファレンスから抜粋
    https://wa3.i-3-i.info/word1991.html

基本的な制約文(CREATE TABLEの場合)

CREATE TABLE テーブル名 (カラム名 データタイプ PRIMARY KEY, ...)

複数カラムの組み合わせに対してPrimary Key制約を設定する場合の制約文

CREATE TABLE テーブル名 
(カラム名1 データタイプ1, カラム名2 データタイプ2, ..., 
PRIMARY KEY(カラム名1, カラム名2, ...))

参考文献

Git コマンド 備忘録

Gitコマンドの整理に至った経緯

いままでSourceTreeなどのGUIに頼りすぎていたので、改めてGitの基礎を整理しておこうと思い、現投稿にまとめました。
また後日Git コマンドとSourceTreeとの違いについてもまとめたいと思います。

Gitコマンド

リポジトリを新規に作成(git init)

$ git rebase <リベース元ブランチ名>

リベースでミスをしたときは、直近の変更履歴を閲覧

$ git reflog

戻したい位置を選んで元に戻す。コード内容も変更。(commitの取消しにも使えます)

$ git reset --hard HEAD@{戻したい位置のHead番号}

戻したい位置を選んで元に戻す。コード内容は変更なし。(commitの整理に使います。)

$ git reset --soft HEAD@{戻したい位置のHead番号}

git rebase 自体を中断して元にもどす

$ git rebase --abort

conflict解消後 git rebaseを継続

$ git rebase --continue

上記コマンド流した後、「 :q 」 (変更を保存せずに終了) または「 :wq 」 (変更を保存して終了) と入力を押下することで、編集モードを終了することができます。

チェリーピック(cherry-pick)

$ git checkout <チェリーピックを反映したいブランチ名>
$ git cherry-pick <gitハッシュ値>

※ チェリーピックは差分のみをとるのであり、その時のスナップショットを撮るものではないことに注意。
Cherry-pickは英語でいいところだけをつまみ食いするとの意味があるようです。

コミット(Git Commit)

$ git commit -m "コミットメッセージ"

リモートブランチにプッシュ(Git Push)

$ git push origin <ブランチ名>

Force Push

$ git push --force origin <ブランチ名>

マージ(Git Merge)

$ git checkout {マージ元ブランチ名}
$ git merge {マージ先ブランチ名}

commit と push の違い

恥ずかしながら、現在までcommitとpushの違いをあまり理解できてなかったので、改めて再度調べていました。
SourceTreeではcommitと同時にPUSHするように設定する設定がある他、現在までPush changes immediately to {ブランチ名}を無意識的にチェックを入れて使ってたのであまり意識していない状態でした。

  • commitは、インデックスにあるファイルをローカルリポジトリに保存です。

  • pushは、保存したデータをリモートリポジトリの保存することです。

Wordで例えますと、文章を書いて保存する処理がcommit、保存した文書をクラウドに保存することになります。

 


参考サイト