Clean Architecture考察

そもそもなぜクリーンアーキテクチャーを考察するのか

DRY原則やSOLID原則などが浸透している昨今ですが、実際の開発現場のソースコードを読み込んでみると必ずしもこれらの原則に則していない場合は多いのではないでしょうか。

そして、そういった開発環境でいざコーディングをしていくと、以下のような問題に直面するのではないでしょうか。

  • あるバグの修正をしたのだが、同じロジックが他の場所でも書かれていたようで重複箇所のバグは依然としてバグったままだった。

  • あるクラスを変更したが、依存性の方向性や範囲が把握しきれておらず、変更の影響で新たなバグを生んでしまった。

  • ビジネスロジックの変更を迫られたが、同じロジックが重複しすぎており修正範囲を特定するだけで一苦労。

  • 想定外の値の入力があり、バグが発生してしまった。

これらは、運用しながら仕様が変わっていくようなスタートアップ系のプロダクトでは大なり小なりどこにもで発生しうる問題です。

これらの問題の共通の原因は、ソースコード「可変性」が欠如している事です。

今回はこういった問題を解決できる糸口を掴むべく、クリーンアーキテクチャーを考察してみたいと思います。

なぜ「可変性」が失われていくのか

新しくプロダクトを新規で作成する時はまだソースコードのサイズは小さく、ちょっとした仕様の変更ならば比較的変更の「影響範囲」も肉眼で確認できるレベルなので、それほど神経質になることは無いと思います。

しかし、幸いにもプロダクトが軌道にのり、運用期間が長くなるにつれてソースコードのサイズは肥大化していき、ある処理がソースコード全体に与えている「影響範囲」も人間の肉眼だけでは把握できなくなっていき変更コストが増大します。

また、オブジェクト指向ではClass同士に「依存関係」というものが発生しますが、この「依存関係」に縛られて凝り固まってしまっている場合、仕様変更などで機能の取替を迫られた場合の変更が困難になります。

「依存関係」とは

依存というのは、例えばクラスAの機能がクラスBの機能を前提に作られている場合などを指し、この場合は「AはBに依存をしている」と言える。

ここで、すこし具体的な例を提示します。

<?php

/**
 * ユーザーの商品購入を実行するクラス
 */
class UserBuyProductService
{
    public function buy(int $userId, int $productId, int $amount): void
    {
        // 決済の処理を行う
        $stripePayment = new StripePayment();
        $stripePayment->payment($userId, $amount);
        
        // 決済が成功したらDatabaseなどに購入した事実を記録する。
    }
}

/**
 * 決済機能を司るクラス
 */
class StripePayment
{
    public function payment(int $userId, int $amount): void
    {
        // ここで決済処理を実行する。
    }
}

上記UserBuyProductServiceは、あるユーザーが$productIdを持つ商品を購入したときの処理を表しており、この購入処理の中でStripePaymentクラスをインスタンス化してpaymentメソッドを使用することで購入処理を実現しています。

これは、UserBuyProductServiceクラスはStripePaymentに依存していると表現することができ、この様な状態を密結合と呼びます。

「密結合」の何がいけないのか

結論から申し上げますと、密結合ソースコード可変性が低くなります。

例えば、前項のUserBuyProductServiceのケースでは決済機能としてStripeを使用していますが、これがビジネスサイドの要望でGMOPaymentGatewayに変更を迫られたようなケースを考えてみましょう。

決済機能は「UserBuyProductService」だけで使われているとは限りません。

だから、既存のソースコードの中で「StripePayment」の影響を受けている処理を全て洗い出して漏れなく「GMOPaymentGateway」の処理にリファクタリングする必要があります。

大規模で複雑なプロダクトほど、「ただ切り替えるだけ」という変更要件に多大な変更コストが発生してしまうのです。

このようなケースの問題点を冷静にみつめると、ソースコード全体が「Stripeで決済をする」という「具体」に対して依存してしまっていることが問題だと仮定することができます。

では、具体の逆説である抽象ソースコードが依存した場合のケースを考えてみましょう。

「抽象に依存」とは

ここで、抽象とはなにか?と言うことを明確に定義する必要があります。

抽象とは「抽出」して「象(かたど)る」と書きます。

つまり、抽象とは、 「ある物事から共通項を抜き出して(抽出)、規格を作る(象る)」 と言い表すことが出来きるかと思います。

この規格を定義するのに、オブジェクト指向プログラミングでは「interface」を使います。

<?php

/**
 * 決済機能の実装クラスが実装していなければいけない
 * 機能の規格だけを定義する。
 * Interfaceは単なる規格(型)なのでインスタンス化されない。
 */
interface Payment
{
    /**
    * 決済を実行する抽象メソッド
    **/
    public function payment(int $userId, $amount): void;
}

上記のように、「interface」として、決済機能の振る舞いとして必要な振る舞いを「引き数」「戻り値」の型と共に定義します。

そして、実際に商品購入処理を行っている「UserBuyProductServie」を、このinterfaceに依存するようにします。

<?php

/**
 * ユーザーの商品購入を実行するクラス
 */
class UserBuyProductService
{
    private $paymentManager;
    
    public function __construct(Payment $peymentManeger)
    {
        $this->paymentManager = $peymentManeger;
    }
    
    public function buy(int $userId, int $productId, int $amount): void
    {
        // 決済の処理を行う
        // $stripePayment = new StripePayment();
        // $stripePayment->payment($userId, $amount);
        
        $this->paymentManager->payment($userId, $amount); // ここの処理がInerface経由になった
        
        // 決済が成功したらDatabaseなどに購入した事実を記録する。
    }
}

/**
 * 決済機能を司るクラス
 */
class StripePayment implements Payment
{
    public function payment(int $userId, int $amount): void
    {
        // ここでStripeでの決済処理を実行する。
    }
}

/**
 * 決済機能を司るクラス
 */
class GmoPaymentGateway implements Payment
{
    public function payment(int $userId, int $amount): void
    {
        // ここでGMOでの決済処理を実行する。
    }
}

上記の変更点は、「UserBuyProductService」のコンストラクタで「Payment」インターフェースを経由して受け取ったインスタンスを、メンバ変数である$paymentMangerに代入し、$paymentMangerからpaymentメソッドを実行して決済を実現しています。

この様な方法を「コンストラクタインジェクション」と言いますが、ここでは「Payment型」インスタンスであればなんでも受け取れるようになります。

そのため、Paymentインターフェースを実装した具象クラスであれば、如何なるインスタンスでも「UserBuyProductService」に外部注入することができます。

この様に、依存関係をInterfeceなどの抽象を通じて外部から注入することをDependency Injection」と言い、こうすることで「可変性」「拡張性」を得ることができるのです。

また、Laravelの場合は「ServiceProvider」にて、「抽象クラス」と「具象クラス」の紐付けを設定することができます。

以下は、ServiceProviderの例

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Packages\Application\Product\ProductCreateInteractor;
use Packages\Application\Shop\ShopCreateInteractor;
use Packages\Application\User\UserCreateInteractor;
use Packages\Domain\CommonRepository\DataStoreTransactionInterface;
use Packages\Domain\CommonRepository\UuidGeneratorInterface;
use Packages\Domain\Models\Product\ProductRepository;
use Packages\Domain\Models\Shop\ShopRepository;
use Packages\Domain\Models\User\UserRepository;
use Packages\Infrastructure\EloquentRepository\DataStoreTransactionEloquentRepository;
use Packages\Infrastructure\EloquentRepository\ProductEloquentRepository;
use Packages\Infrastructure\EloquentRepository\ShopEloquentRepository;
use Packages\Infrastructure\EloquentRepository\UserEloquentRepository;
use Packages\Infrastructure\LaravelFeatureRepository\UuidGenerateLaravelFeatureRepository;
use Packages\UseCase\Product\Create\ProductCreateUseCaseInterface;
use Packages\UseCase\Shop\Create\ShopCreateUseCaseInterface;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;
use Packages\UseCase\User\Get\UserGetUseCaseInterface;
use Packages\Application\User\UserGetInteractor;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        /**
         * Eloquentリポジトリを登録
         */
        $this->app->bind(
            DataStoreTransactionInterface::class,
            DataStoreTransactionEloquentRepository::class
        );

        $this->app->bind(
            UserRepository::class,
            UserEloquentRepository::class
        );

        $this->app->bind(
            ShopRepository::class,
            ShopEloquentRepository::class
        );

        $this->app->bind(
            ProductRepository::class,
            ProductEloquentRepository::class
        );

        /**
         * ファサード系Repositoryを登録
         */
        $this->app->bind(
            UuidGeneratorInterface::class,
            UuidGenerateLaravelFeatureRepository::class
        );

        /**
         * UserCaseを登録
         */
        $this->app->bind(
            UserCreateUseCaseInterface::class,
            UserCreateInteractor::class
        );

        $this->app->bind(
            UserGetUseCaseInterface::class,
            UserGetInteractor::class
        );

        $this->app->bind(
            ShopCreateUseCaseInterface::class,
            ShopCreateInteractor::class
        );

        $this->app->bind(
            ProductCreateUseCaseInterface::class,
            ProductCreateInteractor::class
        );
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

