Cloud Vision API を使って画像のラベル検出と OCR (光学文字認識)を行なう

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

www.ritolab.com


GCP の Cloud Vision API を使って画像のラベル検出と OCR(光学式文字認識)を行なってみます。

Cloud Vision API

cloud.google.com

画像ラベリング・顔検出・光学式文字認識・不適切なコンテンツへのタグ付けなど、画像検出機能を揃えたもの。

画像を理解し、画像から情報を引き出すことができる。

画像認識についてトレーニング済みのモデルを利用できるのが大きな強み。

事前準備

Vision API を使えるようにするには事前に以下の作業が必要です。

  • プロジェクトの作成
  • Cloud Vision API の有効化
  • 支払い情報の登録
    • 新規なら無料トライアル使える
  • サービスアカウントの設定
    • キーの作成
      • json ファイルが払い出されるのでダウンロード。これを用いて API へリクエストすることになります。

GCP 慣れてないとわりと面倒ですが、公式ドキュメントに一通りの手順が解説されているのでそちらを見てみるとわかりやすいと思います。

cloud.google.com

環境作成

Cloud Vision API を使う環境を作っていきます。

今回は Cloud SDK の Docker イメージを使ってコンテナ環境を作ります。

cloud.google.com

そしてその中に Vision クライアントライブラリをインストールして使っていきます。

cloud.google.com

ちなみに分析の実装は python で行ないます。

Dockerfile

FROM google/cloud-sdk:latest

# python コマンドに python3 へのリンクを設定(必須ではない)
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 1

# vision クライアント ライブラリのインストール
RUN python -m pip install --upgrade google-cloud-vision

docker-compose.yml

version: '3.8'
services:
  google_cloud_sdk:
    build: .
    container_name: google_cloud_sdk
    working_dir: /root/src
    tty: true
    volumes:
      - ./src:/root/src
    environment:
      GOOGLE_APPLICATION_CREDENTIALS: /root/src/key.json

環境変数 GOOGLE_APPLICATION_CREDENTIALS に設定しているのは、事前準備でサービスアカウント設定のところでダウンロードしたキーの json ファイルへのパスです。

2 つのファイルを作成後、docker compose build を行い、docker compose up で環境の出来上がりです。

ディレクトリ構成は以下になっています

.
├── Dockerfile
├── docker-compose.yml
└── src/
    ├── resources/ <- 画像ファイルを設置
    ├── key.json
    └── script.py

src ディレクトリにスクリプトと画像を入れていきます

ラベル検出

まずはラベル検出を行ってみます。

ラベル検出は、「画像には何が写っているか」を検出するものです。

cloud.google.com

動物の識別

今回は実験として、以下の画像を用意してみました。

上からそれぞれ 犬・猫・馬・にわとり・アルパカ です。これらのラベル検出を行ってみます。

つまり、これらの画像 1 つ 1 つに対して、「犬」や「猫」といったラベルが検出できればこの実験は成功です。

実装します。

label_detection.py

import io
import os

from google.cloud import vision

def label_detection(image):
    # ラベル検出
    response = client.label_detection(image=image)
    labels = response.label_annotations

    print('Labels:')
    for label in labels:
        print(label.description)

image_list = [
    {
        'type': '犬',
        'file_name': 'dog.jpg'
    },
    {
        'type': '猫',
        'file_name': 'cat.jpg'
    },
    {
        'type': '馬',
        'file_name': 'horse.jpg'
    },
    {
        'type': 'にわとり',
        'file_name': 'chicken.jpg'
    },
    {
        'type': 'アルパカ',
        'file_name': 'alpaca.jpg'
    },
]

client = vision.ImageAnnotatorClient()

for im in image_list:
    print('検証タイプ:' + im['type'])

    file_name = os.path.abspath('resources/animals/' + im['file_name'])

    with io.open(file_name, 'rb') as image_file:
        content = image_file.read()

    image = vision.Image(content=content)

    label_detection(image)

画像を読み込んで Cloud Vision API を通じて分析、ラベル検出を行い出力しています。

