ytake blog

Web Application Developer

Kafka+Spark Structured Streamingとマイクロサービスアーキテクチャ

Kafka+Spark Structured StreamingとProtocol Buffers

マイクロサービスアーキテクチャというと、
適切なドメインモデルを参考にシステムを切り出して、
小さくしてチームを分けてAPI開発すればいいんだな、と思う方も多いかもしれませんが、
実はこのまま鵜呑みにしてやってしまうと、事業をうまく支えられるかどうか厳しい面が多くあります。

ビジネスを進める上でアプリケーションが動いていることは当たり前ですが、
事業を拡大したり推進していくには、エビデンスとなるデータが必要不可欠です。

つまりAPI連携をしてうまく動いてるように見えても、
あくまでアプリケーションレベルでの動きだけで、
マーケティングやさまざまなエビデンスになるデータを取得することは困難である
という状況になってしまうことが多々あります。

愚直にこれに対応しようとなると、数多くのバッチ処理、
バッチ処理の依存を解決するための仕組みづくりや、
深夜を超えて処理されるバッチ処理についての問題解決などなど、
実は最初に目をつぶって後回しにして、数年後ににっちもさっちもどうにもいかなくなる、
というのがデータ処理関連では非常によくある問題であったりします。
そもそもグローバルになると深夜という概念がなくなりますので、
なんでも日本時間の夜中に定期実行させる、というのが難しくなりますね。

事業をうまく支えるには マイクロサービスアーキテクチャというキーワードが注目されて数年経ちますが、
我々が目指すのはマイクロサービスアーキテクチャではなくて、
リアクティブシステムでなくてはいけません。
*リアクティブシステムはリアクティブ宣言(Reactive Manifesto)で定義されていますので、
詳しくはそちらを参照ください。

www.reactivemanifesto.org

リアクティブシステムを目指していけば、
自然とマイクロサービスアーキテクチャになっていくというのがなんとなくわかるかと思います。
マイクロサービスアーキテクチャが主で考えてしまうと、
GraphQLなどのBFF的な解決策やAPIそのものについての思考が優先されてしまうため、
リアクティブシステムの文脈はあまり見えてこないかと思います。

ということで、今回はリアクティブシステムを支えることができる
メッセージブローカーとしても強力なApache Kafkaを中心に、
Spark Structured Streamingと、アプリケーション関連で利用ケースも多いProtocol Buffersを扱う例の解説です。

なんでメッセージブローカー?

そもそもマイクロサービスアーキテクチャ等でKafkaのようなメッセージブローカーを使え、
と多くの実践者が発信するのはREST APIやGraphQLなどで通信を行う場合、
副作用のないReadを扱うには非常に強力で、むしろこれ以外の選択を取るのは難しいわけですが、
副作用のあるPOST、PUT、PATCH、DELETEのようなものへのアプローチであったりします。

単一のアプリケーション観点でみるとこれらはただのHTTPメソッドだったりするかもしれませんが、
システム全体でドメインモデルなどと照らし合わせてみると、
多くがドメインイベントだったり(イベントストーミングなどで導き出したものなど)がほとんどのはずです。
つまりWebアプリケーションの裏にあるデータの流れ(ストリーム)の観点では、
ビジネスを推進して行ったり、エビデンスとしていく信頼できるデータは
このドメインイベントのスナップショットを活用していくことであったりします。

もちろん永続されたRDBMS上のデータもそうですが、
ほとんどは副作用があるデータとRDBMSやS3やその他たくさんのデータを組み合わせなければなりません。

ここの解決策に、例えばAPIにPOSTリクエストを送った後に、
分析とか機械学習とか、マーケティング等に必要なデータを他のストレージに入れていきますね、
という処理ばかりを作っていくと全て同期的に処理しなければならなくなりますし、
なにしろn層コミット問題をAPI側で作り込まねばなりません。
データ連携がうまくいかないのを検知してSlackなどで通知して手動対応、もあるかもしれませんが、
アプリケーションが使われれば使われるほど手動対応がかなりの割合を占めていきます。
これではマイクロサービスアーキテクチャ的な側面だけを導入しても、
数年後にはうまく機能しないシステムと評価されてしまうでしょう。(それもひとつの経験ですが!)

この状態の場合は、DebeziumやAWSのDMSなどを使ってCDCを導入して解決していくしか無くなってしまうわけですが、
これにしても結局Apache KafkaやAmazon Kinesisといった
大規模な分散処理に対応したメッセージブローカーを活用することとなります。 (Akkaなどで解決するのももちろんあります)

今回はタイトルにもある通り、Apache Kafkaです。
AWS上ではMSKとして提供されていますし、オンプレでもクラウドでもConfluentなどで構築することができます。

kafka.apache.org

www.confluent.io

KafkaとProtocol Buffers

アプリケーション間でやり取りをするにあたって、すべてのシーンで型の制限がないJSONを使い続けるのはやはり難しいです。
ということでProtocol Buffersを利用することも多くあるでしょう。

developers.google.com

今回は例として、単純なユーザー作成に関するものを扱うものを使うとします。

syntax = "proto3";

package sample;
import "google/protobuf/timestamp.proto";

option java_package = "com.github.acme.sample.combine";
option go_package = "github.com/acme/sample/pbd";

message UserAction {
  uint64 correlationId = 1;
  enum EventType {
    CREATED = 0;
    DELETED = 1;
  }
  EventType event = 2;
  uint32 userId = 3;
  string name = 4;
  google.protobuf.Timestamp created = 5;
}

副作用が大事といいつつUPDATEがないのはただの例だからです。

ただのprotoファイルですがマイクロサービスアーキテクチャだったり、データ処理観点が混ざっているので
APIなどのWebに近いアプリケーションはGo、データ処理はSpark+Scalaでやるという体です。

Apache Kafkaとの組み合わせでよく質問されますが、
Protocol Buffersはただのバイナリとして扱われますので、Kafkaでも問題なく扱えます。

Goでは下記のようにして扱えます。

protoファイルで定義した通りに詰め込みます。

package message

import (
    pbd "github.com/acme/sample/publisher/pbdef"
    "google.golang.org/protobuf/types/known/timestamppb"
    "math/rand"
    "time"
)

func makeTimestampForProto() *timestamppb.Timestamp {
    return timestamppb.Now()
}

func ExampleMessages() []*pbd.UserAction {
    var sua []*pbd.UserAction
    return append(sua, &pbd.UserAction{
        UserId:  uint32(1),
        Event:   pbd.UserAction_CREATED,
        Name:    "aaa1",
        Created: makeTimestampForProto(),
    }, &pbd.UserAction{
        UserId:  uint32(2),
        Event:   pbd.UserAction_CREATED,
        Name:    "aaa2",
        Created: makeTimestampForProto(),
    })
}

Kafkaへの送信はこんな感じです。

(複数のtopicへのメッセージをうまく扱うにはインターフェースなどにしておくといいでしょう)

package message

import "github.com/confluentinc/confluent-kafka-go/kafka"

// Publisher interface
type Publisher interface {
    Client() *kafka.Producer
    RetrieveTopic() *string
    RetrievePartition() int32
}
package pub

import (
    "fmt"
    "github.com/confluentinc/confluent-kafka-go/kafka"
    "github.com/acme/sample/publisher/message"
)

// Client Kafka client struct
type Client struct {
    Producer *kafka.Producer
}

type Messenger struct {
    publisher message.Publisher
}

// RequestParameter for publisher
type RequestParameter struct {
    Byte []byte
    Key  []byte
}

// NewProducer create producer
func NewProducer(broker string) (*Client, error) {
    p, err := kafka.NewProducer(&kafka.ConfigMap{
        "bootstrap.servers":        broker,
        "api.version.request":      "false",
        "message.timeout.ms":       "300000",
        "socket.timeout.ms":        "30000",
        "message.send.max.retries": "5",
    })
    return &Client{Producer: p}, err
}

// Publish to kafka bootstrap server
func (c *Messenger) Publish(parameter RequestParameter) error {
    deliveryChan := make(chan kafka.Event)
    km := &kafka.Message{
        TopicPartition: kafka.TopicPartition{
            Topic:     c.publisher.RetrieveTopic(),
            Partition: c.publisher.RetrievePartition(),
        },
        Value: parameter.Byte,
    }
    if len(parameter.Key) != 0 {
        km.Key = parameter.Key
    }
    err := c.publisher.Client().Produce(km, deliveryChan)
    if err != nil {
        return err
    }
    e := <-deliveryChan
    m := e.(*kafka.Message)
    if m.TopicPartition.Error != nil {
        fmt.Printf("failed to deliver message: %v\n",
            m.TopicPartition)
    } else {
        fmt.Printf("delivered to topic %s [%d] at offset %v\n",
            *m.TopicPartition.Topic,
            m.TopicPartition.Partition,
            m.TopicPartition.Offset)
    }
    return nil
}

// SampleTopicClient for no key
type SampleTopicClient struct {
    kafka     *Client
    topic     *string
}

// NewSampleTopicClient client for no key example
func NewSampleTopicClient(topic string, c *Client) *Messenger {
    return &Messenger{publisher: &SampleTopicClient{kafka: c, topic: &topic}}
}

func (c SampleTopicClient) Client() *kafka.Producer {
    return c.kafka.Producer
}

func (c SampleTopicClient) RetrieveTopic() *string {
    return c.topic
}

func (c SampleTopicClient) RetrievePartition() int32 {
    return kafka.PartitionAny
}

このような感じで送信すると、Kafkaにはvalueはただのバイナリとして保管されます。
アプリケーション連携でAPI連携で使いたくなりますが、
これをドメインイベント、メッセージとして連携していきます。
Kafkaを使うのでサブスクライブするコンシューマは、
protoファイルを使って必要な値に変換するだけとなりますので、特に変わったことはありません。

Kafkaのtopicはgroup.idが分かれていれば並行処理をいくつも増やせることができますし、
partitionを分割することでround robin的に負荷分散をすることもできます。 Kafka Connectをそのまま使えば何も実装せずにS3やRDBMS、Elasticsearchや他のデータストレージへ保管できますので、
前述のCDC的なこともドメインイベント、メッセージを主としてできます。
(DynamoDB StreamsやDMSの場合は書き込んだタイミングでメッセージ送信になります。)

ECのカートのようなものへのアプローチとしてはまだ足りませんが、
だいたいのアプリケーションの副作用的な動作はこれでイベントを正として保管しつつ(イベントソーシングとしても)、
後続の処理は非同期で、リアクティブシステムの要素を取り込んで独立してきます。
CQRSだったりに繋がっていく話ですね。

ではデータ処理観点ではどうでしょう。

ScalaPB

データ処理観点ではメッセージブローカーに到達した時に、
何かのデータと組み合わせて拡張したようなデータソースをつくることはよくあります。
ウインドウ集計などもそうです。

マイクロサービスアーキテクチャなどで分割されたアプリケーションであっても、
メッセージブローカーでドメインイベントがスナップショット的に送信されていれば
こうしたことは容易です。
最初の頃にやってしまいちなのは、ドメインイベント送信だ!といいながら中身は
パラメータ化されていて、受け取った側はそれをもとに副問合せしてほしい、というパターン。
これにしてしまうと、他の処理が先に動いていたり、partition分割で他のコンシューマが処理していたりすると、
後続の処理全体がファントムリードやダーティリード的な状態に陥りやすくなりますので、
やらないように注意してください!コマンドバス的に信号だけを送るようなアプローチは場合はまた別ですが。

さてさて、ScalaではScalaPBを使ってProtocol Buffersを扱うことが多くあります。

scalapb.github.io

project/scalapb.sbtなどを作って下記のものを記述します。

addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.3")
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.11.3"

build.sbt にも同様に追記していくといいでしょう。
sparkと組み合わせるには以下のような具合です。

libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-library" % "2.12.10",
  "org.apache.spark" %% "spark-core" % "3.0.3" % "provided",
  "org.apache.spark" %% "spark-sql" % "3.0.3" % "provided",
  "org.apache.spark" %% "spark-sql-kafka-0-10" % "3.0.3",
  "com.thesamet.scalapb" %% "sparksql-scalapb" % "0.11.0",
  "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf",
  "junit" % "junit" % "4.13" % Test,
  "org.scalatest" %% "scalatest" % "3.2.7" % Test
)

assembly / assemblyShadeRules := Seq(
  ShadeRule.rename("com.google.protobuf.**" -> "shadeproto.@1").inAll,
  ShadeRule.rename("scala.collection.compat.**" -> "scalacompat.@1").inAll
)

Compile / PB.targets := Seq(
  scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"
)
Compile / PB.protoSources += file("../protobuf")

ここで注意なんですが、最新のSpark 3.2.0だとScalaPB自体で対応が追いついていないものがあるので、
うまく動かなくなりますので、2022/1/2現在では 3.0.3 を使うようにしておきましょう。

これで、protoファイルに記述した option java_package = "com.github.acme.sample.combine"; がパッケージとして
そのまま利用できます。

Kafka+Spark Structured Streaming(with ScalaPB)

spark.apache.org

簡単なStream処理だけであればKafka Streamsだけでもできますが、
他のデータをくっつけて集計したり、ほかのストレージに書き出したりは
Apache Sparkに任せた方がより多くのことができます。

SparkSessionは特に変わったものはありません・・。

import org.apache.spark.sql.SparkSession
import org.apache.spark.{SparkConf, SparkContext}

/**
 * @param appName
 * @param checkPoint
 */
class SparkApplication(appName: String, checkPoint: String) {

  protected def context(): SparkContext = {
    val conf = new SparkConf().setAppName(appName)
    conf.set("spark.sql.session.timeZone", "Asia/Tokyo")
    new SparkContext(conf)
  }

  def createSession(): SparkSession = {
    val spark = context()
    spark.setCheckpointDir(checkPoint)
    spark.setLogLevel("WARN")
    SparkSession.builder
      .appName(appName)
      .getOrCreate
  }
}