これを活用することにより、例えば、「Paymentインターフェースがコンストラクタインジェクションされる時にはGmoPaymentGatewayが自動的に具象クラスとしてインジェクションされる」といった設定を行うことができます。

この様に「抽象」に依存することによって、この先、決済機能を「Pay.jpに切り替えたいんだけど...」などという要求が発生た場合、「Payment」インターフェースを実装した「PayJp」クラスを作成し、ServiceProviderの「Payment」インターフェースに紐づく具象クラスを「PayJp」クラスに切り替えるだけで、ソースコードの全ての決済処理を「PayJp」に切り替える事ができます。

クリーンアーキテクチャーとは

前置きが長くなりましたが、ここでようやくクリーンアーキテクチャーについて見ていこうと思います。

クリーンアーキテクチャーといえば、この同心円のイメージが有名ですし、本家です。

簡潔に言うならば、コードをレイヤーに分け、依存性の方向を一方向にすることで保守性を高めようとするためのガイドラインだと言えるでしょう。

同心円の図からそれぞれの意味を理解する

基礎的な考え方を理解しなければ話は進まないので、まずは同心円の図について読解していきます。

依存の方向性

この矢印はレイヤー同士の依存関係の方向性を示している。

つまり、同心円の外側の要素が、内側の要素に向かって依存をするようにし、「Entities」などの内側の要素が外側に向かって依存しないようにすることを示しています。

同心円の図によれば、clean architectureの全ての要素の依存の方向性は全て「Entities」に向けられています。

では、この「Entites」とは一体何者なのでしょうか。

Entities

同心円の最も中心にある「Entities」がこれに当たります。

「ビジネスルール」を集める場所であり、そのアプリケーションの「名詞」にあたるモデルを定義する場所です。

「ビジネスルール」とは、そのアプリケーションがシステム化(自動化)されていないとした場合でも存在するドメインルールの事。

例えば、ECショップのシステムであれば、User(出品者)、Shop(店舗)、Product(商品)などのドメイン知識が「Entity」クラスとして表現されることになります。

ECショップというものは、システム化される以前も「出品者」と「店舗」と「商品」といった実体は存在し、ビジネスとはこれら名詞同士の関係性によって説明ができます。

「出品者」が「店舗」を開いて、「商品」を陳列した

このように、名詞そのものの属性や、名詞同士の繋がり合いのことを「ビジネスルール」と言います。

「Entities」の領域はこれらのビジネスルールを、他のレイヤーから保護するように隠蔽します。

そして、その他のレイヤーがこの隠蔽されたビジネスルールに依存することで、ビジネスルール(仕様)の変更を一箇所に集めることを実現するのです。

ここで、Userのビジネスルールを考えてみましょう。User(出品者)は、名前などの情報を持っています。

簡単に例を書くと以下の様になる。

<?php

/**
 * Class UserEntity
 * 出品者の情報を表現するEntityクラス
 */
class UserEntity
{
    /** @var string **/
    private $id;

    /** @var string **/
    private $name;


    public function __construct(
        string $id = null,
        string $name
    ) {
        $this->id = $id;
        $this->name = $name;
    }
    
    public function getId(): string
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

ただしこれでは不完全で、上記のコードの様にクラスのメンバがプリミティブ型(stringやintなどの原始的な型)で定義されていると、なにかと不都合な事が生じます。

ここで登場するのが「ValueObject」という値を表現するオブジェクトです。

ValueObject

プリミティブ型は、「stringは文字列、intは数値」とある程度、型の制約をしてくれるものの、ビジネスルールとしてはこの制約では不十分です。

たとえば上記のUserEntityクラスの場合、Userの氏名であるnameの文字数はstring型の最小単位の空文字を許容して良いのでしょうか?

この様に、プリミティブ型の制約はビジネスルールの制約よりもザルであることを踏まえ、以下のようにValueObjectを作成します。

<?php

/**
 * Class UserName
 * 出品者の氏名を表す値
 */
class UserName
{
    // 名前の最低文字数を表す定数
    public const MIN_LENGTH = 1;
    
    /** @var string **/
    private $_value;
    
    private function __construct(string $value)
    {
        $this->_value = $value;
    }
    
    public static function create(string $value): self
    {
        // 入力値の審査をする
        self::validation($value);
        
        return new self($value);
    }
    
    //値の審査をし、審査基準に満たなければエラーをスローする。
    private static function validation(string $value): bool
    {
        if (mb_strlen($value) < self::MIN_LENGTH) {
            throw new RuntimeException('名前は必須です。');
        }
    }
    
    // プリミティブな値を返すgetterメソッド
    public function value(): string
    {
        return $this->_value();
    }
}

このようにValueObjectを作成し、先程のUserEntityをリファクタリングします。

<?php

/**
 * Class UserEntity
 * 出品者の情報を表現するEntityクラス
 */
class UserEntity
{
    /** @var string **/
    private $id;

    /** @var UserName **/
    private $name;


    public function __construct(
        string $id = null,
        UserName $name
    ) {
        $this->id = $id;
        $this->name = $name;
    }
    
    public function getId(): string
    {
        return $this->id;
    }

    public function getName(): UserName
    {
        return $this->name;
    }
}

変更点は、コンストラクタでの$nameの取得の型がstringからUserNameに変更され、UserEntityのメンバ変数である$nameもUserNameに型が変更されています。

これを実際にインスタンス化すると以下のようになります。

<?php

$userEntity = new UserEntity(
    'aaaa-bbbb-ccccc', // Userの識別子ID
    UserName::create('斎藤 一') // UserNameとして名前を生成
);

この時、もしもUserName::createの引き数に与えられたプリミティブの値が必要文字数を満たしていなければ、UserNameの値審査機能がエラーをスローし、不正な値の混入を阻止します。

また、ビジネスルールの変更で、「UserNameは最低文字数を5文字にしてほしい。」などという要求が出てきた場合も以下の箇所を一箇所変更するだけで全てのコードにルールの変更を反映させることができます。

<?php

class UserName
{
    // 名前の最低文字数を表す定数
    public const MIN_LENGTH = 5; // ここを変更するだけ。
    
