🚀

チームオンボーディング用のハンズオンをつくった話

2022/12/17に公開

これはFOLIOアドベントカレンダー 17日目の記事です。

チーム内のオンボーディングの一環として、新しく入ったメンバーがチーム内の技術スタックやアーキテクチャの基本的な使い方を学ぶためのハンズオンを作りました。どんな内容か、やってみてどうだったか紹介します。

経緯

私が所属しているチームは、投資一任契約の契約管理を行うシステムの開発と運用をしています。
もちろん今回のハンズオン誕生前にも、オンボーディングに使える資料は豊富に用意されていました。ただ資料形式というのはどうしても座学的なものが中心になるため、実務とのギャップが否めません。実際のコードベースで使うアーキテクチャやライブラリに慣れる機会があれば、オンボーディングから業務へスムーズに移行できるのではないかと思いがありました。別のチームには既にハンズオン形式のオンボーディングが整備されており、いつかうちにも用意したいねという話はチーム内でも出ていました。
そんな中、ちょうど新しいメンバーのチーム加入が決定しました。これが大きなきっかけとなり、ついにオンボーディング用ハンズオンを作ろうということになったわけです。
この時点でシステムはファーストリリースから半年以上の運用を経ており、技術スタックやアーキテクチャがある程度成熟していたことも後押しになりました。

内容

Scala のコードベースの穴埋め形式で課題を設けました。ハンズオンのリポジトリのREADMEやドキュメント、コードのコメントをみながらコード修正をして、全ての課題でユニットテストとコードレビューが通ったら完了です。チーム内で導入しているライブラリの課題から始まり、データモデルやアーキテクチャ等の課題を経て、最終的に web API を1つ実装するところまで行う形にしています。業務タスクだけではアーキテクチャの全体感を持ちにくいものですが、ボトムアップ的に課題を進めることでなんとなくでも俯瞰してもらえるように意識しました。また、こまめにレビューとフィードバックを行えるようにユニットテスト形式にしています。これは入社したばかりの人に起こりがちな「こだわり過ぎ」対策であり、ローカルのテストさえ通ればどんどんレビューに出してほしい(通ってなくても質問等はいつでも歓迎)というこちらのスタンスを示しています。

課題のイメージ

課題の内容を一部紹介します。実際の課題では業務コードに関する知識を前提としているので、省略したものです。

テンポラルデータモデルに関する課題

テンポラルデータモデルはRDB内でデータの履歴管理をする手法の一つです。特にBiTemporal Data Model は、ビジネスロジック中のエンティティの状態遷移と障害対応等のデータ訂正を区別して取り扱うことができます。詳しくは BiTemporal Data Modelに入門中 - だいたいよくわからないブログ , 業務から見たテンポラルデータモデルの解釈と利用方法の紹介 等を参照してください。

開発時によくある実装や運用中のオペレーションを踏まえて、以下のような課題を用意しました。

  • Transactional Data Model で表現されたエンティティを取得するメソッドを実装する
  • Transactional Data Model で表現されたDBテーブルのレコードを修正して、指定した期間に有効な状態にする
  • あるエンティティを BiTemporal Data Model に則って状態更新するメソッドを実装する
  • BiTemporal Data Model で表現されたエンティティを取得をするメソッドのテストで、適切なレコードを取得するように業務時刻(実時間)を設定する

DCIアーキテクチャに関する課題

チーム内で管理している契約管理のアプリケーションでは Akka を実行基盤にした DCI(Data, context and interaction)アーキテクチャを採用しています。DCIは、システムの構造(what a system is)を表現するDataとシステムの振る舞い(what a system does)を表現するInteraction(ロールとして実装される)を分離した上でモデリングし、それらを業務の文脈(Context)の中で結びつけることによってビジネスロジックを組み立てる手法です。モデリングの整合性と柔軟性を保てることが利点のひとつだと思っています。詳しくは Data, context and interaction - Wikipedia 等を参照してください。