結果は以下になりました。(左からスコアの高い順)

検証画像:犬

Glasses, Dog, Dog breed, Carnivore, Orange, Companion dog, Fawn, Toy dog, Snout, Sporting Group
label_annotations {
  mid: "/m/0bt9lr"
  description: "Dog"
  score: 0.9633671045303345
  topicality: 0.9633671045303345
}

スコアも 0.96 とほぼ犬と判断して良い結果になっています。

Glasses のスコアの方が若干高いのですが、これはおそらく犬が入っている青いやつのことを判定しているのでしょうか。

検証画像:猫

Cat, Carnivore, Whiskers, Felidae, Ear, Small to medium-sized cats, Snout, Close-up, Fur, Domestic short-haired cat
label_annotations {
  mid: "/m/01yrx"
  description: "Cat"
  score: 0.9540761709213257
  topicality: 0.9540761709213257
}

検証画像:馬

Nose, Horse, Eye, Plant, Working animal, Stable, Liver, Sorrel, Eyelash, Horse tack
label_annotations {
  mid: "/m/03k3r"
  description: "Horse"
  score: 0.9794358611106873
  topicality: 0.9794358611106873
}

検証画像:にわとり

Phasianidae, Beak, Comb, Chicken, Neck, Galliformes, Bird, Feather, Poultry, Livestock
label_annotations {
  mid: "/m/09b5t"
  description: "Chicken"
  score: 0.8774992227554321
  topicality: 0.8774992227554321
}

検証画像:アルパカ

Hair, Llama, Eye, Alpaca, Camelid, Sheep, Fawn, Terrestrial animal, Wood, Snout
label_annotations {
  mid: "/m/0pcr"
  description: "Alpaca"
  score: 0.931623101234436
  topicality: 0.931623101234436
}

画像に対して複数の要素があればそれだけ検出するので複数のラベルが返りますが、きちんと動物を認識してくれていますね。

アルパカだけ、ラマの方がスコアが高かったので結果がしっくりこなかったですが、ラベル検出はこれらを使って何をどう判断するかっていうのが結構難しいなと思いました。 (そこに在る要素が全て出てくる・近似しているものも出てくるので、この結果を使って何がしたいのかは明確にしないと上手に使いこなせなさそう)

鮎と鮎の塩焼き

続いてこんな画像を用意しました。

鮎と、鮎の塩焼きです。

おそらく「鮎」っていう検出はされないんだろう(鮎って世界で広く認知があるのか知らない)とは思いますが、シンプルに生魚と焼き魚ってどう区別されるのかという、個人の興味です。

結果はこうなりました。(上からスコアが高い順)