    (以下省略......)

この様に、MIN_LENGTHの値を一箇所変えることで、ソースコード全体のUserNameに関わる処理に対してルールの変更を伝播することができます。

ここまで、EntitiesValueObjectをみてきましたが、これらを定義しただけではまだアプリケーションとして体をなしていません。

モリー上で表現したEntityやValueObjectの状態を保存や再構築することが、アプリケーションをたり立たせる上で必要になります。

それを実現するのが「Gateways」という存在です。

Gateways

LaravelではしばしばRepositoryパターンが採用されますが、このRepositoryがGatewaysに該当します。

主にデータベースに対する保存や、再構築を担当するレイヤーです。

Laravelの場合はこの領域でEloquentモデルのORMを使うことでDatabaseの操作を行います。

ここで問題となるのが、依存性の方向性です。

普通にRepositoryをクラスとして定義してしまうと、それを使う後述するUseCase層からGatewaysに依存の方向性が直接逆流してしまいます。

ここでInterfaceを使った「依存性逆転の原則」を使うことで、依存の方向性を維持することを実現します。

この例でみると、具象クラスであるReopsitory(Gateways層)はinterfaceを実装することでInterfaceへ依存のベクトルを延ばしています。(白抜きの矢印は実装による依存ベクトルを示す。)

この事を「依存性逆転の原則」と言います。

まずInterfaceから見ていきましょう。

<?php

interface UserRepositoryInterface
{
    /**
     * @param UserEntity $user
     * @return UserEntity
     */
    public function save(UserEntity $customer): UserEntity;
}

この様に、引数と戻り値の型を制約したInterfaceを定義します。

この抽象に依存する形で、具象クラスであるRepositoryを作成します。

<?php

class UserRepository implements UserRepositoryInterface
{
    /**
    * @param string $uuid PHPサイドで生成したUUIDの識別子
     * @param UserEntity $user
     * @return UserEntity
     */
    public function save(string $uuid, UserEntity $user): UserEntity
    {
        // 具象クラスとして処理を実装
        /**
        * @var User ORマッパーのEloqunetUserモデル
        */
        $ormUser = User::create([
            'id' => $uuid,
            'name' => $customer->getName()->value(),
        ]);
        
        // UserEntityをORMから再構築して返却
        return new UserEntity(
            $ormUser->id,
            UserName::create($ormCustomer->name)
        );
    }
}

この様に、InterfaceにRepositoryが依存する形をとり、UseCaseなどからはInterfaceをサービスコンテナを通じて呼び出すようにすることで、依存性の方向性を維持することができる。

Geteways層であるRepositoryをInterfaceを通じて抽象に依存させることの意味は、UseCaseからみた時のRepositoryの「可変性」を維持するためと言えます。

以下の図を御覧ください。

この様に、RepositoryInterfaceを実装した具象クラスがEloquentRepositoryInmemoryRepositoryの2つ実装されているケースを考えます。

InmemoryRepositoryの用途は主にテスト用で、特にDataStoreなどに保存することなく、連想配列としてメンバ変数に値を保存するようなクラスです。

RepositoryInterfaceは現状ではこれら2つの具象クラスをbundleしている形になっており、UseCaseはこのRepositoryInterfaceへ依存しているため、どちらの具象クラスも外部注入(DI)することが叶います。

この性質を利用すれば、実際の運用の際はEloquentRepositoryを使用し、TestのときはInmemoryRepositoryを使用することができ、簡単にテストデータの構築を行うことができます。

また、技術の革新により新たに高性能なDataStoreが生まれて、DataStoreをそちらに乗り換えたい。

そして、そのDataStoreがEloquentモデルに対応していない...。

その様な場合でも、RepositoryInterfaceを実装した新たなDataStore用の具象Repositoryを作成することで、UseCaseなどの層に影響を与えることなく入れ替えを行うことができるのです。

この様に、アプリケーションを特定の技術基盤に縛られないように「pluggable」(脱着可能)にして置くことで、ここでも「可変性」を守っているというわけです。

UseCase

UseCaseはアプリケーションのAPI で、ドメインオブジェクトを操作し利用者の目的を達成することが役割で、1つのビジネストランザクションとして定義します。

Entityは「名詞」に該当するということを上げましたが、UseCase「活動(動詞)」を表現します。

「pluggable」な建て付けにするためのinterfaceとその実装がこれにあたります。

「pluggable」にすることで、テストの際はテスト用のUseCaseの具象クラスとすり替えて、UI層のテストを行いやす行くすることや、Databaseの建て付けなどがまだ確定していない段階でのフロントエンドの先行開発なども容易になります。

ここで具体的なUseCaseの実装例を示します。

<?php

namespace Packages\Application\User;

use Packages\Domain\CommonRepository\UuidGeneratorInterface;
use Packages\Domain\Models\User\AuthUserEntity;
use Packages\Domain\Models\User\UserEmail;
use Packages\Domain\Models\User\UserId;
use Packages\Domain\Models\User\UserName;
use Packages\Domain\Models\User\UserPassword;
use Packages\Domain\Models\User\UserRepository;
use Packages\UseCase\User\Create\UserCreateRequest;
use Packages\UseCase\User\Create\UserCreateResponse;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /** @var UserRepository  */
    private $userRepository;

    /** @var UuidGeneratorInterface  */
    private $uuidGenerator;

    /**
     * UserCreateInteractor constructor.
     */
    public function __construct(
        UserRepository $userRepository,
        UuidGeneratorInterface $uuidGenerator
    ) {
        $this->userRepository = $userRepository;
        $this->uuidGenerator = $uuidGenerator;
    }

    public function __invoke(UserCreateRequest $request): UserCreateResponse
    {
        // Userのドメインモデルを生成
        // この時、全ての値の審査も行われる
        $user = new AuthUserEntity(
            UserId::create($this->uuidGenerator->generateUuidString()),
            UserName::create($request->getName()),
            UserEmail::create($request->getEmail()),
            UserPassword::create($request->getPassword())
        );

        // Repositoryに投げて永続化
        $user = $this->userRepository->create($user);

        // レスポンスとして返却する公開情報はResponseクラスで指定
        return new UserCreateResponse(
            $user->getId()->value(),
            $user->getName()->value(),
            $user->getEmail()->value()
        );
    }
}

UseCaseはinterfaceを通じて、後述するControllerから使用されますが、ControllerとUseCase間のデータのやり取りはDTO(Data Transfer Object)で行います。

例えば、上記のソースコードでUseCaseの__invokeメソッドの引き数はUserCreateRequestというDTOクラスです。

この点において、Laravelを普段から使っている人であれば、「フロントエンドからの送信を受信しているFormRequestをそのままUseCaseの引き数にしてしまえばよいのでは?」という疑問を抱くと思います。

ですが、ここでもう一度クリーンアーキテクチャーの同心円を思い出して頂きたい。

この図をみると、Frameworksのレイヤーは同心円上の最も外側に位置します。

LaravelのFormRequestクラスはこのFrameworks層の住人であり、これにUseCaseが依存することは依存方向性の逆流を意味します。

UseCaseが特定のFrameworkなどの技術基盤を前提に作られてしまうことは、「可変性」の低下を招くことになるため、DTOを使うことで「疎結合」を維持したいというモチベーションがここにはあります。

UserCreateResponse(OutputDTO)については、外部公開すべきデータを制御するために用いています。

Entityクラスのメンバ変数はprivateなので、これを公開範囲を指定し、公開可能なデータとしてControllerに返却したいモチベーションがここにはあります。

Controllers

Controllers は入力をアプリケーション(UseCases)が要求する形に変更して伝えるのが役目です。

今回はLaravelを使うので、laravelのControllerがそれに当たります。

フロントエンドから送られてきたリクエストの内容を、UseCaseが求める形に変換し、UseCaseに伝えます。

さらに、httpリクエストがレスポンスと対である兼ね合いで、laravelなどのフレームワークの場合はUIへの変換処理もControllerが担うことになります。

本来これらはPresenterの役割ですが、今回は割愛させて頂きます。

以上を踏まえアーキテクチャーを考察

以上までを踏まえて、Laravelで実際にコードを書いてみます。

全体図

これは今回私なりに考えた、Laravelでクリーンアーキテクチャーに従う場合の全体図になります。

今回のサンプルアプリケーションも、基本的にこの画像のスキームに則って実装します。

薄いピンク色に囲まれたゾーンが、クリーンアーキテクチャーの同心円状の外側に位置するFrameworks層にあたり、基本的な考え方としてはLaravelのEloquentモデルの機能や、FormRequestなどのValidation機能などはこのゾーンに閉じ込めてフル活用します。

そして、色の囲みがないUseCaseやEntityなどの領域は基本的にLaravelやその他の技術基盤に依存させないクリーンな状態を保つようにします。

サンプルアプリの仕様

UserはShopを複数持つことができて、Productを複数出品できる。

サンプルアプリリポジトリはこちら

実際にアプリケーションを構築する

上記の使用を全て上げてしまうと冗長になるので、ここでは、Userの登録機能だけを取り出してサンプルアプリリポジトリの解説をしていきます。

まずはEntityとValueObjectを定義する

UserEntity

<?php


namespace Packages\Domain\Models\User;

/**
 * Class UserEntity
 * Userを表現するEntity
 * @package Packages\Domain\Models\User
 */
class UserEntity
{
    /** @var UserId */
    protected $id;

    /** @var UserName */
    protected $name;

    /** @var UserEmail */
    protected $email;