口座振替を題材にDCIアーキテクチャを学ぶ課題を用意しました。チーム内のシステムでは口座振替を取り扱うことはありませんが、DCIの説明でよく使われる題材の1つなので採用しました。口座振替という文脈(Context)の中で、口座データ(Data)に対して、入金元と送金先のロール(Interaction)を割り当てるように DCI を実装していきます。具体的には以下のような穴埋め問題です。

口座振替コンテキストの中の Actor メッセージのハンドリング

MoneyTransferContext.scala
/**
  * 口座振替コンテキスト(Context)
  */
object MoneyTransferContext {

  sealed trait Command

  /* 口座振替依頼コマンド */
  case class Transfer(
      transactionId: TransactionId,
      source: Account,
      destination: Account,
      amount: Int
  ) extends Command

(中略)

  private def receiveMessages(replyTo: ActorRef[Bank.Command]): Behavior[Command] = {
    Behaviors.receive {
      case (context, message: Transfer) =>
        // 口座データ(Data)に入金元口座ロールを割り当て(Interaction)、アクターを生成
        val source = context.spawn(
          SourceAccountRole.assignTo(message.source, context.self),
          s"source-account-${message.transactionId.value}"
        )
        // 課題: 口座データ(Data)に出金先口座ロールを割り当て(Interaction)、アクターを生成してください。
        ???
  
        // 課題: 入金元口座ロールアクターに処理を依頼してください。
        ???

        Behaviors.same

(後略)

入金元口座ロールアクターの振替メッセージのハンドリング

※問題をシンプルにするためにトランザクションについては保証していません。

SourceAccountRole.scala
/**
  * 入金元口座ロール
  */
object SourceAccountRole {

  sealed trait Command

  /* 振替コマンド */
  case class Transfer(
      transactionId: TransactionId,
      amount: Int,
      destination: ActorRef[DestinationAccountRole.Command]
  ) extends Command

(中略)

  private def handleTransfer(
      context: ActorContext[Command],
      self: Account,
      command: Transfer,
      replyTo: ActorRef[MoneyTransferContext.Command]
  ): Behavior[Command] = {
    // 課題: 送金先口座ロールアクターに入金処理を依頼してください。
    // 送金先口座ロールアクターへの参照は command.destination を使用してください。
    // 自分自身の入金元口座ロールアクターへの参照は context.self を使用して取得できます。
    ???

    // 課題: 自分自身の入金元口座ロールアクターに出金処理を依頼してください。
    ???

    Behaviors.same
  }

(後略)

出金先口座ロールアクターの入金メッセージのハンドリング

DestinationAccountRole.scala
/**
  * 出金先口座ロール
  */
object DestinationAccountRole {

  sealed trait Command

  /* 入金コマンド */
  case class Deposit(
      transactionId: TransactionId,
      amount: Int,
      replyTo: ActorRef[SourceAccountRole.Command]
  ) extends Command
  
(中略)  

  private def handleDeposit(context: ActorContext[Command], self: Account, command: Deposit): Behavior[Command] = {
    // 課題: 残高を増やし入金元口座ロールアクターに入金が成功したことを伝えてください。(self.increaseBalance を使ってください)
    ???

    Behaviors.stopped // 入金が完了したらアクターは終了させる
  }

(後略)

結果

実際に、新しく入ったチームメンバーにハンズオンをやってもらいました。まずは1人で取り組む時間を取りつつ、つまった時や理解を深めたい時には質問をしてもらうという形です。ときにはペアプロも交えながら、無事にやり遂げてもらうことができました。振り返ってみれば、こうした進め方やコミュニケーションの取り方はかなり実務に近く、よりシームレスなオンボーディングが実現できたように思います。

結果、実務に移行したメンバーからハンズオンの内容を踏まえた質問をもらったり、ハンズオンで力がついた実感があるという嬉しいフィードバックがあったりしました。チーム初の試みで手探りの部分も多くありました(チームのマネージャーも業務の合間にハンズオンを試してくれました)が、このような反応から一定の成果はあげられたのではないかと感じています。ただ、コードコメントが不足していたり、課題の設定意図が伝わりにくいところがあったりと、改善点もみつかりました。地道に直していき、次に新しいメンバーが来た時には更によいものができるようにしたいと思います。

Discussion