ここではKafkaを接続してStructured StreamingでSelectだけする例です。
Spark Streamingでもできますが、
Spark SQL形式で記述できたりするのでこちらから入っていく方が簡単かなと思います。

Kafkaへの接続はreadStreamでformatにKafkaを指定するだけです。
DataFrameになりますので扱いも簡単に!

import org.apache.spark.sql.{DataFrame, SparkSession}

object KafkaDataFrame {

  def make(ss: SparkSession, bootstrapServers: String, topic: String): DataFrame = {
    ss.readStream
      .format("kafka")
      .option("kafka.bootstrap.servers", bootstrapServers)
      .option("subscribe", topic)
      .option("startingOffsets", "earliest")
      .load()
  }
}

SparkSessionを生成して、Kafkaと接続してストリーム処理はこれだけで始められます。

import com.github.acme.sample.combine.definition.UserAction
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types.{IntegerType, LongType, StringType, TimestampType}
import scalapb.spark.Implicits._
import scalapb.spark.ProtoSQL

import java.util.Properties

object StructuredStreamingRunner extends App {

  if (args.length < 1) {
    throw new IllegalArgumentException(
      "This program takes one argument: the path to an environment configuration file.")
  }
  val ss = new SparkApplication(getClass.getName, "/tmp/streaming_runner").createSession()

  val prop = new Properties
  prop.load(new java.io.FileInputStream(args(0)))
  val df = KafkaDataFrame.make(ss, "127.0.0.1:9092とかkafkaのbootstrap.servers", "topicの名前")
  // 省略
}

簡単!
このままの場合は、Spark内でのスキーマは

    root
    |-- key: binary (nullable = true)
    |-- value: binary (nullable = true)
    |-- topic: string (nullable = true)
    |-- partition: integer (nullable = true)
    |-- offset: long (nullable = true)
    |-- timestamp: timestamp (nullable = true)
    |-- timestampType: integer (nullable = true)

という定義になります。
valueがProtocol Buffersでバイナリになったものです。
これを取り出すためにScalaPBを介して分解します。 UDF (User Defined Function) を介してバイナリをStructに変換できます。
UDF自体は使い勝手がいいわけですが、Sparkは分散処理が基本なので多用すると
パフォーマンス劣化につながるので注意してください。
JSONなどはStructTypeでschemaを定義して
org.apache.spark.sql.functionsfrom_jsonなどを使うだけです。

val parseUserAction = ProtoSQL.udf { bytes: Array[Byte] => UserAction.parseFrom(bytes) }

このudfを適用すると、下記の通りprotoファイルで記述した通りの定義になります。

 |-- value: struct (nullable = false)
 |    |-- correlationId: long (nullable = true)
 |    |-- event: string (nullable = true)
 |    |-- userId: integer (nullable = true)
 |    |-- name: string (nullable = true)
 |    |-- created: struct (nullable = true)
 |    |    |-- seconds: long (nullable = true)
 |    |    |-- nanos: integer (nullable = true)

このままでも value.event のようにアクセスすることもできますが、他のDataFrameとJOINなどをする時にちょっと面倒です。
わかりやすく最適化して、value自体は削除してもいいでしょう。

  df
    .select(col("key"), col("value"))
    .withColumn("key", col("key").cast(StringType))
    .withColumn("value", parseUserAction(col("value")))
    .withColumn("correlation_id", col("value.correlationId").cast(LongType))
    .withColumn("event", col("value.event").cast(StringType))
    .withColumn("user_id", col("value.userId").cast(IntegerType))
    .withColumn("name", col("value.name").cast(StringType))
    .withColumn("created", col("value.created.seconds").cast(LongType))
    .withColumn("created_timestamp", col("value.created.seconds").cast(TimestampType))
    // ウインドウ集計したい場合などは ウォーターマークを付与するなどしましょう
    // .withWatermark("created_timestamp", "10 minutes")
    .drop("value")
    .createTempView("events")

例えば上記のようにすると、Spark SQL内ではeventsがテーブル名となりますので、
慣れ親しんだSQLで記述が自由にできるようになります。
keyはKafkaでpartitionが複数ある場合の判定などにも使うもので、この例ではkeyとvalueをDataFrameから取り出して、
そのDataFrameを基準にカラムなどの定義をする処理になっています。
必要なカラムを直感的に操作できるように定義した結果は下記のとおりです。

 |-- key: string (nullable = true)
 |-- correlation_id: long (nullable = true)
 |-- event: string (nullable = true)
 |-- user_id: integer (nullable = true)
 |-- name: string (nullable = true)
 |-- created: long (nullable = true)
 |-- created_timestamp: timestamp (nullable = true)

これがeventsテーブルとなるので、いかようにも分析なり、データ結合なりウインドウ集計などができます。
最後に下記を追加することで、指定したKafkaのトピックにデータが受信されると、
サブスクライブの処理的な感覚でマイクロバッチ的にストリーム処理されていくようになります。

  ss
    .sql("SELECT * FROM events")
    .writeStream
    .format("console")
    .start()
    .awaitTermination()

ドメインイベントなどをメッセージとしてアプリケーションに適用することで、
Protocol Buffersを使ったリアクティブシステム・マイクロサービスアーキテクチャ的なアプリケーションに対しても
データ観点で重要なスナップショットを活用してさまざまなビジネスや、もちろん他アプリケーションへ繋げることができます。

手法ばかりに注目していると、こうしたデータ処理やその先のビジネス的な戦略など、
裏側の世界にたいして有用でないものにもなり得ますので、
是非メッセージ・イベント駆動などに触れてみていただけるといいかなと思います。

Apache Kafka or Amazon Kinesis + Apache Sparkの組み合わせはまだしらばく定番かと思いますので、
これをきっかけに取り組んでもらえると仲間が増えて嬉しいです!

多様な働き方で忙しかった2021年振り返り

2021年は忙しかった

年末でいつもの振り返り。
今年は多忙につき年末のいつものアドベントカレンダー参加は見送っていたのと、
ブログ等のアウトプットもあまりしていなかったので、振り返りと、どんなことをしていたのか、
ということの殴り書きです。

スターフェスティバル

本業でメインでコミットしているスターフェスティバルの総括はCTOのsotarokのエントリを!

note.com

この中で自分はどういうことをしていたのかというと、
前半は致命的な問題を抱えているシステムの改善などを主にやってました。
もちろんそれは技術分野だけではなく、
業務フローやアプリケーション起因のオペレーション、データ設計も含めて全体的にここに問題があるぞ、
というのを明らかにする活動もやりつつ、非エンジニアの方々と率先してコミュニケーションしながら、
信頼を得るための活動だったり、とにかくドメインエキスパートになるためにかなり振り切った活動をメインに。
入社して半年過ぎくらいで大体の改善ポイントだったり、
改善のための長期ロードマップを自分の中で確定させて大体の行動指針をまとめて、
周りに少しずつ共有していく、ということを日常的に回して実際に着手を開始していました。

組織は当然生き物なのでその時で変わったりでいろいろやることが増えてきたなぁというのが後半。
CTOのエントリにもあるプリンシパルエンジニアという役割がついたのもこのくらいの時期だったかも・・。

後半はデータ領域の改善やインフラ領域も持ちながら、根深い問題を抱えているアプリケーションの刷新・移行に着手開始!
内外に向けたDXだったり、全社の意識を変えていくというパワーを使うことを徐々にメイン。
DRE的な働きはここ数年ずっと注力してやっていたり、Webアプリケーションは長くやっていて、
加えてプロダクト開発的な視点や、経営関連(他社でCTOやってた)もこれまで携わっていたのもあり、
エンジニアによっては全くエンジニアの動きに見えないところも多々あったとは思うんですが、
全社的に良い環境、良い文化に変えていかなければエンジニアが良いモノづくりをできない、というのは当たり前なので、
当たり前のことを当たり前にやっていました。

組織や経営的なことはCTOのsotarokがやっているので、
その他の例えば長期プロジェクトを進める上で必要な関係者の意識を徐々に変えるための活動だったり、
データ観点での業務改善の意識づくりだったりに重きを置いて仲間を増やそう、みたいなこともやりつつ、
他プロジェクトの技術的なアドバイスだったり(全体的なシステムの繋がりなどを踏まえたやつ)、
ひたすらETL周りの開発だったり、リアクティブシステムの仕組み作りだったりを並行してやっていました。

全然これはまだまだ途中なので数年かかる話ではあるんですが、
当然だけどかっこいいアーキテクチャで作る、という頭は毎回ほとんどなく(最初からマイクロサービスアーキテクチャ化も絶対にしない)、
できた結果がそうだった、というスタイルなので完成図がこうみたいなことはほとんどなくて、
ただデータ観点やビジネスの変化に耐えうる柔軟な変化と回復力などを考えて基本的にリアクティブ、
というのをベースにしています。

全体的にWebアプリケーションとデータ領域と業務がこうなる、という全体像がある程度見えてはいるわけですが、
これまで全領域に携わってきた!みたいな方はほとんどいないので、ytake何言ってんだこいつ
みたいなことも多かったかなと思います(市場的にもそういう方は少ないです)。

これはアプリケーションが主の思考ではなくて、

  • 事業を運営、拡大するにあたってエビデンスとなるデータはどのくらいあるか、どのくらい正確か
  • データがどのタイミングでどう生まれているか(ドメインイベントに近いですね)
  • データの中身はどういった業務と結びついているか
  • データに対する意識はどのくらいか

みたいなところから会社全体に入り込んでいき、
業務フローや事業設計だったりも分析しながら、それを支えられるアプリケーションになっているか、
大元のデータ設計はどうなっているかとメスを入れていく感覚です。

物事を進めていく上でアプリケーション開発は確かに大事ですが、
かっこいい設計や、洗練されたコーディング、開発言語選択よりも、
会社が事業を進めていくため、拡大していくためのエビデンスになるものを重視しなければなりません。

アプリケーションのここを変えることで反対側のデータがこうなって、業務がこう変わる、
そうすることで価値のあるデータに繋げられる!
これを使えばマーケティングや、セールス、もちろん今後のアレにつながる!
とアプリケーションに近いところからかなり反対側まで見ています。
言葉で書くと簡単ですが、実際にはもっといろいろあります。
(エンジニア組織外の方ととにかく会話しまくって、ドメインモデル作ったりとかコンテキストマップ作ったりとか)
これらを実現していくために技術を使って解決する、
ではそれをどんなスケジュールで、ロードマップはどう?と逆算して動いていくわけです。

ここからさらに加速していくと思いますが、
ここはなかなか本だけ読んでなんとかなる部分ではないし、数年後にわかるみたいなものなので、
関わる方はそういうものだと思ってください。
これもここ5年くらいと思考的に同じですね、昔一緒に働いていた方々は大体わかるかも。
最近の言い方だと、いわゆるCDOに近い感じかなと思います。
人によってはプロダクトマネージャ・プロジェクトマネージャにも見えるしエンジニアにも見える、
というものですね。

進もうと思う方向に進めるにはなんでもやる、という今までのスタイルそのまんまではありますが、
エンジニアでもそういう行動をしたい!という方は多いと思いますので、
中長期の事業などを考えてどういうステップを踏んで解決していくか、
それに連動して地球の裏側は何が起きてどうなっていくか、みたいな思考を重ねていくと
良いトレーニングになるかもしれません。
*後述する副業の方ではそうしたメンタリング、実現するための技術共有などもやっています。

技術的なこと

技術的には、自分の中ではほとんど新しいことはやっていなくて、
これまで培ったものをそのまま活かす、みたいな具合です。
相変わらずPHPはほとんど触らず、内部APIとか常駐プロセス型のアプリケーションはGoだったり、
データ領域はここしばらく使っているいつものApache Kafka / ConfluentとApache Sparkあたりを中心に
Scalaで開発したり(流石にこの辺をPHPでやるのは難しいっていうかできないので)。
Embulkのプラグイン作ったりもしてました。
システム全体のアーキテクチャ設計はこれもここ数年とあんまりスタイルは変わっていないので、
ソフトウェアのアプリケーション設計的な視点で見ればEvent Sourcing+CQRS、
データ的なものを取り入れて全体でみるとリアクティブシステムよりなものを作っていました(いまも)。

久しぶりにElasticsearchで形態素解析作ったりして、過去の経験をそのまま使って半分遊んでましたね・・w

エンジニアは引き続き募集してますので、一緒に働いてみたいという方は是非!
上記にあるような動きをしたりが実際に見えたり、関わったりできるので将来的にCTOとかテックリードを目指したい!
っていう方には結構いい環境だと思います。
メルカリの元CTOがいろいろやっているっていう職場に興味がある方も多いでしょう!

findy-code.io

副業

コロナ禍というのもあり 今年は本業に加えて副業も。
知人の会社の手伝いをやっていたり、複数開発に携わっていたりというのを10年ぐらいやっているので、
時間を使いながら自分の知見を広めたり、いろんな課題のアプローチ方法を洗練させようというのもあり、
いわゆる技術顧問や社外CTO的なものを何社かで始めました。
(社名公開しても問題ないんですけど、各社で公開されていないので社名は秘密)

技術顧問というとコンサル的なことだったりもするんですが、
各企業で問題が異なるので、組織的なところやチームビルディンやプロジェクトマネージメント的なことも実際にやりながら、
コードレビューやペアプロ、モブプロ、メンタリングなどももちろんやっています。
事業側の方と話してロードマップ作ったりとかもやりました(大変だった・・)。

マイクロサービスアーキテクチャ化などで協力することもあり、
ドメイン駆動設計についてのワークショップや、ユースケース駆動やモデリングの勉強会、イベント駆動に関する勉強会、
分散処理勉強会、データ設計の勉強会などもかなり濃く実施しました。
(そういうのもあって、パブリックなイベントにはほとんど参加せず)

副業はKotlinをサーバサイドで使うところも多かったので、
Kotlinやったりtsやったり、たまにPHPだったりAWS移行の手伝いをしたりしてました。