    /**
     * UserEntity constructor.
     * @param UserId $id
     * @param UserName $name
     * @param UserEmail $email
     * @param UserPassword $password
     */
    public function __construct(UserId $id, UserName $name, UserEmail $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

    /**
     * @return UserId
     */
    public function getId(): UserId
    {
        return $this->id;
    }

    /**
     * @return UserName
     */
    public function getName(): UserName
    {
        return $this->name;
    }

    /**
     * @return UserEmail
     */
    public function getEmail(): UserEmail
    {
        return $this->email;
    }
}

UserId

<?php


namespace Packages\Domain\Models\User;

/**
 * Class UserId
 * Uesrの識別子であるIDを表すValueObject
 * @package Packages\Domain\Models\User
 */
class UserId
{
    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userId
     */
    private function __construct(string $userId)
    {
        $this->_value = $userId;
    }

    public static function create(string $userId): self
    {
        return new self($userId);
    }

    public function value(): string
    {
        return $this->_value;
    }

    /**
     * UserId同士が等しいか審査する
     *
     * @param UserId $otherId
     * @return bool
     */
    public function isEquals(UserId $otherId): bool
    {
        return $this->_value === $otherId->value();
    }
}

UserName

<?php


namespace Packages\Domain\Models\User;

use RuntimeException;

/**
 * Class UserName
 * Userの氏名を表すValueObject
 * @package Packages\Domain\Models\User
 */
class UserName
{
    public const MIN_LNEGTH = 3;
    
    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userName
     */
    private function __construct(string $userName)
    {
        self::validation($userName);

        $this->_value = $userName;
    }

    public static function create(string $userName): self
    {
        return new self($userName);
    }

    public function value(): string
    {
        return $this->_value;
    }

    private static function validation(string $value): void
    {
        if (mb_strlen($value) < self::MIN_LNEGTH) {
            throw new RuntimeException(sprintf('名前の最小文字数は%sです', self::MIN_LNEGTH));
        }
    }
}

ここで注目したいのはvalidationメソッドです。

この様に、ValueObject自身に値審査機能があることで、そもそも不正な値で値オブジェクトを生成させないという建て付けになります。

また、今回のケースの「ユーザーの名前の文字数は3文字以上にしたい」といったルールをDomainObjectであるValueObjectに共通化することで、たとえばルールの変更を行う際に、DomainObjectの変更をするだけで全体に変更を反映することができます。

<?php

class UserName
{
    // ここを変更するだけでプロジェクト全体のUserNameに関するルールが変更される。
    public const MIN_LNEGTH = 20;

UserEmail

<?php


namespace Packages\Domain\Models\User;

/**
 * Class UserEmail
 * UserのEmailアドレスを表すValueObject
 * @package Packages\Domain\Models\User
 */
class UserEmail
{
    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userEmail
     */
    private function __construct(string $userEmail)
    {
        $this->_value = $userEmail;
    }

    public static function create(string $userEmail): self
    {
        return new self($userEmail);
    }

    public function value(): string
    {
        return $this->_value;
    }
}

AuthUserEntity

UserEntityは、登録処理のときはUserPasswordが必須ですが、普段アプリケーション上の挙動を実現するためにはむしろUserPasswordはEntityの中には不要です。

そこで、今回は認証用にUserEntityを継承し拡張したAuthUserEntityを作成します。

<?php


namespace Packages\Domain\Models\User;

/**
 * Class AuthUserEntity
 * 認証関連用のUserEntity
 * @package Packages\Domain\Models\User
 */
class AuthUserEntity extends UserEntity
{
    /** @var UserPassword */
    private $password;

    public function __construct(UserId $id, UserName $name, UserEmail $email, UserPassword $password)
    {
        parent::__construct($id, $name, $email);
        $this->password = $password;
    }

    /**
     * @return UserPassword
     */
    public function getPassword(): UserPassword
    {
        return $this->password;
    }

}

追加点は$passwordというメンバ変数とそのgetterメソッドが追加されたに過ぎません。

UserPassword

<?php


namespace Packages\Domain\Models\User;

// TODO:ここでLaravelのファサードに依存してしまうのはあまり良くないので解決策を考える。
use Illuminate\Support\Facades\Hash;
use RuntimeException;

/**
 * Class UserPassword
 * Userのパスワードを示すValueObject
 * @package Packages\Domain\Models\User
 */
class UserPassword
{
    public const MIN_LENGTH = 8;

    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userPassword
     */
    private function __construct(string $userPassword)
    {
        $this->_value = $userPassword;
    }

    public static function create(string $userPassword): self
    {
        self::validation($userPassword);

        return new self($userPassword);
    }

    public function value(): string
    {
        return $this->_value;
    }

    /**
     * 平文のパスワードをハッシュ化する処理
     *
     * @return string
     */
    public function getHashValue(): string
    {
        return Hash::make($this->_value);
    }

    private static function validation(string $userPassword): void
    {
        if (strlen($userPassword) < self::MIN_LENGTH) {
            throw new RuntimeException(sprintf('パスワードは最低%s文字です。', UserPassword::MIN_LENGTH));
        }
    }
}

ここで注目したいのは、getHashValueという関数です。

ここではDatabaseに保存する際に、平文のパスワードをハッシュ化するための機能をValueObject自身に実装しています。

ただし、コード上のTODOでも記載したとおり、このハッシュ化ロジックにはLaravelのファサード機能を使っています。

DomainObjectであるValueObjectが「Laravel」という特定の技術基盤に依存している形になってしまっているので、このあたりのベストプラクティスはまだ私も考察中ですが、今回はこのままで進めたいと思います。

Entityが出揃ったところでRepositoryを作成

UserRepository

<?php

namespace Packages\Domain\Models\User;

use App\User;

interface UserRepository
{
    public function getById(UserId $userId): UserEntity;

    public function create(AuthUserEntity $userEntity): UserEntity;
}

Repositoryは上記を見てわかるように、ただのインターフェースです。

実際の具体的な処理はこのインターフェースを実装した、具象クラスにて実装します。

UserEloquentRepository

<?php

namespace Packages\Infrastructure\EloquentRepository;

use Packages\Domain\Models\User\UserEntity;
use Packages\Domain\Models\User\AuthUserEntity;
use Packages\Domain\Models\User\UserId;
use Packages\Domain\Models\User\UserRepository;
use Packages\Domain\Models\User\UserEntityFactory;
use App\User;

class UserEloquentRepository implements UserRepository
{
    public function create(AuthUserEntity $userEntity): UserEntity
    {
        $ormUser = User::create([
            'id' => $userEntity->getId()->value(),
            'name' => $userEntity->getName()->value(),
            'email' => $userEntity->getEmail()->value(),
            'password' => $userEntity->getPassword()->getHashValue(),
        ]);

        return UserEntityFactory::createFromORM($ormUser);
    }

    public function getById(UserId $userId): UserEntity
    {
        $ormUser = User::find($userId->value());

        return UserEntityFactory::createFromORM($ormUser);
    }
}

ここで注意したいのは、LaravelのEloquentモデルを直接返さず、戻り値としてUserEntityに載せ替えている点です。

これは、後述するUseCase層にLaravel特有の技術基盤であるEloquentモデルを漏れ出さないようにするためです。

EloquentのことはEloquentRepositoryの中だけで完結しろというわけです。

また、ここで新しい概念である「UserEntityFactory」というものが登場しているのでこの点を補足説明致します。

UserEntityFactory
<?php


namespace Packages\Domain\Models\User;

use App\User;

/**
 * Class UserEntityFactory
 * UserのEloquentモデルからドメインオブジェクトである
 * UserEntityを生成する処理を担うクラス。
 *
 * @package Packages\Domain\Models\User
 */
class UserEntityFactory
{
    public static function createFromORM(User $user): UserEntity
    {
        return new UserEntity(
            UserId::create($user->id),
            UserName::create($user->name),
            UserEmail::create($user->email)
        );
    }
}

中身の処理は上記の様になっており、単純にEloquentModelのUserを自前のDomainObjectであるUserEntityに載せ替えているだけの処理です。

この程度であれば、このFactoryクラスの存在意義は感じにくいかもしれませんが、例えば多数のリレーション関係を持つモデルをEntityの載せ替える処理は、それ自体が複雑なロジックになります。

この「載せ替える」というロジックはそもそもRepositoryの責務そのものとは別問題なので、Factoryというクラスに役割を分割するというわけです

DomainObjectをまとめ上げてUseCaseを作る

UserCreateUseCaseInterface

<?php


namespace Packages\UseCase\User\Create;

interface UserCreateUseCaseInterface
{
    public function __invoke(UserCreateRequest $request): UserCreateResponse;
}

UseCaseもインターフェースで抽象化します。

理由は、バックエンドが完成するまえにでもUseCaseをスタブと切り替えることでフロントエンドの先行開発などができるからです。

ここでもやはり「pluggable」な構成にすることで、柔軟性をもたせることができるというわけです。

UserCreateRequest

<?php


namespace Packages\UseCase\User\Create;


class UserCreateRequest
{
    /** @var string */
    private $name;