検証画像:鮎

  • Vertebrate(脊椎動物
  • Fin(フィン)
  • Fish(魚)
  • Marine biology(海洋生物学)
  • Fish products(魚製品)
  • Ray-finned fish(条鰭類)
  • Tail(しっぽ)
  • Feeder fish(フィーダーフィッシュ)
  • Cyprinidae(コイ科)
  • Oily fish(脂ののった魚)

検証画像:鮎の塩焼き

  • Food(食べ物)
  • Ingredient(成分・食材)
  • Seafood(シーフード)
  • Fish(魚)
  • Recipe(レシピ)
  • Fish supply(魚の供給)
  • Fish products(魚製品)
  • Tail(しっぽ)
  • Ray-finned fish(条鰭類)
  • Cuisine(料理)

いまいちよくわからない感じになりましたが、「生物としての魚」と「調理済みの魚」の区別はなんとかつきそうです。

さきほどの動物の例と同じように、ラベル検出は一定の目的を持った上で行わないと、こういうごちゃった結果になったときにそこから何を洞察したいんだっけっていうことになりそうです。

ということで、それではもう少し目的を持った上でのラベル検出を行ってみたいと思います。

それは身分証明書であるか

「それは身分証明書たりうる」の定義って、結構自治体や何らかのサービスによっても違うかもしれないし、それ以前に日本だけの証明書もあると思うので、どこまでそのオブジェクトが「身分証明書」というラベルで検出されるか。を見てみたいと思います。

ということで、日本国内において、日常では身分証明書として十分に通用する以下の 4 つを撮影し、その画像を解析にかけてみます。

  • 運転免許証
  • マイナンバーカード
  • 健康保険証
  • パスポート

結果は以下になりました。(左からスコアが高い順)

検証画像:運転免許証

Product, Identity document, Font, Rectangle, Material property, Suit, Ticket, Blazer, License, Eyelash
label_annotations {
  mid: "/m/01_v7j"
  description: "Identity document"
  score: 0.8999442458152771
  topicality: 0.8999442458152771
}

検証画像:マイナンバーカード

Identity document, Rectangle, Font, Material property, Ticket, Paper, License, Paper product, Electric blue, Number
label_annotations {
  mid: "/m/01_v7j"
  description: "Identity document"
  score: 0.8900585770606995
  topicality: 0.8900585770606995
}

検証画像:健康保険証

Font, Ticket, Rectangle, Number, Screenshot

スコア 0.75 で「もしかしたらチケットかもね」くらいの判定しかされませんでした。

検証画像:パスポート

Identity document, Font, Line, Material property, Eyelash, Ticket, Signature, Paper, License, Paper product
label_annotations {
  mid: "/m/01_v7j"
  description: "Identity document"
  score: 0.9106850028038025
  topicality: 0.9106850028038025
}

「運転免許証」「マイナンバーカード」「パスポート」に関しては「Identity document(身分証明書)」というラベル検出が行われました。

一方で「健康保険証」は身分証明書であるという判断はされなかったようです。この中では唯一顔写真が無かったのでそこも要因なのでしょうか。

Cloud Vision API の精度の良さは認めつつも、独自の慣習や文化を加味したいなら、最終的には自分でモデルを作り上げていく必要があるのかもしれません。

OCR光学文字認識

OCR(Optical Character Recognition, 光学文字認識)は、画像のテキスト情報を検出し抽出する技術です。

いわゆる標識や看板などのプリントされた文字だったり、手書きの文字だとかのテキスト情報を画像から検出して抽出します。

cloud.google.com

プリントされたテキストの検出

まずは活字印刷された看板などがある写真を用意してテキストを検出してみます。

用意したのはこちらの写真です。

岡山県にある日本三名園の一つ「後楽園」に設置されている「池田綱政」という人の解説板です。

長々とした解説、微妙に難しい漢字、そして日本語だけでなく英語の解説もあり、どこまで検出してくれるのでしょうか。

では実装です。

text_detection.py

import io
import os

from google.cloud import vision

def text_detection(image):
    response =  client.document_text_detection(image=image, image_context={'language_hints': ['ja']})

    print(response.full_text_annotation.text)

image_list = [
    {
        'type': '岡山後楽園',
        'file_name': '岡山後楽園.jpg'
    },
]

client = vision.ImageAnnotatorClient()

for im in image_list:
    print('検証画像:' + im['type'])

    # The name of the image file to annotate
    file_name = os.path.abspath('resources/' + im['file_name'])

    # 画像をメモリにロードします
    with io.open(file_name, 'rb') as image_file:
        content = image_file.read()

    image = vision.Image(content=content)

    text_detection(image)

そしてこちらが実行結果です。検出したテキスト情報は以下でした。

検証画像:岡山後楽園
岡山後楽園 歴史ものがたり The history of Okayama Korakuen
いけ
だ
つなまさ
池田綱政が築いた、 やすらぎの庭園「岡山後楽園」 Okayama Korakuen : a leisure garden built for Lord Ikeda Tsunamasa
つなまさ
ちゃくし
池田綱政 Ikeda Tsunamasa (1638-1714)
岡山後楽園を築いた岡山藩二代目藩主池田綱政は、藩祖池田光政の嫡子と
して江戸で誕生。 新田開発や閑谷学校の完成など、父から受け継いだ岡山藩
の基盤を固めた。 また、 能や和歌、 書画に巧みで、 家臣からは 「英断の君」「仁愛
の君」と評されている。
貞享4年(1687) に築庭を開始。 園内中央は大きく開け、 平坦地の一部が芝生
で、残りの大半はもとの田畑をそのまま取り込んだ明るく広々とした庭園で
あった。 綱政は 「あまり手をかけない景色」と喜び、 毎日のように訪れた。 元禄13年
(1700) には敷地が今とほぼ同じ姿となる。 歴代藩主も政務の合間を過ごす
やすらぎの場として利用し、 庭園や建物には藩主の好みやその時々の社会
事情で手が加えられた。 城の後ろにあることから 「御後園」 と呼ばれた庭園は、
明治4年(1871) に後楽園と改称され、明治17年 (1884)に岡山県に譲渡され
一般公開が始まった。 その後、水害や戦災にも見舞われたが、江戸時代の絵
図や古写真にもとづいて復興され、今日にいたっている。
ごこうえん
Ikeda Tsunamasa was the second feudal lord of Okayama, an enlightened ruler
who excelled at both the literary and military arts.
The construction of Okayama Korakuen Garden started in 1687 under his orders.
The initial project featured a wide, open central area covered more in fields than in
lawns, depicting a countryside landscape, which future Ikeda Clan lords were able to
enjoy. As the years passed, various lords slightly modified some of the landscape and
architectural aspects of the garden in order to comply with their taste and with the
current fashion. The garden received its current name "Korakuen" in 1871, and in
1884 the Ikeda Clan ceded it to Okayama Prefecture, which opened the garden to
the general public. Severe damages caused by floods during the 20th century and
fires of World War II have luckily been repaired by using old illustrations called "ezu"
and photographs as references.
「池田綱政像 狩野常信筆」 部分 (曹源寺所蔵)/写真提供 岡山県立博物館
岡山藩主池田家略系図 Ikeda Clan family tree
綱政— 継政
宗政
光政
治政 斉政
(1609-1682) (1638-1714) ( 1702-1776) (1727-1764) (1750-1818) (1773-1833)
Mitsumasa
Tsunamasa
Tsugumasa
Munemasa
Harumasa
Narimasa
斉敏
政
茂政
章政
(1811-1842)
(1823-1893) (1839-1899)
(1836-1903)
= 養子
Adopted child
Naritoshi.
Yoshimasa
Mochimasa
Akimasa
=

全てのテキスト情報が検出され書き出されていました。

手書き文字の抽出

では手書きの文字はどうか。ということでこちらの画像を用意しました。

ホワイトボードに書いた文字を撮影したものです。

雑に書かれた文字でも検出できるのでしょうか。

実行した結果はこちら

検証画像:手書き
手書きってどこまで検出してくれるんでしょうか。
山田太郎

ばっちり検出できていますね。

これ以外にも色々とやってみたのですが、例えばパスポートって自分の名前(サイン)を手書きで書きますが、それもきちんと検出してくれていました。

まとめ

Cloud Vision API を使って画像のラベル検出と OCR を行いましたが、どれも精度高く検出されていました。

そして、こうしてトレーニングされたモデルを使えるのはとても便利でした。

Cloud Vision API は毎月最初の 1000 ユニットまでは無料なので、学びや理解のための利用なら課金も発生しないで利用もできそうです。

ラベル検出ですが、その画像に写っているものを検出できるだけする。というものになるので、例えば「犬」を撮影したとしても、背景に色々写り込んでいればそれらも検出されることになります。

この場合、「この写真は犬がメインの画像です」と判断したい場合にどうやっていけば良いのかっていうところが気になります。

Cloud Vision API では物体検出もできるようなので、ラベル検出というよりはそれを利用して位置や画像に占める割合(面積)とかを見ていったら判断できるだろうか。

cloud.google.com

もしくは Cloud AutoML Vision を使って追加でトレーニングを行なうか。

いずれにしても、目的を明確にした上で手段は検討する必要がありそうです。

もっと理解を深めて改めて色々チャレンジしてみたいと思います。


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

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

Clean Architecture考察

この記事は個人ブログの内容がソースです。 kami-programming.com

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

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