自分の知見を共有して、実際にいろいろやっていただいたり、問題解決や組織が良い方向に変わったり、
当然自分にもやった分だけ問題解決に対するアプローチの抽斗が多くなって返ってくるわけですから、
みのりがある一年でした。
来年も引き続きよろしくお願いします!

*是非うちもお願いしたいんですけどとなっても体が空いていないため現在はお受けできません

書籍

「PHPフレームワークLaravel Webアプリケーション開発 バージョン8.x対応」を少しやったりしていました。

これ以外に実は水面下で単著の書籍を書くぞ!というのがスタートしていました。
が、かなり忙しくなってしまったのもあり、亀のようなスピードに・・。

どんな書籍なのかというと、事業分析とかしつつ技術的にどう改善・開発していくか、
中長期を支えるための分析方法だったり、それを表現するための技術選択だったり、
Webアプリケーション・データ基盤関連・モデリング・コミュニケーション方法だったり、
これからの時代の企業を支えるアプリケーション開発につながるようなそんなヒントになるようなものです。
ただ亀のようなスピードになってしまったので、来年はシュッといきたいところ・・・。

健康

去年頭頃に太ってしまったので3ヶ月くらいで一気に痩せて一年ちょっと。
リバウンドはほとんどなくてちょっと増加したのはありますけど(1kgほど)、
運動等を継続しているので、単順に筋力と共に増えた、という具合かもしれません。
ランニングは毎月大体100km前後くらい走り、週末は1日10km以上必ず走っています。
おかげで1年で1000km以上走って、NIKE RUN CLUBが青になりました。
忙しい時にこそランニングすると思考がリセットされて大変いいですね!
完全に習慣化しているのでランニングシューズにもうちょっと拘りたいところです。
ランニングの荷物を減らしたいのもあって初代Apple WatchからWifiのみだったのが、
セルラー版を買いました。進化した。

ランニングのボトルとちょっとした荷物(iPhone 13 Pro Maxも入ります)を入れられるポーチは
個人的に下記のものが今のところ最強です。

他にもいろいろありましたけど、今年一年はこんな感じでものすごいスピード感で駆け抜けていきましたね・・。

関係者の方々は今年一年お世話になりました。
来年もどんどんやっていきたいと思いますので、よろしくお願いいたします。

スターフェスティバル株式会社は仲間を募集してるらしいぞ

もう少しで在籍1年

スターフェスティバル株式会社にジョインしてもうすぐ一年くらいになります。

中長期の目線を持ってゴールに向かって様々なものを変えていく、
というところにフォーカスして日々改善やいろいろな破壊活動を続けています。

だがしかし、やはり物事を改善していったり、色々な整備や推進をしていくにあたっては
いろんな方の協力が必要で、スピード感もどんどん出していきたい、
ということで、タイトルにある通り各方面のエンジニアを募集しています。

募集だけ書いても面白くはないので、中身を少し書いていきましょう。

何をしている会社?

スターフェスティバル株式会社は、

顧客基盤をもとに、「ソリューション」と「販路」の両軸から、 飲食店の中食・ECビジネス参入を支援

という領域を主としている会社です。
サービスはいくつかあり、そのなかでも ごちクルは見聞きしたことがある方もいると思います。

gochikuru.com

飲食の経験が重要か、その知識が必要か、というと必ずしもそうではありません。
事業などに触れることで、ビジネスを動かすための知識を得ることができます。
当たり前ですが・・。

どんなエンジニアがいるの?

まずエンジニアの組織としては、まだまだ小さい組織です。
いろいろなことを考えると、全然足りない!という大きさです。

全員をここで載せることはできませんが、
中の人たちを紹介しましょう!

CTO

ご存知の方も多いですが、
現在CTOは メルカリでもCTOを務めていた @sotarok で、
吉祥寺のP2B Haus クラフトビアレストランのオーナーをしながら、
技術顧問だったり、いろいろやっています。
いつ寝てるんだろう・・

チームを率いてる人たち

@sotarokと同じく、メルカリで活躍していた @kajiken
エンジニアをやりつつ、プロダクトマネージャもこなしながら、
サービス作りをいろんな面で推進しています。
各方面でリードしていてすごい!

@sotarok、@kajikenと同じ、メルカリで活躍していた @yui_tang
kajikenと同じくエンジニアをやりつつ、プロダクトマネージャもこなし、
サービス作りをいろんな面で推進しています。
チームの育成もしながら、新しい技術チャレンジなども率先していて、
めちゃくちゃバリューを発揮しているすごい人!

パワフルな newcomer

PHP界隈で有望な若きエンジニアとしてお馴染みの @strtyuu
通称あひる、人の形をしていますがあひるです。
あひるさんはエンジニア歴としては相当若いんですが、
本人のキャラと親切さ、そしてチャレンジ精神を兼ね備えていて
チームを盛り上げつつドメインについて考えすぎて寝れなくなってしまうのではないか、
というぐらい事業に向き合っている力強い仲間です。

そして岡山からフルリモート勢の @ikkitang
いろいろな経験をしていて、そしてデータベースは ikkitangに任せておけば安心。
データベースの知識と経験、そしてエンジニアとしての素養とチャレンジ精神を持っていて、
知らないことでもどんどん吸収してどんどんフィードバックしてくれる、
高い行動力を持つ強力な仲間です。

ムードメイカーとしても最高な人たちが新しく加わって、
CTOを筆頭に組織の刷新や、業務フローの刷新やシステム面の改善などを行なっています。

どんな開発をしているの?

このご時世、なかなか他社の話を聞いたり、呑みながら、みたいなのも難しいので・・。

現スターフェスティバルでは、
Webアプリケーションと、データ基盤的な領域の開発の2つが大きなところとしてあります。

Webアプリケーション

スターフェスティバルのエンジニアの多くが携わっているところですね。

元々持っているWebアプリケーションなどはPHPで作られたものが多くあります。
これらのアプリケーションの運用だったり保守をしているメンバーがいたりもします。
PHP8.0へのアップデートに向けて毎日改善したりしています。

ここ一年くらいはPHPで作る、ということはあまりしておらず
Webアプリケーションのサーバサイドでは Node.js(TypeScript)、
フロントエンドはNext.jsを用いて開発することが多く、
GraphQL などを用いてモダンな開発をしてます。

最近はTS得意な勢いのあるチームが React Nativeで
積極的にiOS/Androidアプリもつくっていて大変良い流れです。

環境についてはAWSで、ECSを中心に自動化だったりしている環境です。

データ基盤的なところ

スターフェスティバルの取り組みとしては比較的新しいところで、
自分がメインでいろいろやっている分野です。

一般的なETLを使ったデータ集約を筆頭に、
集約したデータの加工や他データソースとの結合、
レポーティングのための仕組みづくりだったりに取り組んでいます。
データ基盤ということもあって、小難しいものが多かったりもしますが、
個人的にはここ5年くらいずっとやっているところなので、最高に楽しいところではあります。
ライブラリがない?それならば自分で作る、みたいな毎日です。

ここはGoやScalaなどを使って細々した処理を作ったり、
Sparkで動く処理をひたすらやりつつ、データ起点にビジネスを支える仕組みなどを考えて
ビジネス側の人たちと連携しながらわちゃわちゃしています。

どの領域もそうですが、技術ファースト的な考え方をしておらず、
事業を中心に、
要件分析にフォーカスしつつ中長期を意識したものづくりをしています。
当たり前のことですがドメイン駆動です。

とはいっても仲間がたりない

簡単な紹介だけですが、
各領域のエンジニアの仲間を求めています。

開発組織を強化して、様々な改善とサービス作りをもっと推進していきたい
というのが第一!!!

TSを用いた開発では、フロントエンド、バックエンド、というよりも
どちらに対してもある程度のカバレッジを保つTS/JSエンジニアでしょうか。
当然めちゃくちゃ高い専門性重視というわけではありません。
TSをメインの武器にしつつ、どちらでもチャレンジできる、
そしてチームワークと事業を主としてチャレンジ精神あふれるエンジニアであれば
どの開発でも力を発揮することができます。

逆に待ちのスタイルの方はあまりフィットしないかもしれません。

基本的にPHPのエンジニアも、データ領域のエンジニアも同じですね。

当然新しいものづくりだけでなく保守や運用もあります。
例えば0 1のフェーズは苦手だけど、
運用しながらアプリケーションをごりごり改善、リプレースしていくのが好き、
そんな方は神様になれます。

もちろん上記に挙げていないインフラエンジニアも求めています。

開発プロセスや、アプリケーション設計などについても日々ディスカッションしながら
毎日トライアンドエラーができる環境になっていて、
事業をドライブさせていく試みであれば大歓迎な組織です。

あ、住んでいるところは問いません。
フルリモート大歓迎です!!!!

あまり長々書いても誰も読まないので、
今は転職しないけど話を聞いてみたいな、とか
興味あるなーとか、面白そうだな、という方は気軽にtwitterなどで連絡をいただけるとうれしいです。
ここには書けない話なども詳しくお伝えします!!!!!!

「PHPフレームワーク Laravel Webアプリケーション開発 バージョン8.x対応」執筆しました

新しいかもしれないLaravel本

新しいかもしれない、というのは
本書は3年ほど前の「PHPフレームワーク Laravel Webアプリケーション開発」の
改訂版で、
Laravel8対応のコードなどに変更してあり、一部改訂となっているためです。

3年経ったということもあり、いくつか環境も変わっていますので
その辺りが変わっていたり、文章も所々洗練されたような変更もあります。

はじめに

Laravelの入門に向けた書籍や記事は多くあります。
それらを参考にしてリリースされたサービスも多くあるかと思います。

そんなサービスもおそらく数年経って今も運用されていたり、
機能改善などが継続的に行われているのではないでしょうか?

そのような状況でおそらく、改善がしづらい、だったり
機能追加がなかなか難しい状態であったり、
きっといくつかネガティブな要素もあるのかもしれません。
本書は基本的にそういったアプリケーションに対してどう向き合い、
どういう手法をしておくとより良いか、という内容になっています。

本の読み方

前回のレビューなどでもありましたが、
基本的に初学者向けの内容にはなっていません。

アプリケーション開発全体で機能要求があり、どう実装するかイメージはついている、
ただ高い保守性と長期運用に耐えるようにするにはどう考えて、
実装すれば良いか、そのヒントがPHPで書いてあって、かつLaravelだと都合がいいなぁ、
というレベルの方にはフィットするかと思います。

フレームワークのコードはある程度読める、
責務分割とか聞くけどどういうアプローチがあるの?
疎結合とかテストが書きやすいってどういうこと?

という方にも大きなサポートになると思います。

所謂写経をするようなタイプの本ではありません。

それはなぜか?

写経をしたとしてもそれは書籍の中のサンプルへの課題解決にしかならず、
みなさんのアプリケーションで持っている課題は多種多様で、
それこそ写経したからと言って何も解決されません。

この本は何かの開発案件で、こういう機能が要求されていて
でも作り方がわからない、何かいいサンプルはないかなぁ、という方には不向きです。
ただ所々考え方のヒントや聞いたことがないキーワードなどが出てくると思いますが、
それらをヒントに理解できるように学習していくといいでしょう。
きっと大きな力になってくれます。

どうして細かい解説がないの?

本書にある考え方や物事の捉え方などの手法を解説してから
フレームワークの解説をして、写経できるようにしてよ!みたいな感想を持つ方も多いと思いますが、

考え方や物事の捉え方などの手法

これだけで本が数冊書けるレベルなのと、
Laravelというテーマから大きくずれてしまうので、その辺りは解説していません。

参考書籍は3章の最後に載っていますので是非そちらとともに読んでみてください。

こんな中身

本書は必ずしも初めてLaravelを使う、という方には難しい内容かもしれませんが、 多くの内容はLaravelに限らず導入できる内容になっています。

全体を通してLaravelが主体ではありますが、
一般的なフレームワークの本よりも設計面にフォーカスしています。
ドメイン駆動設計文脈で取り上げられる実装パターンは盛り込まず、
責務分割や、API設計などの視点がいくつか入っています。

graphQLなどの導入も多い現在ですが、
RESTでも表現できることはたくさんあります。
そうした方にはHATEOAS、ハイパーメディアについて解説している箇所が
役に立つはずです。
(多分PHPの書籍でこの手の解説がある本は他にありません)

また大きめなアプリケーションになると必ず発生する、
DBに対して非正規化にするか正規化を守るか、でもどうやってパフォーマンスを改善するか、
という問題に対してはLaravelの仕組みを使ったCQRS的な解説がヒントになるはずです。

このあたりはかなり実戦向きで、
簡単な記述方法だけでSQLもなにも意識せずに実装したい、という方には
まったくLaravelらしくないと感じると思いますが、
何も意識せずに簡単に実装して数年経って顕著化した問題に対する
改善策に結びつくように解説しています。

そういう意味でもやはり初学者向きではないでしょう。

ただこの辺りはフレームワーク、言語すらも問わず応用できるように解説しています。

担当

前回と同じく 担当したのは、3章、4章後半、6章、7章と10章です。 こめた想いは以下の通り

3章

幅を広げすぎないようにMVCとADRのみに焦点を絞って、簡単な解説などがあります。
どちらがいい、という話ではなく開発チームの体制や、長期運用するかどうかなどで大きく変わります。
この先の知識や学習するにはどうしたらいいんだろう、
公式ドキュメントだけの使い方だけではない選択肢の一つとして読んでみると楽しめると思います。

4章後半

レスポンスとミドルウェアが主ですが、
少し前に取り上げられることも多かったSSE(ストリームを使ったレスポンス)だったり、
APIの REST Level 3 / HALの導入方法だったり、
これからのアプリケーション開発に導入することができる内容になっていると思います。

7章

EventとQueueの基本に触れながら、データ分析処理などでも用いることが多いCQRSライクな方法に発展します。
大きなアプリケーションではメッセージングをApache KafkaやAmazon SQS、Kinesis Data Firehose、
RabbitMQなどに差し替えてみてください。