    /** @var string */
    private $email;

    /** @var string */
    private $password;

    /**
     * UserGetRequest constructor.
     * @param string $name
     * @param string $email
     * @param string $password
     */
    public function __construct(string $name, string $email, string $password)
    {
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
    }

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

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

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

データの転送用のDTOになります。

LaravelのFormRequetを直接UseCase層に流入させてしまうと、UseCase層がLaravelという特定の技術基盤に侵食されてしまいます。

この事を防ぐためにDTOで連絡を取り合います。

UserCreateResponse

<?php


namespace Packages\UseCase\User\Create;


class UserCreateResponse
{
    /** @var string */
    private $id;

    /** @var string */
    private $name;

    /** @var string */
    private $email;

    /**
     * UserCreateResponse constructor.
     * @param string $id
     * @param string $name
     * @param string $email
     */
    public function __construct(string $id, string $name, string $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

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

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

    /**
     * @return string
     */
    public function getEmail(): string
    {
        return $this->email;
    }
    
    /**
     * 公開可能データを配列で返す処理
     * 
     * @return array
     */
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }
}

こちらもデータ転送用のDTOです。

こちらはUseCaseからControllerへのデータ転送用になりますが、これはEntityのprivateなメンバ変数を、公開範囲を制御しつつControllerへ伝える役割を担っています。

UserCreateInteractor

<?php

namespace Packages\Application\User;

use Packages\Domain\CommonRepository\UuidGeneratorInterface;
use Packages\Domain\Models\User\AuthUserEntity;
use Packages\Domain\Models\User\UserEmail;
use Packages\Domain\Models\User\UserId;
use Packages\Domain\Models\User\UserName;
use Packages\Domain\Models\User\UserPassword;
use Packages\Domain\Models\User\UserRepository;
use Packages\UseCase\User\Create\UserCreateRequest;
use Packages\UseCase\User\Create\UserCreateResponse;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /** @var UserRepository  */
    private $userRepository;

    /** @var UuidGeneratorInterface  */
    private $uuidGenerator;

    /**
     * UserCreateInteractor constructor.
     */
    public function __construct(
        UserRepository $userRepository,
        UuidGeneratorInterface $uuidGenerator
    ) {
        $this->userRepository = $userRepository;
        $this->uuidGenerator = $uuidGenerator;
    }

    public function __invoke(UserCreateRequest $request): UserCreateResponse
    {
        // Userのドメインモデルを生成
        // この時、全ての値の審査も行われる
        $user = new AuthUserEntity(
            UserId::create($this->uuidGenerator->generateUuidString()),
            UserName::create($request->getName()),
            UserEmail::create($request->getEmail()),
            UserPassword::create($request->getPassword())
        );

        // Repositoryに投げて永続化
        $user = $this->userRepository->create($user);

        // レスポンスとして返却する公開情報はResponseクラスで指定
        return new UserCreateResponse(
            $user->getId()->value(),
            $user->getName()->value(),
            $user->getEmail()->value()
        );
    }
}

「UserCreateUseCaseInterface」の具象クラスです。

ここで、いままで作成したEntityやValueObject、RepositoryなどのDomainObject達をコントロールし、アプリケーションとしての振る舞いを実現し、利用者に機能を提供します。

利用者に公開するためにControllerを作る

AuthController

<?php

namespace App\Http\Controllers;

use App\Http\Requests\Auth\AuthLoginRequest;
use App\Http\Requests\Auth\AuthRegisterRequest;
use Packages\UseCase\User\Create\UserCreateRequest;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;
use Packages\UseCase\User\Get\UserGetRequest;
use Packages\UseCase\User\Get\UserGetUseCaseInterface;
use Illuminate\Http\JsonResponse;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * @OA\Post(
     *     path="/api/auth/register",
     *     tags={"User/Auth"},
     *     description="ユーザー新規登録",
     *     @OA\RequestBody(
     *         required=true,
     *         @OA\MediaType(
     *             mediaType="application/x-www-form-urlencoded",
     *             @OA\Schema(
     *                 type="object",
     *                 @OA\Property(
     *                     property="name",
     *                     description="氏名",
     *                     type="string",
     *                     default="Ippei Kamimura"
     *                 ),
     *                 @OA\Property(
     *                     property="email",
     *                     description="メールアドレス",
     *                     type="string",
     *                     default="ippei_kamimura@icloud.com"
     *                 ),
     *                 @OA\Property(
     *                     property="password",
     *                     description="パスワード",
     *                     type="string",
     *                     default="aaaaaa"
     *                 ),
     *                 @OA\Property(
     *                     property="password_confirmation",
     *                     description="パスワード(確認)",
     *                     type="string",
     *                     default="aaaaaa"
     *                 )
     *             )
     *         )
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="認証トークンを返す",
     *     )
     * )
     */
    public function register(
        AuthRegisterRequest $request,
        UserCreateUseCaseInterface $userCreateUseCase
    ): JsonResponse {
        $userCreateRequest = new UserCreateRequest(
            $request->name,
            $request->email,
            $request->password
        );

        $response = $userCreateUseCase($userCreateRequest);

        if (!$token = auth('api')->attempt($request->all())) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        
        return response()->json([
            'user' => $response->toArray(),
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

LaravelのControllerクラスです。

利用者からのRequestを受け取り、UseCaseへそのリクエストを処理できる形に変換して伝えることが役割です。

今回の実装ではPresenterは取り上げていないので、UseCaseからのレスポンスをJsonResponseに変換して利用者へレスポンスを返すような建て付けにしています。

AuthRegisterRequest

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Packages\Domain\Models\User\UserPassword;
use Packages\Domain\Models\User\UserName;

class AuthRegisterRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => [
                'required',
                'string',
                sprintf('min:%s', UserName::MIN_LNEGTH),
                'max:255'
            ],
            'email' => [
                'required',
                'string',
                'email:strict,dns',
                'max:255',
                'unique:users'
            ],
            'password' => [
                'required',
                'string',
                sprintf('min:%s', UserPassword::MIN_LENGTH),
                'confirmed',
            ],
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(
            response()->json([
                'message' => $validator->errors()->toArray(),
            ], 403)
        );
    }
}

LaravelのFormRequestクラスです。

通常の「Illuminate\Http\Request」クラスを継承しており、同時にvalidation機能も提供してくれる便利なクラスなので使わない手はありません。

Controllerはクリーンアーキテクチャーにおける同心円の中で一番外側のFrameworkds層の住人なので、ここでLaravel独自の技術に依存することは問題ではありません。

ここで、「ValueObjectで値の審査をしているからFormRequestでのValidationは不必要では?」という意見もあると思います。

たしかに、この2つは値に対するルールが同一になると思いますが、FormRequestとValueObjectではそれぞれが持つ役割が違います。

ValueObjectは値そのもののビジネスルールであり、FormRequestはそのビジネスルールに従い、Requestを審査するという役割です。

FormRequestはルール以外の者の侵入を拒み、ValueObjectはルール以外の生成を拒みます。

そしてValueObjectが生成を拒むおかげで、Request以外からのルートでEntityが生成される場合でも審査機能を利かすことができます。

この様に、「Laravelの機能を使わない」ではなくドメイン層やアプリケーション層では使わない」というルールに徹して活用すれば、ビジネスルールが特定の技術基盤に依存してしまうことを避けられます。

まとめ

ここまで、クリーンアーキテクチャーをLaravelで活用する方法を自分なりに研究してきましたが、簡潔に言うならば「クリーンアーキテクチャーとは、ビジネスルールの在り処を一箇所に集め、技術基盤との堺にインターフェースを挟むことでビジネスルールに対して"pluggable(脱着可能)"にする事で、"可変性"を保ちながらプロダクトを成長させるための指針」と言えると思います。

特にインターフェースの文脈で私が思うことは、「抽象に依存」という言葉が示すとおり我々開発者にとって物事を抽象化することが最も重要な仕事と言っても過言ではありません。

なぜならば、頭の中で抽象化出来ていない物事のインターフェースを作ることは出来ないからです。

抽象化出来ていないと、UI層で仕様をもみながら行きあたりばったりでコードを書くことになり、結果として「賢いUI」になって行き、可変性の低いソースコードになって行く。

だから、いきなりエディターに向かうのではなく、ドメイン従事者に多くのヒアリングを行い、簡単にでも良いので全体構成を書き出すなどして整理することから始めた方が良い。

開発者が関与するプロダクトの抽象度を上げ、質の高いインターフェースを作り上げることができるかどうかは、その事業ドメインについてどれだけ知り尽くしているかに依存します。

そして、そのインターフェースの抽象度の質によって、そのプロダクトが将来に向けて拡張的で居られるかどうかが決まってしまう。

アーキテクチャーにゴールや正解はなく、常に思考を練り、よりスマートに、よりシンプルにバージョンアップしていかなくてはならないものです。

思考を凝らし、難しいことを単純化するツールとして、クリーンアーキテクチャーの考え方は活用の価値があると思います。

プロダクトチーム立ち上げ時にドラッカー風エクササイズをやってみた

はじめに

こんにちは、株式会社ROXXのエンジニアの佐藤(@r_sato1201)です。
4月からROXXRecordsという新規事業を創る事業部に異動になり、RECJobという新規プロダクトの開発に携わっています。

今回は、RECJobのプロダクトチームのキックオフで行ったドラッカー風エクササイズについて書きたいと思います。

ドラッカー風エクササイズとは

ドラッカー風エクササイズとは、アジャイル開発について解説をしている「アジャイルサムライ―達人開発者への道―」にて登場するチームビルディングの手法です。
4つの質問にチーム全員が答え共有することで、各チームメンバーの価値観を理解や期待感のすり合わせを行うことができます。

目的

チームメンバーの価値観や得意なこと、不得意なことなどを共有し、チームメンバー間の相互理解を深めることが目的です。 チームメンバーのことを知り、お互いの背中を預け合うことでチーム一丸となって目標に向かうことができます。

ドラッカー風エクササイズは発足したばかりのチームや新しいメンバーがジョインした時など、チームメンバーがお互いのことをよく知らない状態で行うと効果が出ます。
私たちのチームも発足したばかりで、お互いをよく知らなかったのでタイミングとしては最適だったと思います。

やりかた

流れ

ドラッカー風エクササイズは以下の流れで行います。

  1. ドラッカー風エクササイズの目的や背景の説明
  2. 五つの質問を記入
  3. 1人ずつ記入した回答の発表と回答に対する確認や質問

1.ドラッカー風エクササイズの目的や背景の説明

ドラッカー風エクササイズの目的や背景を説明を行います。お互いのことを理解するために行うこと、チームの成長のために行うことなどを説明します。

2.五つの質問を記入

アジャイルサムライでは以下の4つの質問に答えるよう紹介されています。

  • 自分は何が得意なのか?
  • 自分はどうやって貢献するつもりか?
  • 自分が大切に思う価値は何か?
  • チームメンバーは自分にどんな成果を期待してると思うか?

私たちはお互いのことをより知ってもらう、理解してもらうことに重きを置くために 質問を1つ追加し、合計5つの質問にして行いました。

  • 自分がこういうのはいやだと思うのはなにか

5つの質問に対して合計10分で、質問毎に最大3つまで記入してもらいました。 あまり時間をかけても仕方がないのと、一番先に思いついたものがその人にとって重要度が高いからです。

3. 一人ずつ記入した回答の共有と回答に対する確認や質問

自分が記入した回答を1人ずつ共有します。
記入した内容をただ読み上げるだけでなく詳細が分かるエピソードや書いた理由を伝えるとより良いと思います。

共有が終わったら確認・質問タイムに入ります。
共有してくれた回答の理解を深めるためにもどんどん質問していきましょう。
ここで重要なことは、質問をする目的は「理解を深めるため」だということです。
理解を深めることが目的なので、この時間で否定や批判を行うことはやめましょう。お互いのことを知るために行っているので否定や批判は目的に沿っていません。

まとめ

今回はドラッカー風エクササイズのことを紹介させていただきました。 プロダクトチームというくくりで、エンジニアだけでなくビジネスサイドのメンバーも含めて行ったのは初めてでしたが、おこなって非常によかったと思います。
プロダクトビジョンを実現するために部署の垣根をなくし、お互いの背中を預けながら進めるために良いスタートを切ることができました。

また、チーム発足の初期段階だけでなくメンバーが増えたり、ある程度時間が経った後に行うのも効果的だと思います。 新しいメンバーのことを知れますし、既存のメンバーに関しても、時間が経ったことでその時メンバーが考えていることや価値観をすり合わせすることができるからです。

最後に

現在株式会社ROXXは一緒にはたらく仲間を募集中です。
現在私が所属しているチームは発足したばかりでメンバーをまだ募集していないので、 以前所属していたagent bank開発チームの求人を記載しておきます。

herp.careers

herp.careers

herp.careers

ゆる〜くデイリーふりかえりをはじめてみた

この記事は個人ブログと同じ内容です

ゆる〜くデイリーふりかえりをはじめてみた

はじめに

こんにちは、 back check 開発チームでスクラムマスターをしているぐっきーこと山口です。 今回は、実験的に始めてから1スプリントが経過したデイリーふりかえりについて学びを得たので記事に書きます。

この記事で伝えたいこと

  • デイリーのふりかえりおすすめです!
  • 24時間単位でふりかえることで、その日の学びが定着しやすくなるよ
  • チームでやると24時間で得た学びを共有できるメリットもついてくる

デイリーふりかえりとは?

デイリーふりかえりとは、その名の通り24時間で起きた出来事にスコープを絞ったふりかえりです。

スクラム開発では、スプリントの最後に完了したスプリントの期間を対象にレトロスペクティブ(ふりかえり)を行いますが、そのスコープを24時間に絞ったふりかえりと考えるとイメージがしやすいかと思います。

なぜはじめたのか

弊チームでは、スクラムのプラクティスに沿って2週間でスプリントを回しています。 毎回スプリントの最後にレトロスペクティブを行なっていますが、2週間を対象にふりかえってもスプリント中にどんなことがあったか覚えていないというペインを個人的に抱えていました。 そこで、デイリーでふりかえることでスプリント中にあったことを覚えておこう。あわよくばその場で気づいたことを検査→適応しちゃおう。(シンプルじゃなくなった。笑)ということを目的に始めてみました。

始めるといっても弊チームは、メンバーが10人以上おり、皆が慣れていない状態でチーム全体にデイリーふりかえりを提案、実験するのはコストが高くつきます。 そこで、まずは定刻にボイチャに参加しているメンバーに声をかけて興味を持ってくれた人たちの中で実践してみました。

デイリーふりかえりの進め方

デイリーふりかえりの進め方に決まりはありません。

というよりデイリーふりかえり自体に公式の定義はなく、呼称も私が勝手に呼んでいるだけです。かくいう私も、お隣の agent bank 開発チームや、アジャイルコミュニテイの方々がチームで導入されているというお話しを聞き、このプラクティスを知ったばかりです。

そんな中で私が試した方法としては、 YOW のフレームワークにそって Y(やったこと)、 O (おきたこと)、 W (わかったこと)を書き出して、参加メンバーと共有していく方法で始めてみました。 最初のうちは共有までで終えていましたが、共有後にだからなんだっけ?な空気を察知したので、最近は解決した方がよさそうなことがあるかを確認する時間を設けてからふりかえりを終わるようにしてみました。(検査の要素を意識するようにした。)

以下、進め方の概要

  • YOW を書く:10分
  • 共有:15分
  • 解決した方がよさそうなことがあるか確認する:5分

YOW の代わりに有名なふりかえりの手法である + / Δ (プラス/デルタ) や、Fun Done Learn などをとりいれてもいいと思います。

続いて、実際に試して感じたことを書いていきます。

失敗したこと

そもそもですが、他のメンバーを誘う段階でなぜ?を共有せずにプラクティスだけを提案して実践を始めてしまいました。 結果として、日によってメンバーの参加率がばらばらになってしまいました。

あわせて私以外のメンバーが2週間の出来事を覚えていられず、思い出す必要性を感じているというペインを抱えているかもわからない状態でした。 この辺は実施始めの段階で言語化していなかったこともあり、継続にあたって改めて共有する必要性があると反省しています。 その結果、目的であった2週間のふりかえりでも参加してくれたメンバーにデイリーでふりかえった内容を活かしてもらうことができませんでした。

つまり、ゆる〜く始めた結果チームとして明確に成果を感じることはできませんでした。

よかったこと

逆にやってみてよかったことも多数ありました。

24時間単位でできごとを見直すことで、その日の学び、起きたことを言語化して認識できるようになりました。 また、午前に行うデイリースクラムとは別に、夕方にデイリーふりかえりで改善が必要なことがないか検査する機会を設けたことで、チームの障害を検知するきっかけが増えました。 さらに、冒頭でも書きましたがチームで学びを共有することで、チームとしての暗黙知が貯まる機会が増えました。

これらを踏まえて、個人的にはお試しでデイリーふりかえりを実践してみる段階で十分成果を感じることができました。

まとめ

結論、デイリーふりかえりをやってみた結果、チームに提案する価値のありそうなプラクティスだということがわかりました。 今後、目的などを明確にした上で、再度有志のメンバーで試してみてチームとして効果を実感できたら導入を提案したいと思います。

みなさんもくれぐれも目的を言語化した上で、みなさんの所属するチームでデイリーふりかえりを試してみてはいかがでしょうか?

最後に、現在 back check 開発チームは一緒にはたらく仲間を募集中です。 ご興味をもっていただけたましたら、 DM 、もしくは下記の応募フォームにてお気軽にカジュアル面談をご依頼ください。

Google アナリティクス 4 プロパティを作成し、GA4 での計測を開始する

この記事は個人ブログと同じ内容です

www.ritolab.com


これまで UA(ユニバーサルアナリティクス)で行っていた計測に加え、GA4(Google アナリティクス 4)プロパティの設定を行い、GA4 にて計測を開始するまでを行っていきます。

はじめに

略語が多めに登場してややこしいので最初に整理だけしておきます。本記事で使う略語はそれぞれ以下に対応しています。

  • GA:Google AnalyticsGoogle アナリティクスそのものを指しています)
  • UA:ユニバーサルアナリティクス プロパティ(終了となる従来の計測方式を指しています)
  • GA4:Google アナリティクス 4 プロパティ(移行する新しい計測方式を指しています)
  • GTM:Google タグ マネージャー

GA4 プロパティの作成

まずは GA の管理画面に遷移します。

プロパティにある、「GA4 設定アシスタント」を押下します。

設定アシスタント画面が表示されたら、「新しい Google アナリティクス 4 プロパティを作成する」にある「はじめに」ボタンを押下します。

モーダルが表示されるので、内容を確認して「プロパティを作成」ボタンを押下します。

これで接続済みのプロパティとして GA4 のプロパティが表示されるようになりました。

「GA4 プロパティを確認」ボタン押下するとアシスタントの設定画面に遷移することができます。

ここからそれぞれの細かい設定を必要に応じて行っていきますが、ひとまず GA4 のプロパティ作成はこれで完了になります。

測定 ID の確認

GA4 プロパティが作成できたので、タグマネージャーに設定を行っていくのですが、そこで必要になる「測定 ID」をメモしておきます。

管理画面のプロパティより、「データストリーム」を表示し、先程作成した GA4 プロパティの詳細を表示させます。

詳細情報の中に「測定 ID」が表示されているのでメモしておきます。

GTM 設定

GA4 プロパティでの計測を GTM(Google タグ マネージャー)で設定していきます。

まずは GTM の画面にアクセスします。

タグ画面に遷移し、「新規」ボタンを押下します。

タグの設定より、「タグタイプを選択して設定を開始」を押下します。

タグタイプを選択します。「Googleアナリティクス:GA4設定」を選択します。

タグの設定画面へ遷移するので、ここで事前にメモした「測定 ID」を入力します。

次に、トリガーより、「トリガーを選択してこのタグを配信」を押下します。

トリガーの選択が表示されるので、「All Pages」を選択します。

タグの設定、そしてトリガーの設定内容を確認して、画面右上の「保存」ボタンを押下します。

これでタグの設定は完了です。

プレビューで確認する

タグの設定ができたら、公開する前にプレビューで動作確認を行っておきます。

画面右上のプレビューボタンを押下し、プレビューを開始します。

タグアシスタントが起動するので、確認する URL を入力し、connect ボタンを押下します。

ポップアップが開いて入力した URL のページが表示されますが、この時に画面右下にタグアシスタントのウィンドウも表示されます。

(ブラウザに Google Chrome を使っている場合)この時に、元の画面を見て「For an improved debugging experience, install the Tag Assistant Companion browser extension」と表示されていたら、install のリンクから Chrome 拡張を入れます。

chrome.google.com

あとはサイトを回遊して、タグが動作しているかを確認します。

タグアシスタント側の Tags Fired に GA4 が表示され、GA4 のタグが動作していることを確認できました。

動作確認ができたので、あとは公開ボタンを押下して変更を適用すれば設定は完了です。

GA4 の画面

GA4 タグを公開できたので、GA4 プロパティの画面を見てみます。

まだタグを公開したてなので何もデータが溜まっていないことがわかりますが、タグは動作しているので、リアルタイムの部分だけは確認できる状態であることが確認できます。

また、左のメニューも UA と比べて変わっていることが確認できます。

別の画面を見てみてます。

ご覧の通り、UA のデータは引き継がれないので現状では空っぽです。

ちなみに UA 側の計測も生きているので、GA4 と UA の画面を両方開いてみると、両方計測が行われていることも確認できます。

まとめ

GA4 での計測を開始するところまでを行いました。

今回は、既に GA で計測を行っている(UA タグ設置済み)の状況で、GA4 のタグでの計測を行なう。という状況で進めましたが、例えばタグマネージャーを使っていない場合などもあると思います。

Google が提供しているドキュメントが充実しているので、基本的にはそちらを探せば、GTM 以外(サイトに GA タグを直接設置している場合)でのタグの設置方法もあるので、探してみてください。

support.google.com

support.google.com


現在 back check 開発チームでは一緒に働く仲間を募集中です。

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

チームのメンバーと開いた勉強会がよかったのでYOWでふりかえる

この記事は個人ブログと同じ内容です

チームのメンバーと開いた勉強会がよかったのでYOWでふりかえる

みなさん、こんにちは。ぐっきーです。

最近チーム内で気になる技術などを勉強するのに、不定期に業務後に集まって勉強会をやるようになりました。

発端としては、「GraphQL に興味があって、 AWS の AppSync を使って簡単に触れるみたいなんで試してみたいんですよね〜」というメンバーの声があったので、「面白そうだから他の人にも声かけて仕事終わりに試してみようよ!」というやりとりからでした。

メンバーのslack投稿
メンバーのslack投稿

今回は、勉強会をやった結果、自分の中でどんな気づきがあったかを YOW の手法でふりかえってみました。

YOW

YOW とは 、「行動」と「その結果」を分けて考えやすい手法で、事実関係が明らかにしやすいことが特徴です。

Y: やったこと

O: 起きたこと

W: 分かったこと

に沿って順に書いていき、事実を元にどんなことが分かったかをふりかえります。

参照:

ふりかえり

さっそくふりかえってみました。

今回は、 Figjam を使い、30分間を測ってその中ででてきたことを付箋に書き出していきました。

以下、ふりかえりのアウトプット:

YOWのアウトプット
YOWのアウトプット

慣れてくると変わるかもですが、初挑戦ということもあり YOW を出すのに時間がかかってしまいました。

もしチームでやってみるときは事前に少人数でトライアルをやってみるなり、時間は多めに確保するなりしてみるといいのかもしれません。

また、ふりかえりのスコープを過去2回開催した勉強会全部を含めてふりかえりました。

その結果、2つめの勉強会に頭を切り替えて思い出しをしている間に時間がどんどん過ぎてしまったので、スコープはなるべく小さくふりかえることの大切さを再認識しました。

まとめ

YOW でのふりかえりは初めてだったのですが、Y.やったこと、O.起きたこと、W.わかったことをひとまとめにして書き出すので、因果関係の仮説が言語化できたのがよかったです。

最後に YOW をやってみたことに対して YOW を出してみました。

Y: わかったことから先に出したものがあった

O: YOW は揃えられた

W: 詰まったら順番は気にしないでいいのかも

YOW の順に書き出すことに詰まった時は、どんなことが分かったんだっけ?→なにが起きたからだっけ?→あれをしたからか!という風に順不同で考えても価値はあると思うので、とにかく小さなことでも書き出してみるのが大事かなと思いました。

今後機会をみて、チームでも試してみたいと思います。

最後に

勉強会定着してきたら業務内でできるように調整したいな!

ついでに宣伝です。 現在 back check 開発チームは一緒にはたらく仲間を募集中です。 カジュアル面談も実施してますので、お気軽にお申し込みください!

herp.careers herp.careers herp.careers herp.careers herp.careers herp.careers

また、弊社 CTO 兼 back check の PM を担当している松本の Meety もご用意しておりますので、チェックしてみてね!

meety.net

ユニバーサルアナリティクス(UA)から Google アナリティクス 4(GA4)に移行する

この記事は個人ブログと同じ内容です

www.ritolab.com


Google Analytics(GA)において、2023 年 7 月 1 日(GA360 は 2023 年 10 月 1 日)にユニバーサルアナリティクス プロパティ(UA)での計測が停止するとのことで、GA4 への移行を行うにあたり、新しいプロパティである Google アナリティクス 4 プロパティ(GA4)への移行についてまとめました。

はじめに

略語が多めに登場してややこしいので最初に整理だけしておきます。本記事で使う略語はそれぞれ以下に対応しています。