個人的なオススメは前回同様
後半のテスト関連の9章と11章です!

アプリケーション開発のヒントとして役立てていただければ幸いです。

フレームワークに拘らずにそういう何か学びたい

いい本がたくさんあります!

あとは若干自分のバイアスがかかったものではありますが、
分析を学んでアプリケーション設計に活かし、
その周辺のインフラやミドルウェアに対してもどういう観点でどうアプローチしていくか、
物凄く長編になりますが何かしらの形になる予定です。
たぶんコードはPHPではない可能性もありますが・・。

こちらもご期待ください

自分流エンジニアの歩み方

これは先日、株式会社アイスタイルにて参加した社内むけで話した内容を清書したものです。

(現在は株式会社アイスタイルの社員ではありませんが技術顧問的な立場でサポートさせていただいてます)

対象として、初学者やエンジニアなりたての人向けではありませんが、
2、3年目の方とかが読むといいのかもしれません。

自分流のこれまでのやってきたものだったりマインド的な話だったりそういうもので、
これをやれば誰もがエンジニアとして成長できる!というわけではありません。

参考にできるところは参考にするか、ヒントにするとか息抜きに読むくらいがちょうど良いです。

加えて将来CTOだったり、技術顧問という立場になりたい、という方にもいいかもしれませんが、
自分のスタンスだったりが多く含まれていますので、
世間一般で求められてるCTOだったり技術顧問との乖離があったりするところもあると思います。
参考にできるところは参考にするか、ヒントにするとか息抜きに読(略

そもそもあなた何している人?

日中はスターフェスティバル株式会社に所属していて、
開発とマネージメント的なことをしていますが、
勤務時間外ではこれまでの経験を生かして技術顧問的なことをしたり、
マイクロサービスアーキテクチャ化の支援、
DX化推進やそれに伴うドメインモデリングの支援、
社内外問わずエンジニアのメンタリング、
組織をリードするために社外CTO的なこと、
クラウド移行だったりオンプレも含めてアーキテクト的なこと、
中長期的な事業戦略とそれに伴うプロダクト開発のロードマップ作成と技術組織のサポート的なものだったり、
そういった支援をたまーにしています。

あとはOSSやプライベートなサービスの開発をしていて、
適材適所で複数言語を使って開発するスタイルの人です。

将来的にそういう働き方をしたいとかという人は何かしらのヒントになるかもしれないし、
ならないかもしれません。

最近ですとわりと多い働き方だったりそういうエンジニアのスタイルの人です?

エンジニアの将来的なキャリア

大体は3つくらいに分割されるかと思います。

  • 技術にフォーカスし、専門的な知識を持って課題解決をするタイプ
  • プロダクト開発だったり事業などで技術関連分野をうまく進められるように開発組織デザインだったり、その周辺の問題解決にフォーカスするタイプ、
  • 技術とビジネスを結びつけるための戦略だったり事業だったりを考えて実行するタイプ

2つに分けられなくもないんですが、3つにしてみました
これには深い意味はありません。

上から一般的に認識されているカテゴライズをするとスペシャリストタイプ、
VPoEタイプ、CTOタイプみたいな具合かなと思いますが、
組織によっては役割が違う場合があります。

スペシャリストタイプがCTOだったりすることもあります。

これは組織によって求められる役割が異なっていたりしますので、こうあるべきというのはありませんが、
どのタイプもある程度技術に精通し、ある程度ビジネス的な要素や、長期的な事業展開などを理解して
それを落とし込む力が必要(というか必須)になります。

技術を理解していない方がイニシアチブを握っていると、
開発組織の最適化などが難しい場合が多々あります。

ただしこの技術というのも、10年前の技術なら知っている、だったり
新しいものは古いものの焼き増しでしょ?新しいのは覚えなくてもいい、
という思考になるのも危険です。

逆に新しいものしか知らない、古いものは不要なのでいらない、というのも危険です。

AWSなどのクラウドネイティブな環境の場合は、通常さほど苦労はしないと思いますが、
やはり万が一の想定外の出来事が起きた時に判断と意思決定、指示ができるか、というのが求められます。

想定外のトラブルが起きた時に技術的な解決方法が全くわからない、それはクラウドの分野でないのでわからない、
もしくは事業をサポートする機能が要求された時にクラウドで提供されていないものなのでできない、
といった判断をすることは致命的な事態につながるケースが多々あります。

とはいえ必ずしも全ての分野において精通している必要はありませんし、それは不可能だと思います。

例えばkaggleなどのコンペに頻繁に参加して、その傍らモダンなフロントエンド技術を追いながら改善・開発をし   データエンジニア的にビッグデータに対応できる様にHDFSやGCPなどの連携部分の設計やミドルウェアの構築、
クラスタ管理と実装をしてマーケティング支援をサポート、
マイクロサービスアーキテクチャ化のためにドメインモデルの作成を行いながら
スクラムマスターをしてWebアプリケーションのアーキテクチャ設計と分散処理実装と・・・

かなり広い範囲となってしまいますが、
一人でこれらをやりながら経営との技術組織をうまく運用するのは不可能です。
(何年もかけて良いのであれば別ですが・・)

ただ細部まで把握していなくても、ある程度の昔と今、これからの技術を知り、
長期的な事業展開に合わせてどの様なものを選択すべきか、みたいな判断ができる様に常にアンテナを貼り、
どういうものか知っておく必要はあります。
もしくは自分がわからない領域に関しては、その分野に精通したメンバーを採用または育成するなどの動きも自身で考え
それができるように事前に動いておくなどの行動力がプラスになります。

責務の範囲だったりがテックリード、VPoE、CTOで異なったりはしますが、
みているところは大体同じでフォーカスするところが異なる、というのはわかると思います。
勘所みたいなところですね。

一人しかいない場合は一人である程度この役割を担うことになり、
役割分割できる場合はそれぞれがそれぞれの領域にフォーカスしますが、
どちらにしても事業展開も技術においても、組織についてもどれかしかしらない、どれかしかやらない、
というわけにはいかないわけにはいきません。

その会社における最適な技術戦略は中長期のビジネス展開から考える方が良いですし、
それを実現するための開発組織はどうしていくか、
スキルが足りない!だったりそれを実現するためにロードマップを経営層と判断していく、
みたいなのは濃淡はありますがついてきます。

将来を見据えてこの辺りを鍛えていく様なマインドだったり、行動をしていくといいかもしれません。

ということで本題に。

どういうことを意識していくといいのか、
自分流でやってきたことは
スキル習得に関しては日々の鍛錬を怠らないことは当たり前として
(自分の場合は開発における技術習得はプライベートな開発が7割くらい占めてます)、
必ずしもゴールとするキャリアのためにスキルをつけるということはなく、
あくまで日々の鍛錬などで(そうでなくても良いですが)習得したスキルを業務などにフィードバックして
改善を重ねた結果そういった役割が返ってくるもの、
という(自分は意識していないんですが)マインドでした。

もちろんそれらを開発にフィードバックするにはチームメンバーの’教育だったり
文化を作っていくなどもセットで必要になりますので、
リーダーシップは必須になります。
大体パワーを使う物事を進めるのは大変で、嫌がる人もおおいと思いますが
ここを自主的に巻き込んで進められる、というのは必須です。

そうでなければ、ただ意見を言うけど何もしない人、ということになってしまいます。

というのも開発や組織やビジネスもそうですが、
一度決めたらここからやり方や方針を変えない、というのはないわけです。

仮に一度決めたらもう変えない、という選択をした場合は長期的な目線でみると
あるところまではルールになどに従って進んでいけば良いですが、
時代とともに開発や組織やビジネスのやり方も最適解も変わっていくためこれに対応しなければなりません。

なので変えていく力、というのは必要なわけです。

この調子で行くと本を書けるくらいの量になっていくので、このへんで・・

学びは大事

まずは関心を持っている技術やその周辺のものをより理解するためには学習が必須です。

プライベートで勉強するの?と業務時間だけでいいとか最近だとそういう話もありますが、
自身のキャリアだったりそういう面でやるかやらないかを選択すると良いと思います。

(文脈によってコードを書いたりするのか、ミドルウェアを試すのかみたいな言葉が混ざってますが適当に読み替えてください)

自分流に昔やってきたことは、
今だとSNSなどで知見をもつ様々なエンジニアの方々の会話が見えたりそこに参加する、ということが容易ですが、
そこの会話を理解したり参加したり記事を読むには、
その人たちが話している内容を理解できる様に知識をつける必要があります。

書籍を読んだり、国内外のエンジニアの方々の記事などを読んでとにかく知識をつけます。

ここにはもちろん開発手法や開発スタイルの話、実装や設計のパターンやそれらを包括したソフトウェアアーキテクチャだったり、
それらをサポートするインフラだったり多岐にわたりますが、
そのキーワードの背景あるものを理解するためにはたくさんの知識が必要になります。
なのでたくさん読んだり知識をつけることを意識します。

ただこの本を読んだり知識をつける、ということですが
モノにもよりますがほとんどの場合において

読むだけということは、
ミュージシャンの教則本を読むだけでは
ギターが弾けないのと同じ
知識だけがある状態は意味がない

ということです。

なので読んだ内容をもとに実際に手を動かして作ってみたり
進めることで疑問に思うことが出てきたらさらに調べます。

そういったことを日常的に試行錯誤するわけです。

これを業務時間でやると、いつまで経っても成果物が上がってこない、仕事が終わらない、
という状態になってしまいます。
ある程度は割り切って仕事のコードとそれ以外のコードみたいな切り替えは必要です。
(そもそも成果物があってお金を稼いでるので、それができないのは本末転倒です)

これで本で読んだ実践に近い知識がついてきます。
それを元に少しずつ開発組織だったり業務に反映していくといいでしょう。

ただ成功する手法だけでなく、いろんなものを想定して異常(エラー)が起きた時にどうリカバリーするか、
そもそも自分で対応できるのか、などの側面も必要です。

これも合わせて日々鍛錬していくことで自分の自信にもつながりますし、
なによりもエラーや異常系の処理だったりと戦える様になって初めて習得したと言えます。

新しい言語なんかを導入する場合はまさにそうですね。
導入だけしてエラー発生!でも直し方も対応方法もわからない、だと厳しいわけです。

とここまでくるとSNSで登場する著名な方々の会話がある程度理解できたり参加することができるようになります。

あるところはわからなくても、不十分でも問題ないです。
足りないところはきっと教えてくれます。

歩みの中の成長ポイント

初学者のうちはとにかく今あるものをある程度使いこなせる様になるのが必要です。

が、経験は関係なく成長につながるものは

とにかくたくさん失敗すること、です。

学びのところにもありますが
とにかく業務外で何回もコードを書いたりミドルウェアの検証したりし、
とにかくダウンさせてそこからの復旧方法だったり、バグへの対処方法を身につけます。

そして実際の業務にフィードバックすると、さらに想定外の物事が起きます。

それはサービス停止につながるかもしれないし、軽微なエラーかもしれないし
なにかしらクリティカルな事象につながるかもしれません(個人情報とか機密情報とかそういうのはダメですよ!)

が、ビビらずにとにかく失敗するものだと思っておきましょう。
リカバリーできてさらに習得できるわけです。

なので、ある人にとっては「あいつは失敗ばかりで全然ダメ」だったり、
過去に一緒に仕事していた人たちは数年前の姿しか知らないので「あいつはイマイチだよ」
という様に思われてるかもしれません。 まぁ実際に自分はそうでしょう。

でもそれは失敗を重ねた結果、成長につながるわけです。

一緒に働く人に感謝しつつ、胸を張って失敗していきましょう。
同じ失敗は繰り返さないようにしていくだけです。

それに最初から完璧を目指すことは不可能です。
すこしでも状況が変わったら変更し辛い作りと評価されたり、不必要に複雑だと思われたり
いろいろあるでしょう。

知識量の違いだったりそれぞれがもってる考え方や手法は様々なので、
よっぽどなケース以外(例えばハードウェア開発をして一度しかコードを変更できない、とか)は100%を目指したり
変に気負う必要はありません。

失敗できない環境だったら・・?それはあまりハッピーなところではないかもしれません。

もし自身がCTOやテックリードやVPoEになった場合は、
失敗を享受する文化だったり組織を作るといいでしょう。

それまでに自身で様々な経験をしているはずです。
みんなが失敗しようともリカバリーすることはできます。

とにかく失敗は大事ということです。

技術的なアンテナ

SNSなどで新しい技術だったりの情報収集はできますが、
やってみた、という例から正確な情報を得ることはできません。

やはり失敗例やうまくいかないケースの方が価値があります。

この辺りは勉強会やカンファレンスなどで発表した方に質問するといいです。
発表時は大体うまくいった話をしますが、同時に失敗した経験も数多くあるはずです。

このあたりを聞き出したり耳を傾けることで、 新しい手法は取り入れるべきか否か、もしくは一部利用できるかなどを判断できるようになります。

もちろんSNSで直接絡んだりすることもできます。

そういった人たちと接点をもったりすることで自分の知識だけでは不十分だったことや、
さらに理解しなければいけないものなどが見つかります。

もちろん手法だけではなく、自身の所属する会社の中長期のビジネス展開を頭の片隅に入れつつ、
ああ、ここならこの技術いれれそうだなとか、そういう思考を意識しましょう。

この技術を使いたいからここをこう変える!ではありません。
技術ファースト的な発想ではなく、あくまで課題解決に対する自身の引き出しとして使います。

そういう意識を養うことでこれから必要そうな技術だったり方法論が見えてきます。
*もちろんビジネス展開ではなく、自身のブランディング戦略みたいな形でもいいでしょう。

技術や手法の見た目だけに囚われて判断してしまったら、
まだ未熟だ、ということです。
目に見える範囲だけではなく、失敗例と、なぜそれを選ぶのか、
みたいなところを意識しておきましょう。

コンフォートゾーンから抜け出すこと

いわゆる心地いいゾーンですが、
ある程度開発できるようになってきたりスキルがつくと、
自分の得意な言語や手法でなんでも解決したくなってきます。
自分もそうでした。

なんでもPHPでやるとか、ちょっと難しいかもしれない手法で解決できるけど、
それはわからんので自分の知ってる範囲だけでやる、という具合です。

世の中には似たケースでいくつかの技術的な選択肢があったりします。

例えばRDBMSでLIKEとかできるけど、スケーラビリティ意識したらSolrとかElasticsearchな訳ですが、
RDBMSの知識そのままでは利用できないため、クラスタの設計だったりサイジングだったり、
インデックスの作り方なども新たに覚えなければなりません。

つまり今はできないけど少しの努力と少し背伸びをすればできるようなものであれば、
心地いいゾーンから少しはみ出て範囲を広げる、ということを意識するようにします。

もちろん全方向にというわけにはいきません。

それこそ冒頭の

kaggleなどのコンペに頻繁に参加して...

みたいになります。

徐々に広げた結果そうなるのは問題ないですが、
成長途中にありとあらゆる分野に伸ばすのは大変労力もかかりますし、
習得が困難になります。

ある程度理解してやるけど、ここは最新追うとか第一線目指す、みたいなのはやらない!
という領域を決めることが大事です。
(自分の場合はフロントエンド技術がそうなんですが)

このゾーンを抜けるためにも学びが必要ということですね。

逆にこのゾーンを抜けない、ずっとそこでいい、という選択をすることもあります。
もちろんこれはその人の特性だったり性格もあるのでそれも問題ありません。

ただCTOやテックリードやVPoE、またはマネージャを目指すみたいな場合はどうでしょう?

今はできないけどすこし背伸びすればできそうだな、というものをつかわないと解決できない要件が現れた時、
例えば数億ユーザーが使うサービスに成長した場合にレコメンデーションなどデータを活用したものが出てくるわけですが、
(DXなどにもよくありますね)

分散ファイルストレージの知識やSparkだとかBeamみたいな知識が必要になるわけです。
少しの労力と学習で使うことができますが、
運用となると多少難しいのでそれなりの時間が必要になります。

ですが、頑張ればできそうです。

(想像しにくいかもしれませんが)

というケースおいて、無理っすできないっす、だったり、
組織の中でこういう手法があるから試してみたら?的なアドバイスをすることができなかったり
推進するようなことが全くできなかった場合にどうなるでしょう。

その分ビジネスの変化ができなくなるわけですからサービスの成長だったり、
それに伴うなにかしらの大きな変更だったりのチャンスがなくなるわけです。

会社の成長を止めてしまうようなやり方を推進しすぎると衰退を早めてしまうものです。
(やりすぎはだめですけど)

いいバランスを取れるようにするためには
適切にコンフォートゾーンを抜け出すのは大事だと思います。

抜け出すヒントとして、自社にそういう環境がなかったら・・・

他社エンジニアと交流を持ってそういう知識をわけてもらったりするような努力は必要です。

もちろん自社でできない場合はアウトソーシングという選択肢がありますが、
ある程度のことを理解していないと提案されてもまったく理解できず、
ビジネス側に反映しようと思っても全然マッチしなかったり、
想像と違ったものが出来上がっていた、などにつながります。

これは危険!

とにかく現時点の自分の知識だけで満足せずに、井の中の蛙にならないように
定期的に自身に刺激を与えて範囲を広げる努力をしましょう。

きっとプラスにしかなりません。

経験や学びをアウトプット

これは必須ではありませんが、習得のためのPDCAだったり、
自分の糧とするには有用だったりするものですが、

カンファレンスやブログなりでアウトプットを継続的にして、
いろんな人の指摘を受けたり、
もしくは受けないように自分の知識とアウトプットが間違っていないtか確認しながら磨いていく、

を重ねることでさらにさらにプラスになります。

ここで得た経験をチームや組織にフィードバックしたり、
自らそれを材料に組織をリードしていく人になっていったりという成長につながります。

自分に自信をもつ、という点でもプラスになるでしょう。

ただアウトプットしたから偉いとかそういうことはありません。

自身の考え方だったりを整理することに価値があると思っています。
アウトプットしたついでに参加者のみなさんと飲み会ができるとか最高ですね
(今はできませんが)

少しだけ意識しておくといいかもしれないこと

繰り返しになりますが、時間外で勉強しろという話ではありませんが、
自身が何もしてない時に他のエンジニアの誰かは常に成長している、ということは事実です。

どういうキャリアを歩んでいくかを照らし合わせながら、
そんな日々成長している人たちに追いつくか追いつかないか、
もしくは著名な人たちと会話できるようなエンジニアになるのか、
などを意識してみるといいかもしれません。

もし具体的な姿などが想像できなければロールモデルを探すのが良いでしょう。

社内外関係なくそうしたエンジニアの方を探してみたり想像してみたりしましょう。

もちろん表向きの姿だけではなく、どういう経験をしてそうなったのか、なども参考にできるはずです。

そうした結果エンジニアとしての歩んだ先が見えてくるはずです。
キャリアのためにやるぞ!と意気込んでもこうしたことは見えてこないかもしれません。

ここに書いてあることはあくまで自分の体験などを掻い摘んで書いてあるだけです。

背景のはもっと様々なことがありますが、参考にできそうなところは取り入れてみるといいかもしれません。

もしみなさんの上司だったりにあたる人たちが、
組織や技術やビジネスなどに対する姿勢、リーダーシップが大きく欠けている人たちだったら・・?
それはチャンスかもしれません(?)

頑張っていきましょう

長文おわり

DDD入門とLaravel

DDDとLaravelについて

先日、主催しているコミュニティで久しぶりに20分トークさせてもらいました。
タイトルはこのエントリと同じようなもので DDD入門とLaravelアプリケーション です。

laravel-meetup-tokyo.connpass.com

資料だけ公開してもミスリードになりそうなため、公開はしませんが内容を詳しく書いていきます。

あくまで対象はDDDに入門してみたい、という方やDDDって実装パターンでしょ、と思っている方向けです。
すでに実践、導入している方向けではありませんのであしからず。
複雑にならないようにわかりやすい文言だけで書いていますが、そうじゃないかもしれません。

前置き

タイトルからわかるようにLaravelなどを使ってある程度のOOP的なテクニックを学び、
ある程度不自由なく実装ができるようになると
アプリケーション設計などのソフトウェア的な探究心が強くなります。

そこでDDD(ドメイン駆動設計) というキーワードを目にすることが多くなり、
そこで実装例としてあげられるレイヤードアーキテクチャやさまざまなOOP的なテクニックに興味が沸き、
習得しようと多くの方が思うわけですが、
このドメイン駆動設計という言葉が一人歩きをしてしまい、言葉のインパクトや
エリックエヴァンスの書籍のイメージ、ネット上にあるさまざまな記事から実装パターンだけにフォーカスしてしまい、
DDDパターンで実装LaravelでDDDを実装
またはリポジトリパターンを取り入れればDDDというような話がよく出てきます。

必ずしもLaravelというわけではありませんが、世間一般的なアプリケーションフレームワークだと思ってください。

これは全く本質的なことではなく、
間違った理解で止まってしまうのは非常にもったいないポイントです。

DDDが全てにおいてずば抜けて素晴らしいもの、というわけではありませんが、
どういうものなのか、というのは知っておく必要があります。
という背景があるなかで、少しでも入門のための知識と間違った理解をしないようにということで。

DDDってよく聞くけどなに?

まずはDDDについてよく聞かれる質問だったり、ネット上でよくみるものですが
以下のようなものがあります。

  • DDDってアーキテクチャで層に分かれていればいいんだよ!
  • DDDで実装するんだけどデータベース肥大化
対策どうやるの?
  • MVCでDDDはできないのでは?
  • DDDで実装したけど全然楽になりません

上記のような内容をよく聞かれたりしますが、
これらは実はDDDについての認識が間違っているために出てくる質問です。

まずDDDとは、日本語でドメイン駆動設計と訳されるものですが、
早い話、実装パターンやアーキテクチャといったレイヤの話ではなく、
ソフトウェアの開発スタイルの一つ です。

問題解決領域つまりドメインに対してどう分析していくか、というところが主になるものです。

では、この問題解決領域とは何を指しているのでしょうか?
ビジネスロジックを指す、という方もいますが
開発者のほとんどの方が会社に所属していたり、
もしくは特定のサービス/アプリケーションの開発に参加しているフリーランスの方や
SIなどの形態だったりと様々だったりしますが、
会社(所属していたり派遣だったりで参加している企業) は世の中に対してどういう問題解決を行いたいか、
という大きな目標だったり思想が必ずあると思います。

たとえば自分が今所属している会社でもあるスターフェスティバルでは

スターフェスティバルは、
「ごちそうで 人々を より 幸せに」を企業理念に掲げ、レストランの中食ビジネス参入支援、
および、フードデリバリー事業を展開する会社です。
レストランや製造工場などの製造パートナー、また、配送パートナーと連携し、
「製造」以外の部分にあたる、「商品開発」「販売促進」「受注」「配達」「料金回収」 までのすべてを引き受けるビジネスモデル

とありますが、
手短にいうと、これが解決したい問題領域(ドメイン)であり、
これに基づいてサービスがいくつか展開、その中にアプリケーションが存在します。
問題解決をするにあたって、これらを 実現するために登場する利害関係者や概念などを整理し、
分析して共通で理解できる概念モデルを導き出す、そしてそれらを起点にして開発に入っていく
わけです。

こういった話は例えば半期に一度全社員集会などがあって社長などから共有されたり、
ビジネスチームから聞くこともあるでしょう。
実はそのタイミングで、「長い話をきくのはウザいな、どうでもいいや」的な姿勢でいると
DDDを実践していくためのヒントや概念が欠落してしまうので、
しっかりと聞き、理解することが非常に大事なわけです。
逆にそこに興味が湧かないと入門・実践していくのが難しい、ともいえます。

DDDに含まれないもの

人によっては違う、というものもあるかもしれませんが、
多くの企業などが掲げる問題の中に非機能要件はありません。
つまり実装言語やフレームワーク、データストレージ、キャッシュやアプリケーションアーキテクチャ
といったものは問題解決領域外のものになります。
解決のために開発者が利用するものではありますが、これらは通常含まれません。

が、例外として言語やミドルウェアやソフトウェア自体をサービスとして提供していたり
それらの領域で活動している団体・企業は除きます(Apache Foundationとか)

DDDに入門するならば

どういうものかざっくりとした概念的なものは多少理解できたと思いますが、
実際に取り組んでいくときの考え方などは後述するとして・・
まずは、どんな言語とどんなフレームワーク、データベースを使って、どういうアーキテクチャで実装するか、
という考えをまずは捨てましょう。

分析したものを実装に落とし込みやすい実装パターンや、言語というのは確かにありますが、
これは本質的なものではありません。

チームでできる範囲の実装方法を採用してもいいでしょう。
エリックエヴァンスの書籍をひたすら読み、
自分たちのアプリケーションをそれに寄せすぎるのも
ドメイン駆動とは遠くなる可能性があります。
(内容を理解してからの話ですよ。まずは読んでおきましょう)

多くの方が経験あると思いますが、新しい機能やサービスの概念などの話をきくと
データベースがここにこういう状態である、だったりこういうテーブル構造にして、
APIがこういうレスポンスで、というように実装するときにどうするか、
ということが頭に浮かぶと思います。

全く考えないというのは難しいかもしれませんが、ここから始まってしまうと
ドメイン駆動ではなく、データベースファーストやデータベース駆動設計だったり、
もしくは他の概念だったりが先行してドメインが主ではなくなっていきます。

なので浮んだとしても全くの別物だと認識しておいた方がいいでしょう。
ここが一番難しいポイントかもしれませんが。

そして書籍やネット上の記事などで目にすることが多いと思いますが、
会社のビジネスモデルやサービス仕様、
カスタマーサービスやビジネスチームの話す内容を理解することです。
アプリケーションレベルの仕様ではなく、マネタイズなども含めて実現したいことを知るのが良いでしょう。

これを実践する方法はいくつでもあります。
カスタマーサービスに加わって業務を体験したり話をよく聞いたり、

企画会議に参加したりといったことができます。

DDDを実践していくには

時代の流れで利用者などが変わっていき課題解決対象が多少変わったり、
当然退職や編成などでチーム構成が変わるため、継続的にコミュニケーションや分析を行う必要があります。
一度やればいい、というものではありません。

分析した結果実装、というフェーズになるわけですが
残念ながらいくら本やネット上で様々記事を読んでも、完璧にコードなどにうまく反映できるものではありません。
当然みなさんの所属する会社のビジネスモデルは本や記事にある例で済む単純なものではありませんし、
ビジネスモデルに変化があれば変わっていきます。

コードに落とし込むときに、実はこうではないか?ということも日常茶飯事であります。
とにかくトライアンドエラーを繰り返し、洗練させていくしかありません。
これはどんな達人であっても絶対にそうだと思います。
一度作ったら終わり、というものでもありません。

入門して実践していくには
とにかく関わるチーム全体で企業理念などの概念からドメインモデルを導き出し、
全員で課題に対して同じ認識と同じ言葉で会話できるようになること
です。

やらないように意識したいこと

これらの概念の分析などをすっ飛ばして、実装パターンだけに飛びついてしまうということは
概念にそったクラス設計やカプセル化にならず、実装者都合のものになってしまいます。
抽象レイヤが導入されたことによってテストがしやすくなる、かもしれませんが、
これはDDDではなくソフトウェアの一般的な問題解決方法の一つを採用しただけにすぎません。

所謂軽量DDDと呼ばれるもので、ビジネスモデルにも沿っておらず、
インターネットなどで見聞きしたカッコ良さそうなパターンを採用し、
短期的にはテストが書きやすいなどのメリットはあるかもしれませんが、
チームでレイヤの分け方くらいしか共通認識がないため簡単に破綻してしまいます。
ドメイン貧血症などもあります。

とはいえこれじゃダメだ!と体験することができるのもこの軽量DDDと言われるものでしょう。
失敗して得ることの方が大きいです。
おそらく5回くらい失敗すれば身を持って学ぶことができます。

こうした実装をすることで発生してしまう例を、境界付けられたコンテキストで紹介しましょう。

境界付けられたコンテキスト

言葉だけで見るとなかなか難解な境界付けられたコンテキストですが、
理解してしまえば怖くはありません。
目の前にある複雑なものも分析がしやすくなるでしょう。

あまりよい例えではありませんが、わかりやすくいうと
これがわからないと分析して導き出す主役たち、
つまりエンティティやバリューオブジェクトを区別して見つけることができないと思います。

ユビキタス言語などにも通じますが、
みなさんは同じ言葉を使っているようで
微妙にあの人とは認識が合わない
そんな経験ありませんか?

例えば昔の話などで構いませんが、友人たちが昨晩TVでみたアニメの話をしていたとします。
自分は原作を読んでいて大体の話を知っているわけです。
このためTVで観なくても大体のことは理解しています。
友人たちの会話に参加して問題なくその話題について話ができました。
ただどうやら若干原作と設定や背景が異なるものがあるようです。
自分自身は原作の知識で話、友人たちはTVで観た知識で話をして通じることは通じますが若干の違和感があります。
後日TVなどで観てみると同じ名前でちょっと違う、実は違うキャラだったことが判明しました。

アニメなどはほとんど観ないのでこうしたケースはあまりないのかもしれませんが、 こういった些細なことは日常にたくさんあります。

仕事でもあると思います。
例えばユーザーについて話しているビジネスチームがいて、
話に参加したところ、エンジニアとしてはDBに存在するユーザーの行のことを想像して聞いていると、
実は特定のステータス(有料課金者だったり)を持っている人のことを指していた、だったり
ある言葉がエンジニアチームと共通したキーワードがありますが、
違う意味で他のチームが使っていてそれをdisるみたいなことをして、意味が違っていてもずっとその言葉を使ってしまう、
などもあるかと思います。

繰り返しになりますが、これらは全て言葉自体が同じでも指しているものが異なるもので、
どこか似ているかもしれないけど、別物として認識しておかなければならないもの、となります。
これは違う、となる境界線がどこかにあるはずで、それらを見つけることが分析のポイントになるわけです。
これが境界付けられたコンテキストです。

実装前に近い段階の話を例にすると、
EC的な通信販売を扱うサービスの開発に参加し、その中で商品という言葉があったとします。

あるビジネスチームはこの商品という言葉を構成するものとして

  • 商品名

  • 扱っている店舗
  • 値段

  • 販売期間

をあげたとします。
これをエンジニア側が一つのクラスとして表現しようとします。

次に配送などを担当しているチームと話します。
このチームはどうやら

  • 商品名
  • 
個数
  • 配送先

を商品という言葉に内包しているようです。

商品を提供している店舗に話を聞いてみましょう。
ここではどうやら商品という言葉は以下のようでした。

  • 商品名

  • 値段

これらは共通した言葉なのでそのままクラスに落とし込んだとします。

  • productName / 商品名
  • storeName / 扱っている店舗
  • price / 値段

  • salesPeriod / 販売期間
  • quantity / 個数
  • shippingAddress / 配送先

全ての要求を満たす商品クラスが完成しました!

ちょっと待って!!!

共通化されましたが、果たしてどの商品のことを指しているのでしょうか?
全てを指しているのであれば、さらに違うチームと会話をして新たな要素が追加されたら
ここにも追加されるのでしょうか?

こうなってしまうと実装レベルでも障壁が生まれてきます。
ある一方では特定の概念を表現できますが、
ある一方では不要な要素がたくさんあり、表現するためには無理して使わなければなりません。
こうなってしまうとおそらくセッターだらけであったりnullableな要素ばかりだったり・・。
所謂神クラスなどと何も変わりません。

これは境界付けられたコンテキストについて認識をせずに、
実装上都合が良いからと共通化してしまうことで発生してしまうよくある例です。
(自分も当然あります)

これらを防ぐには、同じ言葉でも少し意味が違う、というものに沿って
別なものとして表現した方が問題にたいしての表現が良くなります。

これはアプリケーションの特定箇所だけの話ではなく、
アプリケーション全体を俯瞰したときにいろんなところで見つかるものです。
利害関係者を知り、様々な体験と分析をすることでこれらを見つけて落とし込んでいくのが必要不可欠なわけです。

当然上記の例にも実は不十分な点があります。
値段が差すものは税込なのか?税抜なのか?
配送先は都道府県から?それとも市内?区内?
といった些細なものに見えて実はインパクトがあるものだったりが隠されています。

これらを見つけるのは特定の開発者だけ、というレベルの話ではないことがわかると思います。
(例外として一人で企画・運営・マネタイズ・開発などをしてるよ!というケースはありますが)

各チームのバックグラウンドに基づく重要な知識が
隠されていることが多々あるので、これらを見つけ出すのもDDDの一環です。

当然これらはみなさんのアプリケーションによって主としてみるところが変わりますので、
明確にコレさえやれば完璧!問題なし!みたいなものは存在しません。

例えばお酒が好きな人とビールが好きな人を考えても、それぞれ主にする場所が変わるわけです。
極端な話でお酒が好きな人は、特定の好きなお酒というよりも酔えれば良い!というアルコール度数を重要視するかもしれませんし、
ビールが好きな人はアルコール度数よりも、どこの国で作られたIPAなのか、ということを重要視しているかもしれません。
一般的にみると酒でも違うわけです。

こうしたことから書籍などで完璧に導き出せるものがないというのはわかると思います。
どんな名著を読んでその例をそのまま自身が携わっているアプリケーションにそのまま適用しても
(例えばEメールについて本にこう書かれていたのでこうだ!みたいな)
何にもならず、自分たちの注力外の概念が無理矢理結合されるだけとなります。
実装パターンだけ追っても複雑さの解決はされないのです。

エンティティ、モデル

分析の話ばかりで飽きたところに少しだけ開発に関する知識の話をしましょう。
DDDについて少し理解したり、書籍を読んだりすると出てくる言葉にエンティティとモデルという言葉があります。

実はこれは大きくミスリードしてしまう要素でもあり、
開発における境界付けられたコンテキストの代表みたいなものです。

タイトルにもあるLaravelだと、例えばデータベースアクセスを表現する「モデル」と呼ばれるものがあります。
実はこのモデルという言葉はさまざまな意味があります。

ドメインモデル、データモデル、Eloquent
モデル(他にもありますよ!)などをモデルと指すことが多いですが、
どれも違うものを指しています。

どちらか片方の知識だけで片付けてしまうのは、これまでの文章にあったように
実は大変危険で複雑化してしまう原因になります。
それぞれのモデルを正しく理解して一緒に考えるのは止めるのをお勧めします。

例えばDoctrineやtsのtypeORMでもありますが、エンティティ。

DDDにおけるエンティティとは、

ドメインにおける識別しなければいけない特別な存在のことでで、
データモデルにおけるエンティティ
とは、
情報収集する対象のこと
、データベースの行に近いものではありますがそれ自体ではありません。

まったく同じ言葉ですが全くの別物なわけです。
言葉が同じということでエンティティをひとまとめにしてしまうと、
簡単にデータベースなどの入出力と結合してしまったものになってしまいます。

同時にモデルという言葉が同じであっても全く意味が異なっています。
どちらかの知識に寄せて片付けてしまうのは大きな誤解を産んでしまいますので注意が必要です。

これまで述べてきた内容にもありますが、
データベースなどの知識や考えを排除して考えましょう。
データの入出力やAPIのコールなど現実的な事柄がいくつもあるのは当然なんですが、
これらについては後で考えましょう。
むしろ実装でも一番最後くらいでちょうどいいです。

とはいえ実装に落とし込む方法を知りたいんだ・・

この話をしてしまうとそこにフォーカスしてしまうので、
複雑なものにたいする具体的な実装コードサンプルを載せるということはしませんが、
ヒントのような形で雑に載せておきます。

Laravelの機能が楽なので使いたいんですがどうしたら?

使いましょう。
ただDDD入門についてこれまで書いたようにきちんと分析し、
その分析した世界に極力それ以外の知識、つまり非機能要件を持ち込まない工夫が必要です。
とはいえ高度な抽象化やかっこいいパターンを無理に真似する必要はありません。
便利な機能を使ったメソッドなどをインターフェースに含めるなどするだけで問題ないです。

Eloquent使ってもいいの?

どうぞ!
他上記と同じ

リポジトリパターンが巨大で・・

データベースのテーブルなどと対になった作りになっていませんか?
抽象レイヤではなく、まずは特別視しなければならない存在の塊を操作するもの、
そしてこの特別な存在が微妙に違うのであればそれを操作するリポジトリも分けてみましょう。
仕様パターンなどの解決方法がありますが、難しければ無理に取り入れなくても良いでしょう。
まずは少しでも中身がちがう存在を操作するものが混ざっているのであれば分けましょう。

DDDをもとに導かれたコードを
引き継いだがわからない

分析したチームなどが存在しないのであれば、再度分析をしてください。
今のチームの認識とそのときのチームの認識が異なっていて当たり前です。

分析した結果、今の概念と異なるのであれば
新しい知識に基づいて実装し直すなど適度なリファクタリングを重ねましょう。

全てでDDDで題材にされるパターンを用いなければなりませんか?

他のシステムですでにドメインが表現されているのであれば必要ありません。
特別なロジックもなければ、取得して適当に成形するだけで十分、
といった場合はDAO、DTOなど用いましょう。

繰り返しになりますが、なにがビジネスロジックでみたいな判断基準は
みなさんの分析結果や解決しないといけないものは何か、
などの知識基準の話でこういうのは実装しなくていい、みたいな答えはありません。
かならずチームなどで導き出した共通認識の中で判断してください。

最後に

いくつかポイントなどを書きましたが、
この内容のうちどれかは必ずやっていることだと思います。
例えば開発チームとビジネスチームで話を聞く、みたいなことも良くあると思いますし、
アジャイルなどを採用しているところはまさにそういう毎日だと思いますし、
社内ツールを作っている方などはカスタマーサポートチームと話をするということは
日常的なことでしょう。
これらは全て自分たちのアプリケーションをどうやって要件に合わせていくか、
というところがベースになっているはずです。

つまりDDDという名前がついていますが、
ある程度のことは意識せずに大体みなさんやっているわけです。
それをどこまで認識合わせして落とし込んでいくか、というところを主として開発を進めているわけです。
分析の精度を上げるための手法だったり、会話の認識合わせ方法だったりはいくつかありますので、
チームだったり個人に合わせて導入するといいでしょう。
この辺りは非常に有益な本がたくさんありますので是非読んでください。

会話だったり、多少の抽象的なものの考え方だったりに慣れやセンス的なものが必要だったりはします。
この辺りは開発から少し遠いと感じるかもしれませんが、ロジカルシンキングなどの本を読むといいでしょう。

冒頭の例に挙げたいくつかの質問は全くの違うレイヤの話で
DDDの話ではないということが少しでもわかっていただければ幸いです。
(冒頭の質問に対する答えはネット上に素晴らしい記事がたくさんありますのでそちらを参考に)

  • 実装方法起点ではなくビジネス、利害関係者を起点に表現する
  • データの入出力を考えるのは一番最後、分析時などには考えない
  • 流行りモノではなく当たり前なことをやるだけ

こうしたことを意識しておけば入門して実践することがおそらくできるはずです。
(実装方法は別の話ですよ)

DDDに限らず、自身のチーム合わせた開発スタイルを取り入れて問題解決に取り組んでいきましょう。

実践ドメイン駆動設計

実践ドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

  • 作者:Eric Evans
  • 発売日: 2013/11/20
  • メディア: Kindle版

PHPを使ってEvent Sourcing + CQRSをざっくり理解してみよう(Laravel)

これはさりげなく スターフェスティバル Advent Calendar 2020の20日目です。

PHPカンファレンス2020

2019年は登壇などを控えて一休みの期間としていたので一年振りくらいの
と登壇となりました。

発表の内容としてはここ3、4年注力しているデータ処理まわりから、
PHPにおけるWebアプリケーションなどでも活用することができる題材を取り上げてお話させていただきました。

要するに事業に関わっている開発は年々要件も複雑になっていき、
問題解決するためにはいろんな手法があるけど、きちんと分析して
開発しやすいよう、フレームワークにべったり依存してつくるのではなく、
数年先を見越してつくったり、改善する方法の一つにES+CQRSもありますよ、という話です。

お話したように、全てのアプリケーションでペイできるものではありませんし、
ある程度大きな規模だったりある程度複雑な機能だったり、
または周辺サービスや事業自体の構想によって初めて導入するかしないかという話になります。

なんかカッコ良さそうな方法があるから採用しよう!では失敗しますので
よく見極めて導入するのが良いと思います。

さてアドベントカレンダーということもあり、実際に自分自身が手がけてきたものを取り入れて、 ES+CQRSをざっくり理解してみよう ということで概念だけではなく、
PHPのコードも交えて簡単に解説します。

スタフェスではある部分をES+CQRSに置き換える途中だったり、
データ基盤的なものをこれらの手法の発展形ともいえるラムダアーキテクチャなどを用いていろんなものを
作り上げようとしている段階です。

ではざっくりといきましょう。

仮のお話

*ブログか、レビューか、何かそういうものを想像してください。

ユーザーが記事にキーワードを投稿して、頻度の高いキーワードをサジェストしたり、
ワードクラウドみたいなものを実現したいんですよね!
難しいかもしれませんが、キーワードのサジェストはリアルタイムに近いくらいの速度で変えたいです。 もちろんデータ集計や将来的にはレコメンデーションで使いたいです!

すごい雑な内容ですがこんな要望を実現しようという場合に、
キーワードはおそらくタグ的な用途だと思われますが、
記事などのコンテンツと同時にキーワードが投稿されるらしいものというのはざっくりわかります。

おそらくキーワードと、それを投稿したユーザー情報が分ければ良さそうです。
大きくするとキリがないのでここでは例としてユーザーIDだけにします。

ワードクラウドみたいなものやサジェストはどう実現したらよいでしょう?
ワードクラウドみたいなものやサジェスト はどうやら投稿したユーザー向けというわけではなく、
UIやAPIを含めて、不特定多数のユーザー向けの機能なようです。

ユーザーが投稿する時のキーワードとは違う概念になりそう(利用数の概念などが加わります)で、
サジェストはさすがにRDBMSでは無理でしょう。
ElasticsearchやSolrを使えば実現できそうです。

ざっくりとこういう内容だとします。

RDBMSでも近しいことはできなくはないと思いますが、
情報取得にはLIKE検索と集計を多用することになり、アナライザーなどはないため
日本語サジェストに対応するにはかなり厳しいものがあります。

ワードクラウド的なものは集計するだけでできそうですね。
ただし上記のサジェスト、ワードクラウドはユーザーIDは不用そうです。

同じキーワード、という文字でもアプリケーションに関わるユーザーの角度から
多少コンテキストが違うのがわかります。

例としてかなり簡単ですので、ES+CQRSを使うほどでもないですが
どういう風に作っていくか簡単に見ていきましょう。

コマンド実装例

今回の内容のコードは下記で公開しています。

github.com

CQRSについては、取り入れる環境やアプリケーションによって広義の意味だったりすることもありますので、
まずはCQRS Documents by Greg Youngを参照してください。

ベースの考え方としては、副作用のある書き込み処理と副作用のない読み込み処理を分離しましょう。
というものです。
当然DDDとシステムを利用するUI的な問題もともに解決しなければならないため、
スマートUI的な解決方法も取り入れなければなりません。
画面を構成する要素と要件分析は必ずしも一致しません。
(データベースの物理的な設計もデータモデルも異なります。)

このあたりはTask Based User Interface考察の下記の記事もわかりやすいと思います。

qiita.com

現実的にはうまくいろんな要素を取り入れなければ、
アプリケーションの規模によってはパフォーマンス面で深刻なボトルネックがあったり、
多様性のないデータベースなどが溢れてしまいます。

さてこの例のアプリケーションでは、
書き込みとしてはキーワード投稿(通常はブログかなにかの一部ですが、それだけを抜き出したものとして)が、
該当することがわかります。
読み込みとしてはサジェスト、ワードクラウドが該当します。

まずこれらを分割します。

まずはユーザーが投稿するキーワードです。

<?php
declare(strict_types=1);

namespace SampleDomain\Keyword\Entity;

use SampleDomain\User\ValueObject\UserId;

final class Keyword
{
    /**
     * @param UserId $user
     * @param string $word
     */
    public function __construct(
        private UserId $user,
        private string $word
    ) {
    }

    public function getUserId(): UserId
    {
        return $this->user;
    }

    public function getWord(): string
    {
        return $this->word;
    }
}

実際に必要なのはこれだけです。
これがアクションからユースケース(アプリケーションサービス)が実行されます。

<?php
// 省略
    /**
     * キーワードを登録する
     * @LogExceptions()
     * @param int $id
     * @param string $text
     */
    public function register(
        int $id,
        string $text
    ): void {
        $keyword = new Keyword(new UserId($id), $text);
        $this->dbRepository->save($keyword);
    }

書き込みを実行する実装で、簡単に実装できました。
ですが、ここに一つの問題があります。
それは書き込みの処理です。

サジェストがあるので RDBMS(ここではMySQLを使います)とElasticsearchに書き込まなければいけません。
ここで2フェーズコミットの問題が出てきます。

<?php
// 省略
        $this->dbRepository->save($keyword);
        $this->esRepository->save($keyword);

この二つに書き込む場合、MySQLへの書き込みが失敗した場合に
次のElasticsearchへの書き込みを停止することはまだ簡単です。

Elasticsearchが失敗した場合、MySQLへ書き込んだ内容を削除しなければなりません。
この順序を逆さまにした場合でも同じです。

2つとも無事に書き込めたことを確認し、正常終了したものとしてコミット扱いにしなければなりません。
これを回避するにはどれか一つにだけ書き込んで、
バッチ処理か何かでデータを同期してあげれば良さそうです。

ただリアルタイムに近しい頻度で更新して欲しい、という話がありました。
そのあたり要件の調整をしろ、というのもありますが、
そこで止めては今回のサンプルの意味がありません。

<?php
// 省略
    /**
     * キーワードを登録する
     * @LogExceptions()
     * @param int $id
     * @param string $text
     */
    public function register(
        int $id,
        string $text
    ): void {
        $keyword = new Keyword(new UserId($id), $text);
        $this->dbRepository->save($keyword);
        $this->dispatcher->dispatch(new KeywordRegistered($keyword));
    }

リアルタイムに近しい速度を実現させるためにpubsubのメッセージブローカーを使うことにしました。
dispatchを使ってメッセージブローカーへの通知をするように実装してみました。

<?php
declare(strict_types=1);

namespace App\Listeners;

use App\DataAccess\Kafka\KeywordCreatedParameter;
use App\DataAccess\KeywordProducerInterface;
use SampleDomain\Keyword\Event\KeywordRegistered;

class KeywordRegisteredListener
{
    /**
     * @param KeywordProducerInterface $producer
     */
    public function __construct(
        private KeywordProducerInterface $producer
    ) {
    }

    /**
     * @param KeywordRegistered $event
     */
    public function handle(
        KeywordRegistered $event
    ): void {
        $this->producer->add(
            new KeywordCreatedParameter(
                $event->getKeyword()
            )
        );
    }
}

リスナーで受け取ってpublishしているだけです。 これならいけそうです。

とはなりません。
結局、MySQLかElasticsearchのどちらかに書き込んだあとに、
メッセージブローカーへの書き込みが失敗したら戻さなければなりません。
ということで、ここではメッセージブローカーへのpublishに注力すれば良さそうです。

<?php
declare(strict_types=1);

namespace App\AppService;

use Psr\Log\LoggerInterface;
use SampleDomain\Keyword\Event\KeywordRegistered;
use Illuminate\Contracts\Events\Dispatcher;
use SampleDomain\Keyword\Entity\Keyword;
use SampleDomain\User\ValueObject\UserId;
use Ytake\LaravelAspect\Annotation\LogExceptions;

/**
 * Usecase
 */
class KeywordRegistration
{
    /**
     * @param LoggerInterface $logger
     * @param Dispatcher $dispatcher
     */
    public function __construct(
        private LoggerInterface $logger,
        private Dispatcher $dispatcher
    ) {
    }

    /**
     * キーワードを登録する
     * @LogExceptions()
     * @param int $id
     * @param string $text
     */
    public function register(
        int $id,
        string $text
    ): void {
        $keyword = new Keyword(new UserId($id), $text);
        $this->dispatcher->dispatch(new KeywordRegistered($keyword));
        $this->logger->info('publish', ['object' => $keyword]);
    }
}

DebeziumやDynamoDB ストリームを使うことで、この問題を回避することができますが、
ここではどこかの環境に依存していないアプリケーションのアプローチとします。

このアプローチを採用する場合に、当然重要なのがメッセージブローカーに何を使うか、になると思います。
PHPに焦点を当てたこの例では、有力なのはApache Kafkaとなります。
大きな問題としてKafkaへの送信失敗時の挙動です。

PHPの場合、librdkafkaを使ったrdkafkaを利用するわけですが
このlibrdkafkaは通信失敗時のリトライがサポート、
それに加え通信のトランザクションもサポートされています。

<?php
declare(strict_types=1);

namespace App\DataAccess;

use App\DataAccess\Kafka\ParameterInterface;
use App\Foundation\Serializer\SerializerInterface;
use RdKafka\Producer;
use RdKafka\ProducerTopic;
use function is_null;
use const RD_KAFKA_PARTITION_UA;

final class KeywordProducer implements KeywordProducerInterface
{
    /**
     * @param Producer $producer
     * @param ProducerTopic $topic
     * @param SerializerInterface $serializer
     */
    public function __construct(
        private Producer $producer,
        private ProducerTopic $topic,
        private SerializerInterface $serializer
    ) {
    }

    /**
     * to Kafka
     * @param ParameterInterface $parameter
     */
    public function add(
        ParameterInterface $parameter
    ): void {
        $this->producer->initTransactions(10000);
        $this->producer->beginTransaction();
        $this->topic->produce(
            RD_KAFKA_PARTITION_UA,
            0,
            $this->serializer->serialize($parameter->toArray())
        );
        $this->producer->poll(0);
        $error = $this->producer->commitTransaction(10000);
        if (!is_null($error)) {
            throw new \RuntimeException('Kafka Transaction Error.');
        }
    }
}

もちろん選択肢としてApache Pulsarもあるかと思います。
この辺はみなさんのアプリケーションによって最適なものを採用できればいいかと思います。
サンプルコードではjsonで送信していますが、
より強固なアプリケーションにする場合はApache Avroを利用します。
ほかにもKafka Streamsを使うことでメッセージがsubscribeされる前にフィルターしたり(バリデーションなど)、
メッセージの中身によって振り分けたりさまざまなことができます。
この辺りもアプリケーションに合わせて採用するといいでしょう。

これでメッセージブローカーへの送信が確かなものになりました。 このメッセージをサブスクライブしてそのまま補完していけばイベントを再発行することもでき、
遡ることもできます。
またKafka自体にもメッセージを消失させずに30日まで補完する機能がありますので、
その辺りもうまく利用できます。
また手抜きをするならば、kafkaで受け取ったメッセージをそのまま他のデータベースに保存することもできます。
(Kafka Connect)
これでイベントソーシングと組み合わせることができました。

読み込みモデル更新処理

次に読み込みモデルの更新です。
ここではメッセージブローカーをsubscribeしたプロセスの処理が該当します。

PHPではこれを行うにはsystemdかsupervisorなどで常駐プロセスにするしかありません。

ここでLaravelならQueueがあるのに、という話になりますが、
今回はMySQLとElasticsearchを使って二つのデータベースに書き込まなければいけません。
つまり一つのQueueに異なるプロセスが同時にアクセスできる必要があります。
これを一つのプロセスで実装すると2フェーズコミットの問題がそのままくっついてきます。
(同じデータベースであれば問題ありません)

ラウンドロビンというよりも並行して処理が走る、ということになります。
これはもうフレームワークの機能でどうにかなる話ではありません。
それにパフォーマンス面でPHPの読み込み処理をGoやScalaなどに変更する、というのは
事業サービス形の会社ではよくある話だと思います。
この場合、LaravelのPHPシリアライズしたQueueを使い続けていては足枷になってしまいます。

こういった処理をする場合はフレームワーク依存の機能を使わないことが一番です。

MySQLとElasticsearchに書き込むということで、MySQLの方は簡単ですので、
サンプルを見るなり自身で作りなりしてもらえれば良いですが、
問題はElasticsearchです。

サジェストなどを実装しなければなりませんので、LIKE検索の延長で使うだけではできません。
これを解決するにはアナライザーを利用することです。
kuromojiやicu、ngramなどを組み合わせると対応できます。
今回のサンプルでは最低限の構成になっていますので、下記のようなmappingで十分です。

{
  "settings": {
    "index": {
      "number_of_shards": 3,
      "number_of_replicas": 0
    },
    "analysis": {
      "char_filter": {
        "normalize": {
          "type": "icu_normalizer",
          "name": "nfkc",
          "mode": "compose"
        },
        "kana_to_romaji": {
          "type": "mapping",
          "mappings": [
            "あ=>a",
            "い=>i",
            "う=>u",
            "え=>e",
            "お=>o",
            "か=>ka",
            "き=>ki",
            "く=>ku",
            "け=>ke",
            "こ=>ko",
            "さ=>sa",
            "し=>shi",
            "す=>su",
            "せ=>se",
            "そ=>so",
            "た=>ta",
            "ち=>chi",
            "つ=>tsu",
            "て=>te",
            "と=>to",
            "な=>na",
            "に=>ni",
            "ぬ=>nu",
            "ね=>ne",
            "の=>no",
            "は=>ha",
            "ひ=>hi",
            "ふ=>fu",
            "へ=>he",
            "ほ=>ho",
            "ま=>ma",
            "み=>mi",
            "む=>mu",
            "め=>me",
            "も=>mo",
            "や=>ya",
            "ゆ=>yu",
            "よ=>yo",
            "ら=>ra",
            "り=>ri",
            "る=>ru",
            "れ=>re",
            "ろ=>ro",
            "わ=>wa",
            "を=>o",
            "ん=>n",
            "が=>ga",
            "ぎ=>gi",
            "ぐ=>gu",
            "げ=>ge",
            "ご=>go",
            "ざ=>za",
            "じ=>ji",
            "ず=>zu",
            "ぜ=>ze",
            "ぞ=>zo",
            "だ=>da",
            "ぢ=>ji",
            "づ=>zu",
            "で=>de",
            "ど=>do",
            "ば=>ba",
            "び=>bi",
            "ぶ=>bu",
            "べ=>be",
            "ぼ=>bo",
            "ぱ=>pa",
            "ぴ=>pi",
            "ぷ=>pu",
            "ぺ=>pe",
            "ぽ=>po",
            "きゃ=>kya",
            "きゅ=>kyu",
            "きょ=>kyo",
            "しゃ=>sha",
            "しゅ=>shu",
            "しょ=>sho",
            "ちゃ=>cha",
            "ちゅ=>chu",
            "ちょ=>cho",
            "にゃ=>nya",
            "にゅ=>nyu",
            "にょ=>nyo",
            "ひゃ=>hya",
            "ひゅ=>hyu",
            "ひょ=>hyo",
            "みゃ=>mya",
            "みゅ=>myu",
            "みょ=>myo",
            "りゃ=>rya",
            "りゅ=>ryu",
            "りょ=>ryo",
            "ぎゃ=>gya",
            "ぎゅ=>gyu",
            "ぎょ=>gyo",
            "じゃ=>ja",
            "じゅ=>ju",
            "じょ=>jo",
            "びゃ=>bya",
            "びゅ=>byu",
            "びょ=>byo",
            "ぴゃ=>pya",
            "ぴゅ=>pyu",
            "ぴょ=>pyo",
            "ふぁ=>fa",
            "ふぃ=>fi",
            "ふぇ=>fe",
            "ふぉ=>fo",
            "ふゅ=>fyu",
            "うぃ=>wi",
            "うぇ=>we",
            "うぉ=>wo",
            "つぁ=>tsa",
            "つぃ=>tsi",
            "つぇ=>tse",
            "つぉ=>tso",
            "ちぇ=>che",
            "しぇ=>she",
            "じぇ=>je",
            "てぃ=>ti",
            "でぃ=>di",
            "でゅ=>du",
            "とぅ=>tu",
            "ぢゃ=>ja",
            "ぢゅ=>ju",
            "ぢょ=>jo",
            "ぁ=>a",
            "ぃ=>i",
            "ぅ=>u",
            "ぇ=>e",
            "ぉ=>o",
            "っ=>t",
            "ゃ=>ya",
            "ゅ=>yu",
            "ょ=>yo",
            "ア=>a",
            "イ=>i",
            "ウ=>u",
            "エ=>e",
            "オ=>o",
            "カ=>ka",
            "キ=>ki",
            "ク=>ku",
            "ケ=>ke",
            "コ=>ko",
            "サ=>sa",
            "シ=>shi",
            "ス=>su",
            "セ=>se",
            "ソ=>so",
            "タ=>ta",
            "チ=>chi",
            "ツ=>tsu",
            "テ=>te",
            "ト=>to",
            "ナ=>na",
            "ニ=>ni",
            "ヌ=>nu",
            "ネ=>ne",
            "ノ=>no",
            "ハ=>ha",
            "ヒ=>hi",
            "フ=>fu",
            "ヘ=>he",
            "ホ=>ho",
            "マ=>ma",
            "ミ=>mi",
            "ム=>mu",
            "メ=>me",
            "モ=>mo",
            "ヤ=>ya",
            "ユ=>yu",
            "ヨ=>yo",
            "ラ=>ra",
            "リ=>ri",
            "ル=>ru",
            "レ=>re",
            "ロ=>ro",
            "ワ=>wa",
            "ヲ=>o",
            "ン=>n",
            "ガ=>ga",
            "ギ=>gi",
            "グ=>gu",
            "ゲ=>ge",
            "ゴ=>go",
            "ザ=>za",
            "ジ=>ji",
            "ズ=>zu",
            "ゼ=>ze",
            "ゾ=>zo",
            "ダ=>da",
            "ヂ=>ji",
            "ヅ=>zu",
            "デ=>de",
            "ド=>do",
            "バ=>ba",
            "ビ=>bi",
            "ブ=>bu",
            "ベ=>be",
            "ボ=>bo",
            "パ=>pa",
            "ピ=>pi",
            "プ=>pu",
            "ペ=>pe",
            "ポ=>po",
            "キャ=>kya",
            "キュ=>kyu",
            "キョ=>kyo",
            "シャ=>sha",
            "シュ=>shu",
            "ショ=>sho",
            "チャ=>cha",
            "チュ=>chu",
            "チョ=>cho",
            "ニャ=>nya",
            "ニュ=>nyu",
            "ニョ=>nyo",
            "ヒャ=>hya",
            "ヒュ=>hyu",
            "ヒョ=>hyo",
            "ミャ=>mya",
            "ミュ=>myu",
            "ミョ=>myo",
            "リャ=>rya",
            "リュ=>ryu",
            "リョ=>ryo",
            "ギャ=>gya",
            "ギュ=>gyu",
            "ギョ=>gyo",
            "ジャ=>ja",
            "ジュ=>ju",
            "ジョ=>jo",
            "ビャ=>bya",
            "ビュ=>byu",
            "ビョ=>byo",
            "ピャ=>pya",
            "ピュ=>pyu",
            "ピョ=>pyo",
            "ファ=>fa",
            "フィ=>fi",
            "フェ=>fe",
            "フォ=>fo",
            "フュ=>fyu",
            "ウィ=>wi",
            "ウェ=>we",
            "ウォ=>wo",
            "ヴァ=>va",
            "ヴィ=>vi",
            "ヴ=>v",
            "ヴェ=>ve",
            "ヴォ=>vo",
            "ツァ=>tsa",
            "ツィ=>tsi",
            "ツェ=>tse",
            "ツォ=>tso",
            "チェ=>che",
            "シェ=>she",
            "ジェ=>je",
            "ティ=>ti",
            "ディ=>di",
            "デュ=>du",
            "トゥ=>tu",
            "ヂャ=>ja",
            "ヂュ=>ju",
            "ヂョ=>jo",
            "ァ=>a",
            "ィ=>i",
            "ゥ=>u",
            "ェ=>e",
            "ォ=>o",
            "ッ=>t",
            "ャ=>ya",
            "ュ=>yu",
            "ョ=>yo"
          ]
        }
      },
      "tokenizer": {
        "kuromoji_normal": {
          "mode": "normal",
          "type": "kuromoji_tokenizer"
        }
      },
      "filter": {
        "readingform": {
          "type": "kuromoji_readingform",
          "use_romaji": true
        },
        "edge_ngram": {
          "type": "edge_ngram",
          "min_gram": 1,
          "max_gram": 10
        },
        "synonym": {
          "type": "synonym",
          "lenient": true,
          "synonyms": [
            "nippon, nihon"
          ]
        }
      },
      "analyzer": {
        "suggest_index_analyzer": {
          "type": "custom",
          "char_filter": [
            "normalize"
          ],
          "tokenizer": "kuromoji_normal",
          "filter": [
            "lowercase",
            "edge_ngram"
          ]
        },
        "suggest_search_analyzer": {
          "type": "custom",
          "char_filter": [
            "normalize"
          ],
          "tokenizer": "kuromoji_normal",
          "filter": [
            "lowercase"
          ]
        },
        "readingform_index_analyzer": {
          "type": "custom",
          "char_filter": [
            "normalize",
            "kana_to_romaji"
          ],
          "tokenizer": "kuromoji_normal",
          "filter": [
            "lowercase",
            "readingform",
            "asciifolding",
            "synonym",
            "edge_ngram"
          ]
        },
        "readingform_search_analyzer": {
          "type": "custom",
          "char_filter": [
            "normalize",
            "kana_to_romaji"
          ],
          "tokenizer": "kuromoji_normal",
          "filter": [
            "lowercase",
            "readingform",
            "asciifolding",
            "synonym"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "word_field": {
        "type": "keyword",
        "fields": {
          "suggest": {
            "type": "text",
            "search_analyzer": "suggest_search_analyzer",
            "analyzer": "suggest_index_analyzer"
          },
          "readingform": {
            "type": "text",
            "search_analyzer": "readingform_search_analyzer",
            "analyzer": "readingform_index_analyzer"
          }
        }
      }
    }
  }
}

この辺りはこれを解説するだけで数記事になりますので、
興味ある方は下記のものをはじめとして色々読み込んでみてください。

www.elastic.co

これを利用すれば集計と利用頻度順のサジェストをうまく活用できます。
ここまでデータベースだけの世界で解決できました。

これを読み込む、クエリの処理は単純に問い合わせてDTO(Data Transfer Object)だけあれば十分でしょう。
例えばクエリの対象がUIや複雑なAPIであれば、それに合わせたデータを構築すれば済みます。
また対象のインデックスの中身のデータを更新したければ、
それ用のメッセージをpublishし、subscribeした処理で書き換えれば済みますので、
複雑な処理がWebアプリケーション側からも排除できます。
またチームで開発をする場合は、開発者の分担もうまくできるようになります。
(どこかの層にビジネスロジックが集中しすぎないように配慮する必要はあります。)

<?php
// 省略

    /**
     * @param HandlerInterface $handler
     * @param Consumer $consumer
     * @param int $offset
     */
    public function handle(
        HandlerInterface $handler,
        Consumer $consumer,
        int $offset = RD_KAFKA_OFFSET_STORED
    ): void {
        $consumer->addBrokers($this->broker);
        $topic = $consumer->newTopic($this->topic, $this->topicConf);
        $topic->consumeStart($this->partition, $offset);
        while (true) {
            $message = $topic->consume($this->partition, 120 * 10000);
            if ($message instanceof Message) {
                match ($message->err) {
                    RD_KAFKA_RESP_ERR_NO_ERROR => call_user_func($handler, $message),
                    RD_KAFKA_RESP_ERR__TIMED_OUT => throw new SubscriberTimeoutException('time out.'),
                };
            }
        }
    }

今回の例ではsubsrcibeの仕組みと、そのデータを受け取って処理するクラスを分離するようにしました。
この辺りは単純にデータ処理だけになりますので、特に難しいものはありません。
なにかのビジネスロジックを解決するわけでもありませんので、
シンプルな仕組みになっています。

<?php
declare(strict_types=1);

namespace App\DataAccess\Elasticsearch;

use DateTime;
use App\Foundation\Kafka\HandlerInterface;
use Elasticsearch\Client;
use RdKafka\Message;

class RegisterKeyword implements HandlerInterface
{
    /**
     * @param Client $client
     * @param string $index
     */
    public function __construct(
        private Client $client,
        private string $index
    ) {
    }

    /**
     * @param Message $message
     * @throws \JsonException
     */
    public function __invoke(
        Message $message
    ): void {
        $decoded = json_decode($message->payload, false, 512, JSON_THROW_ON_ERROR);
        $word = '';
        if (isset($decoded->body)) {
            $word = $decoded->body;
        }
        if ($word === '') {
            return;
        }
        $d = new DateTime();
        $this->client->index([
            'index' => $this->index,
            'body' => [
                'word_field' => $word,
                'created' => $d->format('Y-m-d\TH:i:s')
            ]
        ]);
    }
}

クエリ実装例

クエリとしては、特に何かに注力しなければいけないことはありません。
コマンドと同じクラスを使わない、なにかを変に汎用化させないなどができていれば良いでしょう。

<?php
declare(strict_types=1);

namespace App\DataAccess\Elasticsearch;

use App\DataAccess\GetSuggestKeywordInterface;
use Elasticsearch\Client;
use function array_merge;

final class SuggestKeyword implements GetSuggestKeywordInterface
{
    use AggregateQuery;

    /**
     * @param Client $client
     * @param string $index
     */
    public function __construct(
        private Client $client,
        private string $index
    ) {
    }

    /**
     * @param string $word
     * @return array
     */
    public function findByWord(
        string $word
    ): array {
        $params = [
            'index' => $this->index,
            'body' => $this->aggsQuery(),
        ];
        if ($word !== '') {
            $params['body'] = array_merge($params['body'], [
                'query' => $this->suggestMatchQuery($word)
            ]);
        }
        $result = $this->client->search($params);
        return $result['aggregations']['keyword']['buckets'];
    }

    /**
     * @param string $word
     * @return array
     */
    private function suggestMatchQuery(
        string $word
    ): array {
        return  [
            'bool' => [
                'should' => [
                    [
                        'match' => [
                            'word_field.suggest' => [
                                'query' => $word
                            ]
                        ]
                    ],
                    [
                        'match' => [
                            'word_field.readingform' => [
                                'query' => $word,
                                'fuzziness' => 'AUTO',
                                'operator' => 'and'
                            ]
                        ]
                    ]
                ]
            ]
        ];
    }
}

Elasticsearchへの問い合わせは上記のような形になります。
ここで取得したものをインターフェースを挟んで
DTOに変換してあげると要件を実現できます。

<?php
declare(strict_types=1);

namespace App\QueryProcessor;

use App\Transfer\Keyword;
use Generator;
use App\DataAccess\GetSuggestKeywordInterface;

final class SuggestKeywordQueryProcessor
{
    /**
     * @param GetSuggestKeywordInterface $suggestKeyword
     */
    public function __construct(
        private GetSuggestKeywordInterface $suggestKeyword
    ) {
    }

    /**
     * @param string $word
     * @return Generator
     */
    public function run(string $word): Generator
    {
        foreach ($this->suggestKeyword->findByWord($word) as $row) {
            yield new Keyword($row['key'], $row['doc_count']);
        }
    }
}

気になる方は実際のサンプルコードを見るなどすると理解できると思います。

最後に

一つ一つの処理は単純になることがわかると思います。
ただし作るものや関わるミドルウェアが増えるのは事実ですので、
利用するミドルウェアについてもしっかりと理解をする必要があります。

またこのKafkaのデータハブとして作用する仕組みを使って、
データ基盤系のデータパイプラインやSparkを使ったより高度なアプリケーションを開発することができます。
その辺りになると本格的な分散処理や、Webアプリケーションとは全く違う知識やテクニックが必要となりますので、
エンジニアとしてのスキルセットをさらに増やしたい方にはいい入り口になると思います。
これらはES+CQRSではなくてイベント駆動になりますが、その先にUIがあることも多々ありますので考え方は踏襲できます。

もうちょっと丁寧に図解とかしようと思いましたが力尽きたので今回はこの辺で。

会社のアドベントカレンダーということで、
こういった処理を作ったり考えたり、改善をしまくりたい!というTSをメインにして
GoやPHPをやりたい!というエンジニアの方や、
KafkaやSparkなどを使ったストリーム処理(Scala、Java)とかやりたいというエンジニアの方、
それらを支えるインフラをやりたい!SREみたいなことしたい!というインフラエンジニアの方を

募集しております!!!!!

お気軽に@ex_takezawaなどにご連絡ください。