  • GAGoogle AnalyticsGoogle アナリティクスそのものを指しています)
  • UA:ユニバーサルアナリティクス プロパティ(終了となる従来の計測方式を指しています)
  • GA4Google アナリティクス 4 プロパティ(移行する新しい計測方式を指しています)

なぜ GA4 に移行しないといけないのか

support.google.com

発表の通り、従来の UA では、2023 年 7 月 1 日を以て計測が行われなくなります。つまり、GA の UA 画面を見ても何もデータが入ってこなくなります。

計測停止後も GA 上の UA 画面は見られるみたいですが、それも 6 ヶ月後くらいに見られなくなります。

Google AnalyticsUA を終了し GA4 へ移行する背景

UA は web サイトの計測を想定しています。

一方で昨今では、ネイティブアプリ(スマホアプリ)も当たり前の時代になり、クロスデバイスでの計測やネイティブアプリ計測など、web アプリケーションに限らずこういったアプリの計測もスムーズに行えるように最適化された GA4 が発表されました。

UA と GA4 で変わること

マーケター的には「画面 UI が変わる」という部分、エンジニア的には「計測方法が変わる(セッション(ヒット)単位からイベント単位へ)」というのが大きく変わることと言えそうです。

レポートとか作成して GA の画面を眺めるのが日課になっている人には新しい UI への慣れが課題になりそうですが、エンジニア的にはイベントベースの計測となったことで、実はシンプルになったんじゃないか感はあったりします。(やれイベントだ PV だっていうのが一本化された、という意味で)

ただし、GA4 になると API 関連も変更あるはずので(スキーマが異なるため)、Reporting API を使っていたりする場合はそこの対応は必要になりそうです。

いつ GA4 に移行するのが良いのか

できるだけ早く移行するのが最良です。なぜかというとUA の計測データは GA4 には引き継がれない」からです。

どういうことかというと、UA と GA4 は GoogleAnalytics で見る画面が別になります。GA4 で計測を開始した場合、GA4 の画面で見られる数値は GA4 で計測したものだけです。

こんな感じで、GA4 の画面に UA で計測していた時のデータは入ってこないため、事実上これまでの UA での計測データとはさよならしなければなりません。

つまり、過去データがどれだけの範囲必要かによって前もって GA4 での計測を始めておく必要があるということです。

UA と GA4 は同時に利用できる

UA から GA4 に移行すると、それまで見れていた指標がすべて見れなくなり、完全にゼロの状態からスタートするのか?というのが心配になりますが、UA と GA4 はサポート終了までは同時に利用できます。

さらに前述の通り GA 上の画面としても UA と GA4 は別になるので、ひとまず GA4 での計測も開始しておき、2023 年 7 月 1 日 までは移行期間として UA の画面と GA4 の画面を両方見つつ、段々と GA4 の UI に慣れていく。ということも可能です。

さらに、GA4 はこれで完成ではなく、UA サポート終了までにも機能追加がおそらく行われていくのではないかと思われるので、そういった意味でも、サポート終了までは UA と GA4 を併用していくという使い方が現時点では良いと思います。(UA にあったのに GA4 ではあれが見れないこれが見れないがもしあった場合に、それがもしかしたら今後見れるようになるかもしれないという意味で。どうなるかわかりませんがここは現時点で諦めるというよりも、継続して情報収集していく姿勢の方が正解だと思います。)

UA のデータは活かせないのか

せっかくこれまで UA で溜めてきたデータがさらっと無くなるのに納得がいかない人もいると思います。

UA で溜めたデータはエクスポートして CSVスプレッドシート等で落とすか、Reporting API で過去のデータを引っ張ってきてどこかに保存することで救出はできそうです。

support.google.com

developers.google.com

UA と GA4 ではスキーマが異なるので、UA のデータを取り出せても両者を統合して分析するとしたら工夫が必要になりますが、ひとまず、UA のデータを取り出すこと自体はできそうです。

まとめ

ユニバーサルアナリティクスのサポート終了アナウンスが出たのは 2022 年 3 月 16 日

blog.google

本記事執筆が同年 4 月 23 日、そしてサポート終了が 2023 年 7 月 1 日、ということで、今から切り替えても GA4 に 1 年以上のデータは溜められそうです。

結局のところ何をしても移行待ったなしのため、早めに対応するのが最良であることには変わりありません。

次の記事では、実際に移行作業を行っていきたいと思います。


現在 back check 開発チームでは一緒に働く仲間を募集中です!!

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

PHPerKaigi 2022 やっぱオフライン楽しいね

この記事は個人ブログの転機です

PHPerKaigi 2022 やっぱオフライン楽しいね - メガネの日記

はじめに

今年も無事にPHPerKaigi 2022 開催できたこと、本当にありがとうございます!
なんと今年はオフラインと、オンラインのハイブリット開催でした!

今回も PHPerKaigi 2021 に引き続き、コアスタッフとして参加させていただきました

f:id:akki_megane:20220413144423j:plain

オフライン楽しいですね

そしてオフラインはやっぱり楽しいですね!

久しぶりに合った方と近況を雑談したり、
初めて参加くれている方とも話せてとても楽しいものです

ベテラン勢がいつもの感じで、フリースペースで雑談してたり、
みんな血眼になってトークンを探しているを見ると、いつもの感じだなーとほっとしました

↓PHPerの刻印押してドヤって自分です f:id:akki_megane:20220413162143p:plain

アンカンファレンス 今年も無限LTをしました

f:id:akki_megane:20220413164820j:plain

実は2019年から、毎年恒例で無限LTという狂気のLT回を実施しているんですが、
2022年の今年も、現地でアンカンファレンスを実施することができました!

開始当初は 参加表明してくれたのは5人だったのですが、 LTをしているうちに飛び入りで参加したいと手を上げてくれるひとが増え、最終的には10名の方がLTをしてくれました

無限LTにテーマの縛りありません、自分は最近発表された、Lambda Function URLs の話しを当日作って話ました 他にも、notion、kotlin、Dockerの壊し方、高専、などなど今年もバラエティー豊かでとても面白かったです!

LTしてくれたみなさん、LTを聞いてくれたみなさんありがとうございました!
ぜひ来年もやりたいので、みなさんお待ちしております

無限LTの説明
speakerdeck.com

感想

やっぱオフラインは最高でした、設営等の会場準備は大変でしたが、
久しぶりの再開や、新しい出会いなどオンラインならではの楽しさをたくさん味わえました

来年あるならぜひ、懇親会でお酒を飲みながら話したいなー

最後に宣伝

現在 back check 開発チームは一緒にはたらく仲間を募集中です!!

herp.careers herp.careers herp.careers herp.careers herp.careers herp.careers