Diverse developer blog

株式会社Diverse(ダイバース) 開発者ブログです。

後編:歴史ある婚活サービスyoubrideをリプレイスしようとしていた話

Diverseの須藤(id:kurotyann) です。

今回はリプレイスの結果の後編を共有します。

主にフェーズ1〜2の技術的な知見について書きました。

なお、「前編:歴史ある婚活サービスyoubrideをリプレイスしようとしていた話 - Diverse developer blog」でもお伝えしたとおり、このリプレイスは今年3月に経営判断により終了しました。残念ながらバックエンドのNestJSはプロダクションにデプロイされることなく終了しました。NestJSは多数の会社でプロダクション運用の実績があり、採用する価値のあるフレームワークです。弊社も本番へデプロイして、さらなる知見を得てコミュニティに還元したかったのですが、それは次回のプロジェクトへ持ち越しとなりました。

このブログでは本番デプロイは叶わなかったものの、それでもニーズがありそうな知見に絞って成果を報告したいと思います。

TypeScriptを主軸にした構成

このリプレイスを開始した昨年10月に弊社の開発言語をTypeScriptとDartに統一する方針を定めました。弊社のエンジニア組織の体制を考慮したとき、この二つの言語への投資が最もユーザーのニーズに応えやすい体制になると考えました。詳細は、以下の記事を参照してください。

このリプレイスは、TypeScriptとDartのメリットを実感するためのプロジェクトとして開始した一面もあります。DartはモバイルアプリをFlutterで開発中のため、TypeScriptはモバイル以外を担当する言語となります。今回はフロントエンドにNext.jsを、バックエンドにNestJSを採用しました。どちらもTypeScriptをフルサポートするフレームワークで、フロントからバックエンドまで同じ言語で、かつ各種npmパッケージ(ESlintやPrettierなど)の恩恵を受けながら開発できます。

リプレイスは前編で説明したように、利用規約などの静的なページから移行を開始しました。既存の構成に、Next.jsやNestJSをAWS ECS(Fargate)で追加し、ELBのルーティングを修正するのがリプレイスの主な開発フローです。赤枠が今回のリプレイスで追加修正されたリソースです。

リプレイスで追加修正されたリソース(赤枠)

Next.jsは静的なページをSSGで生成し、生成したファイルのキャッシュをNginxが担当するシンプルな構成にとどまりました。この後編ではTypeScriptで統一した開発環境や、NestJSやPrismaなどのニーズの多そうな知見に絞って紹介したいと思います。

1. Monorepo構成

TypeScriptで統一したので、リポジトリもモノリポ構成にしました。リポジトリのディレクトリ構成を簡単にした図がこちらです。

.
├── api
│   ├── infra
│   ├── initdb.d
│   ├── prisma
│   ├── src
│   └── test
├── mobile
│   └── api-client
├── openapitools.json
├── renovate.json5
└── web
    ├── cypress
    ├── infra
    ├── nginx
    ├── public
    └── src
        └── api-client

apiがNestJS、webはNext.jsのディレクトリです。mobileのディレクトリは、OpenAPI Generatorで自動生成したAPIクライアントのディレクトリです。別リポジトリで開発中のFlutterの pubspec.yaml から参照しています。モバイルだけでなく、webにもOpenAPI GeneratorでAPIクライアントコードをsrcの直下に自動生成しています。

apiとwebにあるinfraディレクトリには、CI(GitHub Actions)でECS(Fargate)へデプロイするときに参照する task-definition.jsonDockerfile を環境別に配置しています。TerraformとFlutterはリプレイス前から別リポジトリで管理中なので、このモノリポに含めません。

課題

今回は、NxやlernaやTurborepoなどのモノリポ管理ツールは利用しませんでした。npm の --prefix や、各ディレクトリに配置した Makefile にコマンドを集約して、make -C ../api command_name でコマンド実行を楽にしていました。

モノリポ管理ツールは、npmの共通パッケージを集約してビルドキャッシュできるなど利点はあるものの、今回のリプレイスに必須ではないと判断して見送りました。次回で挑戦します。

2. NestJSはDDDベースのModule分割

NestJSはModuleごとに機能を隔離するアーキテクチャをとり、Moduleごとの複雑さを管理しやすくする設計をサポートしています。しかし、機能の分割単位を誤ると、Module同士の依存関係が増えてコードが密結合になり、開発が大変になります。弊社のModuleは以下のDDD構成です。

/**
 * プレゼンテーション (Controller)
 *  ↓
 * サービス (Service)
 *  ↓
 * ドメイン (Domain)
 *  ↑
 * インフラ (Repository)
 */
api/src/modules
└── auth
    ├── auth.controller.ts
    ├── auth.module.ts
    ├── domain/
    ├── dto/
    ├── repository/
    └── service/

弊社ではModuleはドメイン単位で分割して、Module同士が依存する関係を避けるようにしました。つまり、Moduleから他のModuleを呼び出すのは原則禁止です。モジュール内に影響を閉じ込める設計を優先します。その代わり、Module間で似たような処理が重複するのは許容しました。例えば以下のとき、重複を許容します。

  • リポジトリ層で、userテーブルの呼び出しが多数のモジュールに記載される。
    • 例:authやuser
  • ドメイン層で、似たようなモデルや処理が記載される。
    • 例:authやuser

しかし例外として、どうしても共通Moduleとして切り出す方が効率的である場合は common/ に切り出します。なお、この共通モジュールはプレゼンテーション層(controller)を持ちません。そして、ドメイン知識を含まないもの、共通利用が確実な小さな処理は、utils/ に切り出しました。最後に、環境変数やORM(Prisma)など、あらゆる箇所から呼ばれるModuleを @Global にしてlibs/ に配置します。

api
 └── src
     ├── libs
     │   ├── config
     │   ├── prisma
     │   └── sentry
     ├── modules
     │   ├── auth
     │   ├── common
     │   ├── health-check
     │   └── user
     └── utils
         └── validator
             └── class-validator.ts

課題

このModuleとディレクトリ構成で開発した期間がまだ数ヶ月なため、大きな課題はありませんでした。ただ、リプレイスを進めてコードが増えると、この設計でも辛みがでるかもしれません。そこは次回のプロジェクトで試行錯誤したいところです。

3. テスト時のPrismaのseedデータのリセット

開発時のテストデータや、テストケースごとのseedデータのリセット方法です。DBの初期化とユーザー作成は docker compose up 時に docker-entrypoint-initdb.d/init.sh で可能です。

// api/initdb.d/init.sh

#!/bin/bash

mysql -u root -ppassword < "/docker-entrypoint-initdb.d/0_setup/database.sql"

テーブル作成とseedデータのリセットもPrismaで簡単にできます。api/prisma/seed.ts に投入データを非同期で書きます。

// api/prisma/seed.ts

import { PrismaClient } from '@prisma/client'
import { createTestUser } from './seeds/user'

const prisma = new PrismaClient()

async function main() {
  // test user
  await createTestUser(prisma)
}

main().catch(e => {
  console.error(e)
  process.exit(1)
})

// api/prisma/seeds/user.ts

import { PrismaClient } from '@prisma/client'

export const createTestUser = async (prisma: PrismaClient): Promise<void> => {
  await prisma.user.upsert({
    where: {
      id: 1,
    },
    update: {},
    create: {
      id: 1,
      email: 'test_user01@local.youbride.jp',
      gender: 1,
      birth: new Date('1980-12-24'),
      status: 1,
      created_at: new Date(),
      updated_at: new Date(),
    },
  })
}

あとはDBコンテナが起動中に docker compose run --rm nestjs npx prisma migrate reset --force すれば、DBがテストデータの初期状態にリセットされます。ローカルで開発中に何度でもやり直しが効くので便利です。

これを単体テストやE2Eテストをjestで走らせたとき、テストケースごとにやりたい場合 beforeAll などで、 execPromisify('npm run prisma:reset') を呼び、無理やりDBをテストデータの初期状態にリセットしていました。

import { exec } from 'child_process'
import util from 'util'

const execPromisify = util.promisify(exec)

/**
 * prisma:reset は以下を実行する
 *  1. DBテーブルを削除
 *  2. DBテーブルを作成(migration)
 *  3. DBテーブルにシードデータを投入(seed)
 *
 * @see api/prisma/migrations
 * @see api/prisma/seeds
 * @see api/prisma/seed.ts
 * @see https://www.prisma.io/docs/guides/database/seed-database#integrated-seeding-with-prisma-migrate
 */
export const resetDatabaseTables = async () => {
  // package.json の scripts に 
  // "prisma:reset": "npx prisma migrate reset --force" がある前提
  await execPromisify('npm run prisma:reset')
}

// auth-signup.repository.spec.ts

import { resetDatabaseTables } from '@/../test/utils/prisma/prisma-util'

beforeAll(async () => {
  await resetDatabaseTables()
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [ConfigModule, PrismaModule],
    providers: [AuthSignupRepository],
  }).compile()

  repository = moduleFixture.get<AuthSignupRepository>(AuthSignupRepository)
})

課題

この対応でテストケースごとにDBの状態を気にすることなく、テストコードが書けます。しかし、参照するDBが一つなのでテストを直列で実行(jest --runInBand)しないと実行順やタイミングでテストが失敗します。 さらに、直列なのでテストケースの増加に比例してテスト時間も増えるし、npm run prisma:reset も毎度2~3秒かかります。

ぱっと思いつく解決策として...

  • テスト時に参照するDBを複数にする
  • DBの状態に依存しないテストケースと、そうでないケースを別ディレクトリに分ける
  • DB接続は諦めてテストデータをモックにする

などがありそうです。テストを書くモチベーションに直結するので、次回のプロジェクトでは良い解決策を見つけます。

ELBのルーティングルール管理

TypeScriptやDartの話だけでなく、リプレイスの苦労も共有します。一番苦労したのは新旧画面のルーティングです。

段階的に画面をリプレイスする場合、画面のパスを新旧で別にして共存させるか、パスの転送先を新旧で切り替えるかだと思います。弊社では後者を選びました。

ELBのルーティング

課題

このとき、新画面のルールを旧画面のルールより優先するようにTerraformで書く必要があります。つまり、aws_lb_listener_rulepriority を期待するルールの昇順に並び替えないといけません。しかし、terraform applyしたとき、もし既存ルールの優先度と追加したルールが重複すると、applyが中断されてルーティングが崩れた状態でaws上にリスナールールが構築されます。

追加ルールと、既存ルールの priority が重複しないか確認しながら、terraform apply するよりも、awsコンソールから手動でルールを追加後、優先度が変わったルールを terraform import する方が安全だと判断し、フローを変更しました。

  • ① 新ルールを既存のルールの優先度と被らないように追加する
resource "aws_lb_listener_rule" "example-replace" {
  listener_arn = aws_alb_listener.production.arn
  priority     = local.replace-base-priority + 2 // 次のルールは重複を避けて3にする
  • ② 追加したルール数に合わせて、XXXX-rule-countを変更する
locals {
  base-priority         = 1
  base-rule-count       = 1
  replace-rule-count    = 3
  replace-base-priority = local.base-priority + local.base-rule-count
  prod-base-priority    = local.replace-base-priority + local.replace-rule-count
}
  • ③ PRレビューはするがmasterには一旦マージしない
    1. ①と②の修正をレビューしてもらう
    2. マージすると優先度の重複で失敗する可能性があるのでマージはしない
  • ④ AWSコンソールで対応する環境のリスナールールに ① のルールを追加する
  • ⑤ ルールが正常に切り替わったらterraform import でルールを追加する
  • terraform plan で差分なければPRをmasterにマージする

awsコンソールで新ルールを手動追加しつつ、terraformのローカル変数でルールの重複を避ける作戦です。あまり賢い解決策ではないので、ここも良い解決方法を模索したいです。

最後に:成果をつなげていく

これまでDiverseはPerl、Ruby、Dartでの開発経験はあるものの、TypeScriptでの開発経験や歴史がまだ浅い会社です。リプレイス中は、毎週火曜日に弊社で技術知見共有会(Tech-DLN)を開き、TypeScriptをはじめリプレイスで得た知見を社内に共有していました。リプレイスを機会に社内での技術共有が進みはじめたので、その推進力を次のプロジェクトへと繋げます。

リプレイスは終了しましたが、次のプロジェクトでもTypeScriptとDartでの開発を続ける予定です。このような開発体制に興味のある方、もっと良いやり方があるなど、カジュアルに話したいことがある方は、Meetyで是非お話しましょう!

meety.net

前編:歴史ある婚活サービスyoubrideをリプレイスしようとしていた話

Diverseの須藤(id:kurotyann) です。

昨年10月から婚活サービスのyoubrideのリプレイスを開始しました。

しかし、このリプレイスは今年の3月に経営判断により終了しました。

このリプレイスを最後までやり切りたかったですが、Diverseの今後の成長を考慮した判断となります。5ヶ月間という短期間ですが、得られた知見は多く、Techブログに結果を残すことにしました。

今回は、以下の2部構成でリプレイスの結果を共有し、このブログは前編になります。

世の中のプロジェクトは成功か失敗のどちらかに区別されます。しかし、今回のリプレイスは成功とも失敗とも言い切れないまま終了しました。ある意味で珍しい事例です。この成果が社内だけなく、サービスのリプレイスを検討したり、実施中の社外の方々への参考になると幸いです。

老舗サービスの変遷と構成

婚活サービスのyoubrideは、スマホがないガラケー時代からあるサービスです。運営会社が変わりながら、2007年2月にyoubrideというサービス名になりました。開発言語もJavaからPHPへと変わり、2013年ごろのリニューアルと同時にPHPからPerlへ変更されています。さらに、モバイルクライアントは、2019年にFlutter+gRPCの構成へリプレイスされています。詳細は過去のTechブログをご覧ください。

現在のyoubrideの構成を簡略化した図がこちらです。今回のリプレイスに関係するリソースに絞って構成を書いてます。

youbrideの構成図(簡略化)

構成図を説明すると、SledgeというPerlのWebフレームワークが、PCとスマホ向けのWebページを別々にレンダリングしつつ、一部のREST APIを担当しています。そして、一部のWebページはWebViewを経由してFlutterで参照しています。gRPCはモバイルアプリのAPIであり、Flutterからのみ呼び出されます。他にもサービスの管理画面や各種バッチ処理が、PerlやRubyで稼働しています。

なぜリプレイスするのか

リプレイスを決めた理由は「サービスの改善速度を低下させる技術的負債を解消するため」です。これを解決すると、youbrideを起点に会社(経営/組織/技術)を改善することに繋がっていくと考えました。

youbrideの技術的負債は、まずはPerlへの依存です。Perlから得られるメリットは年々少なくなっています。さらに残念ながら、Perlを使ってサービスを改善したい人が社内外で減少しています。最も改善すべきドメイン部分が全てPerlなため、改善する人手が足りず、改善コストが上がってしまいます。

もう一つの負債は、Webページがモバイル、フロント、サーバーで密結合している点です。あるWebページのデザイン変更のとき、モバイルアプリ、PCのWeb、スマホのWebと3つの領域に影響が出ます。もちろん、これは場合によってはメリットです。サービス共通で表示する静的なページ(例:利用規約、会社概要など)は変更コストが低く、変更漏れの心配も最小限にできます。

しかし、youbrideは静的ページ以外でも共通で利用中のWebページが複数あります。特に課題なのは、新規登録のオンボーディングページです。各プラットフォームのUIコンポーネントで体験を最適化したいのに共通であるため、各プラットフォームの変更が大変でした。理想は、モバイルはFlutterでUIをレンダリング、Webはレスポンジブにレンダリングして、新規登録に必要なAPIをサーバーに用意してリクエストです。

近い将来は、以下の構成へ段階的にリプレイスしようと計画していました。なお、開発言語はTypeScriptとDartへ統一する計画です。

リプレイス後の理想の構成図

TypeScriptとDartへ統一する背景は過去の記事をご覧ください。

リプレイスチームを作る

リプレイス専任のチームを作りました。このチームのメンバーは、youbrideを改善中の現行チームからは編成しませんでした。理由は現在のサービス改善速度を下げずに、リプレイスを進めるためです。

チームメンバーはCTO室から3名と、業務委託から3名の合計6名で進めました。業務委託は週1~2の稼働で技術的なアドバイスを担当し、リプレイスを行うのはCTO室の3名です。

フロント、バックエンド、インフラと全領域を3名で担当するため、TypeScriptやDartで統一したのは間違いではなかったと思います。工数の見積もりやタスクのアサインなどが、少し楽になったと私がPMをしていて感じました。

リニューアルではなくリプレイスです

リプレイスの基本方針を以下にまとめました。

  1. 画面デザインやAPI仕様は「基本的に現行のまま」とする
  2. ただし、「ニーズに合わない仕様やデザインまで移行しない」
  3. 変更は「ユーザーのニーズを損なわず、工数削減できる変更なのか」を考える

つまり、リニューアルではないことを前提にリプレイスを進めようとしました。仕様を一度に変更するとデグレが起きて失敗する可能性が高いです。また仕様調査を十分にしたとしても、次はどう仕様変更するかを考える調整コストが発生します。

リプレイス担当とサービス改善担当のチームを別にしたのは、リプレイスに注力するためです。ユーザーのUX改善するリニューアルなどは、リプレイスの後にしました。

もう一つ、この方針にした理由は、デザイナーリソースが足りず、リプレイス専任でデザイナーをアサインできなかったのもあります。リプレイスの対象画面の新デザインまでエンジニアに負担させると、作業負担が重すぎると判断しました。よって、画面デザインはそのまま、ただし容易に変更可能だったり、廃止可能な部分は「ユーザーのニーズを損なわない範囲」で対応するとしました。

段階的にリプレイスする

リプレイスは段階方式で実施しました。一気に変更は加えず、リプレイスをフェーズとステップの段階にわけ、各フェーズの変更を本番へデプロイします。youbrideは歴史あるサービスで機能も豊富です。プラットフォームもモバイルアプリからWeb(PC/スマホ)まであります。一気に変更して広範囲に影響が及び、安全なデプロイが困難になる事態を避けようとしました。

さらに、段階的な本番デプロイで成果を確認しやすくしたかったのも理由の一つです。フェーズごとの変更に不具合がないか、フェーズの終盤で変更を本番へデプロイして確認します。フェーズは安全にデプロイできる範囲で分割しています。フェーズの対象範囲が大きすぎる場合は、さらにステップに分割しました。実際に作成したリプレイスのロードマップはこちらです。

youbrideリプレイスロードマップ

このロードマップは1枚でリプレイスの計画を伝える資料です。左から右へ進み、フェーズ0からフェーズ7へと進みます。フェーズ0はリプレイス前の状態です。youbrideはプロダクトチームがサービスの改善を担当しています。フェーズ1から、リプレイスを専任で担当するCTO室がリプレイス作業を開始します。フェーズ3までCTO室は関わり、フェーズ4以降は既存チームに移譲できる体制にまで改善する計画をつくりました。フェーズ4以降の期日は、フェーズ1~2の成果で決める予定でした。

フェーズ1では、静的ページ(会社概要や利用規約など)のリプレイスのために以下を実施しました。

  • Next.jsやNestJSのツールをセットアップ
  • Terraformで必要なAWSのリソースをセットアップ
  • CI/CDやドキュメントの整備
  • 静的ページを本番へデプロイする

AWS ELB(主にALB)のリスナールールを使い、新旧画面のパスを切り替えながらリプレイスします。失敗したとしても、ユーザーへの影響が限定的なページで試しながら、新しい構成へ安全にデプロイできる環境をフェーズ1で整えました。

実際にはフェーズ1は予定より早く完了し、次のフェーズ2へ進んでいました。フェーズ2に認証処理を選択したのは、以下が理由です。

  • 認証を突破しないと、他の主機能へのアクセスが技術的に困難なこと
  • モバイルアプリの登録やログインはWebViewであり、改善要望が多かったこと
  • Flutterから、Perlでレンダリングされた画面の依存を無くし、モバイル側の開発速度を改善したかったこと

なお、このフェーズ1〜2の技術的な知見は後編にて説明します。

Notionへの移行

リプレイスの開始と同時にドキュメントシステムをDocBaseからNotionへ移行しました。Notionへの移行もリプレイスに必要な準備でした。リプレイス中に、youbrideの仕様を新たにドキュメントにまとめ直すことにしたのです。

10年以上つづくyoubrideの仕様を把握している社員はおらず、その仕様がまとまったドキュメントも十分にはありません。DocBaseには利用価値のあるドキュメントなのか、微妙なページが多数存在しました。これらもサービス改善速度を低下させる一つの要因でした。

リプレイスのまとめ方はフェーズやステップごとにドキュメントを分け、ページ内の構造をテンプレにしました。さらに、今後の新規参入者も考慮してオンボーディング資料もまとめていました。

そして、プロダクトチームに現在も有効なドキュメントをDocBaseからNotionへ移行してもらいました。ドキュメントが多いため、すべての移行が順調に進む訳ではないですが、今後の仕様をどう書き残すか一定の方向性ができました。システムだけでなくドキュメントも試行錯誤しながら、改善するサイクルが出来てきました。

Notionへの移行は、別記事でもまとめているのでご覧ください。記事では今年の1月となっていますが、リプレイス専任チームは先行してNotionを使って移行準備を進めていました。

リプレイスは計画的に

ここまで「リプレイスの理由と、リプレイスをどのように進めたのか」を紹介しました。youbrideのリプレイスは一人では不可能でした。複数のチームとメンバーへの説明と理解が必要です。事前の計画は必須でした。

リプレイスは計画を立て社内に共有後、実行します。想定とズレた部分は適宜修正しつつ、ゴールに向かって進む。書いたり言ったりするのは簡単ですが、実際に実行すると本当に大変です。このとき計画がないと軸がなく、メンバーが混乱してチームがバラバラになります。

多くの場合、リプレイスしたいサービスは歴史があり、機能も豊富で仕様も複雑でしょう。計画を立てることが大変な作業で、とりあえず見切り発車したくなります。それでも計画は誰かが事前にたてる必要があります。なぜなら、絶対にすべては思うようにいかないからです。そして、繰り返しになりますが、一人ではできないからです。

このリプレイスも計画どおりに進まず、経営判断での停止で終了しましたが、計画がなければ経営判断の前に終わっていたでしょう。リプレイス完遂という形で成果は共有できませんが、得た知見は今後の会社の成長にとって価値あるものでした。後編では、その成果を書きました。

Flutterの状態管理ツールをproviderからriverpodに移行しました

id:kikuchy です。

婚活サービスyoubrideのスマートフォンアプリは以前からFlutterを採用しています。
developer.diverse-inc.com

このアプリでは、始めはscoped_model、次にproviderを状態管理ツールとして採用してきました。
この度、通常の開発を大きく止めることなくproviderからriverpodへと移行できたので、どのように移行したのかをお話したいと思います。

前提:なぜriverpodにしたのか

providerパッケージ(以下、provder)もriverpodパッケージ(正確にはflutter_riverpodパッケージ。以下、riverpod)も同じ作者(Remi Rousseletさん)による状態管理&依存性注入のためのライブラリです。

両者で実現できる機能はほとんど変わりません。
できることは主に以下のとおりです。

  • ChangeNotifierStateNotifierによる状態の変更を検知し、Widgetの再ビルドを自動的に行う
  • 任意のインスタンスを遅延生成して任意のWidgetでの使用を可能にする

riverpodへの乗り換えメリット

同じならば乗り換える必要がないのではないかと思うかもしれません。しかし、riverpodならではのメリットがあるのです。

  • コンパイルタイムでの依存性解決ができる(実行してみたらWidgetツリーの上部にProviderがない、といったことが起こらない)
    • それによる実行時エラーを減らすことができる
    • Widgetのコードの見通しが良くなるので保守性が向上する
  • コンストラクタから状態の更新ができる
    • ProxyProviderを使う必要がない。ref.watch()さえ書ければ依存関係にあるインスタンスを作り直すことができる
  • インスタンス参照のキーが型名だけではないので柔軟な管理が可能
    • 同じ型のproviderをいくつも用意できる
    • familyを使えばユーザーIDでインスタンスを分けることも可能でとても良い

特に一番上の、コンパイルタイムでの依存性解決が可能、というのはモバイルアプリ開発においてとても重要です。
モバイルアプリの場合、不具合が見つかってもストアの審査などがあるためすぐには修正版のリリースができません。
ランタイムではなく、コンパイルタイムで不具合発見の可能性を上げられるというのはありがたいことなのです。

riverpodのデメリット(providerの方が良いこと)

実際に使用してみて、逆にproviderの方が良いこともありました。

  • 記述はどうしても長くなる
    • 型推論が効く箇所での context.watch() はすごく記述量が少なくて済むんだと実感した
    • 型推論のせいでどんなインスタンスを取得しているのかわかりづらいケースもある
  • ConsumerWidgetなど特別なWidgetを使用する必要がある


総合的に見て、メリットの方が大きいと判断したため導入を決めました。

移行のステップ

導入を決めたとは言え、一息に導入できるわけではありません。
すでにproviderで管理していたクラスはたくさんありますし、他にもやりたい改修はいっぱいあります。
一度に置き換えるのは無理と判断し、順序を踏んで移行することにしました。

前提

youbrideアプリはレイヤードアーキテクチャを指向しています。
以下の層は下から上方法の依存関係を持ちます。

  • Repository層…通信、永続化(APIクライアントなど)
  • UseCase層…ドメインロジックの抽象化
  • View層…画面やアプリのWidget
    • View層内は変形MVVMによる状態管理
      • ViewModel…画面の状態を表現するデータ(freezedで作ったValueObject)
      • Controller…画面が行える処理の抽象化と状態保持(StateNotifier継承クラス)
      • Page / Component…画面を表現するWidgetとパーツ単位のWidget

このうち、Repository層とUseCase層は簡単に移行できると判断しました。
Repositoryはほとんど何にも依存していないですし、UseCaseはRepositoryに依存するだけ(しかも依存性注入はコンストラクタから行うようにしている)でした。
View層については、providerの特性上、各画面のWidgetをproviderのProviderでラップする作りになっています。したがって、画面ごとに移行する計画が立てられそうです。

そうなると段階的な移行が必要になります。
riverpodで管理しているインスタンスをproviderから使用する、ということはできるのでしょうか。
調査した結果がこちらになります。

zenn.dev

調査の結果、段階的な移行は可能と判断し、実行に移されることになりました。

実際の移行

Repository層 -> UseCase層 -> View層 の順で移行することにしました。

Repository層とUseCase層についてはひたすらにriverpod用のhogeRepositoryProviderを書いてゆき、先の記事に書いたブリッジ用のクラスでproviderでも使用できるようにしていきました。

View層は、始めは画面単位で移行することを計画していました。
が、複数画面間で共通して使用するProvider(Global State)と、特定の画面のみで使用するProvider(Local State)があり、Local Stateの一部はGlobal Stateに依存しているという関係ができあがっていました。

そのため、まずはGlobal Stateから移行し、次にLocal Stateを移行するという手順に変更。

Global Stateの変更はアプリ全体に影響が出るため、リリース前のテストを念入りに行う必要がある。そうなると、全部置き換えてしまってからテストした方が効率が良い、ということになっため、View層の置き換えは(他のタスクの手を止めて3-4日止めて)一息にやることになりました。

全部で1週間程度で終了することができました!

pubspec.yamlからproviderを削除したときの達成感がすごかったです。

providerからriverpodへ移行したことでインスタンスのライフサイクルが変わったことによる不具合がいくつか見つかりましたが、随時修正を行い、現在では問題なくなっています。

実装時に使える小技

providerのProviderのlazy: falseと同じことをしたい

アプリ起動時にすぐインスタンス化が始まって欲しいときなどに。
以下のようなクラスを作って、ProviderScope直下に置くことで実現できます。

class Instantiater extends ConsumerWidget {
  const Instantiater({
    required this.child,
    required this.toBeInstantiated,
    Key? key,
  }) : super(key: key);
  final Widget child;
  final List<ProviderBase> toBeInstantiated;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    toBeInstantiated.forEach((p) => ref.read(p));
    return child;
  }
}

/*
使い方
ProviderScope(
  child: Instantiater(
    toBeInstantiated: [
      // アプリ起動時にインスタンス化される
      hogeProvider,
    ],
    child: MyPage(),
  ),
);
*/

runAppより前に初期化しておきたいインスタンスの注入方法

shared_preferencesなど、Futureでしかリソースを取得できないものの取り扱い方は大きく下の二通りがあります。

  1. FutureProviderでインスタンスを管理する
  2. main内でawaitしてインスタンスを取得、runApp時にアプリケーションに引数から渡す

1. のFutureProviderだとインスタンスがAsyncValueでラップされてしまうため、他のクラスのコンストラクタから注入したいケースでは不便になります。
ref.read(sharedPreferencesProvider)とという形で使用できるようにしつつ 2. の方法を実現するにはどうしたら良いでしょうか。

riverpodにはProviderのoverride(Providerに紐づくインスタンスをProviderScope外から指定する)を行う機能があります。
これを使ってrunAppより前に初期化したいインスタンスを注入することにしました。

Future<void> main() async {
  // インスタンスはmainで生成してしまう。SharedPreferencesならそんなに時間かからないのでawaitしても問題ない
  final sp = await SharedPreferences.getInstance();
  retuen runApp(
    MyApp(
      sharedPreferences: sp,
    ),
  );
}

// ダミーのProviderを用意する
final sharedPreferencesProvider = Provider<SharedPreferences>((_) {
  throw throw UnimplementedError("アプリケーション起動時にmainでawaitして生成したインスタンスを使用する");
});

class MyApp extends StatelessWidget {
  ...
  Widget build(BuildContext context) {
    return ProviderScope(
        overrides : [
          // Providerが使用するインスタンスを指定する
          sharedPreferencesProvider.overrideWithValue(sharedPreferences),
        ],
        child: ...
    );
  }
}

移行してみて

「このHoge型はProviderで参照できると思ったのに、参照できなかった!」というランタイムエラーに遭遇する心配がなくなり、体感でも開発中にランタイムエラーに遭遇する頻度が減りました。

また、注入するインスタンスの依存関係がWidgetのツリー構造に縛られなくなったため、設計の自由度も向上しました。
現在はサーバーから受信したデータの新しい取り扱い方を検討している最中です。

ちゃんと最新のライブラリを使えているという気分の良さもあります。
(開発環境が新しい、というのはエンジニアの士気にも採用にも関わってきますからね!)

アプリの根幹に関わるライブラリを交換するのは手間ですが、交換によるメリットは大きいです。


youbrideではFlutterをもっと活用することを計画しています。
その上で得られる理解や開発するものはなるべく発信していく所存です。

今後もDiverseはコミュニティに知見を還元してゆきます\\ ٩( 'ω' )و //

redash×Slackでよろこびの声を自動通知!見せびらかし会レポート #5

こんにちは!Diverse広報担当です。

Diverseでは週に1回、エンジニア同士で「見せびらかし会」を開催しています。

f:id:diverse-tech:20210928172234p:plain
「見せびらかし会」とは、各エンジニアが1週間の中で頑張った開発Tipsを、仲間に向けて発表する場。
各自の知見やノウハウの共有を目的に行っています。”見せびらかす”という仰々しい名前ですが、ちょっとした工夫や改善も大歓迎!というラフな発表会です。

▼過去の見せびらかし会ブログ
見せびらかし会 カテゴリーの記事一覧 - Diverse developer blog

今回はエンジニアの青山さん(@yoichi_aoyama)とkumanomi(@m_kumanomi)さんの”見せびらかし”です。
ぜひお読みください!


▼目次

  • redashを使って、よろこびの声をSlackに通知する仕組み
    • 青山さんの今週の見せびらかしについて
    • どんな仕組みを作ったのか?
    • ―(一同拍手)ここから質問タイムです!
  • ユーザーのプロフィール単位でマッチ率を分析できる仕組み
    • kumanomiさんの今週の見せびらかしについて
  • 最後に

redashを使って、よろこびの声をSlackに通知する仕組み

青山さんの今週の見せびらかしについて

青山:今回の私の見せびらかしは、youbrideを成婚して退会してくれたユーザーさんのコメントを自動通知する仕組みです。
redashでクエリを実行して、その結果をSlackに通知する形にしました。
f:id:diverse-tech:20210928172648p:plain
直近の成婚退会者を抽出するクエリを用意し、そのクエリを実行してSlackに投げる部分はPythonで作っています。
redashのプラグインとして登録されていたPythonを使って実装しました。

続きを読む

SingleChildScrollViewでアニメーション実装!見せびらかし会レポート #4

こんにちは!Diverse広報担当です。

Diverseでは週に1回、エンジニア同士で「見せびらかし会」を開催しています。

f:id:diverse-tech:20210917162138p:plain
「見せびらかし会」とは、各エンジニアが1週間の中で頑張った開発Tipsを、仲間に向けて発表する場。
各自の知見やノウハウの共有を目的に行っています。”見せびらかす”という仰々しい名前ですが、ちょっとした工夫や改善も大歓迎!というラフな発表会です。

▼過去の見せびらかし会ブログ
見せびらかし会 カテゴリーの記事一覧 - Diverse developer blog

第4回目は、エンジニアoyaさんの”見せびらかし”と、oyaさんが感銘を受けたabuiさんの”見せびらかし”を紹介します。
ぜひお読みください!


▼目次

  • LIVE配信画面でテキストをアニメーションで出す方法
    • oyaさんの今週の見せびらかしについて
    • どうやってアニメーションを作ったのか?
    • 実装までの試行錯誤…ある日神が降りてきた!
  • UIImageViewのアニメーション実装
    • oyaさんが感銘を受けたabuiさんの見せびらかしを紹介!
    • どんな仕組みで作ったのか?
  • 最後に

LIVE配信画面でテキストをアニメーションで出す方法

oyaさんの今週の見せびらかしについて

oya:今回は、YYCのワイワイライブで使える「ワイワイBOX話題アニメーション」について紹介します。
※画面最下部にテキストを出せるアニメーションです。
f:id:diverse-tech:20210921134339g:plain

LIVE配信画面の場合、そもそもいろんな場所にオブジェクトが出されていますよね。
テキストを入れようにも2行以上にするのがそもそも難しく、チームメンバーからも「1行で入れるべき理由」を聞いていました。そういう制限の中で、頑張ってアニメーションを入れました!

続きを読む

スクラム開発への思い切ったスイッチが、良質なアウトプットにつながった

f:id:diverse-tech:20210915161710j:plain アジャイル開発のひとつである「スクラム開発」は、少人数の開発チームにおけるコミュニケーションを軸に開発を進めていく手法ですが、この度YYCチームでは、スクラム開発への完全移行を行いました。「チームメンバーが受け入れてくれたから思い切ったシフトができた」と話すのは、YYCのプロジェクトマネージャー兼エンジニアリングマネージャである藤田雄大(ふじた・ゆうた)さんです。

今回は、YYCのスクラム開発への移行の経緯と、移行したメリット、さらには見えてきた課題や今後の展望などを聞きました!

ウォーターフォール型の限界を感じつつ、なかなか踏み切れなかった

――これまではスクラム開発を導入する必要がなく、ウォーターフォール型の開発を行っていたのですか?

藤田:はい、正確にはウォーターフォール(マイクロマネジメント)に近い形で進めてきました。開発マネージャーが2人体制から1人体制になる組織的な変更があり、このままのでは対応できないと思いました。また、これまでの体制ではエンジニアやデザイナーがマネージャーからの指示を待って行動する。というマインドになってしまいDiverseのカルチャーデッキで求めている姿とのズレが生じていました。「ここで大きな転換が必要だな」と感じていました。そのタイミングで「スクラム開発を試すのはどうだろうか?」とPOからコメントを受けたのです。 実は、以前スクラム開発の導入に失敗した経験があり、正直なところ今回のスクラム開発導入にも不安はありました。しかし再度1から学び直したことで、本質を理解できていなかったことに気づくことができました。学びを通して改めて「YYCチームでスクラム開発を進めることができる」と確信したのです。

――なるほど…そしてスクラム開発へと踏み切ったのですね。

藤田:とはいえ、急にやります!と言ってできるものでもありません。まずはチームメンバーにスクラム開発についての説明からスタートしました。何回か勉強会を開き、スクラム開発についての知識を持ってもらう場を増やしました。「まずは教科書通り、スクラム開発のいろはに沿ってやりましょう。そこからYYCらしさを出しましょう」とメンバーに伝え、今年の4月からスタートすることになったんです。

f:id:diverse-tech:20210915162005j:plain

「チームで取り組む」という意識がアウトプットの質を高める結果に

――丁寧に、慎重に導入したのですね。スクラム開発において、大きく変わった点はどこですか?

藤田:大きく分けると2つ挙げられると思います。ひとつは、ミーティングの最適化です。これまでは、定例の朝会のほか、急に大きなミーティングが入ったりと突発的な対応が多かったのですが、これを改めました。木曜日を出社推奨日に決め、みんなで集まって疑問点や困りごとの解決を図るようにしたのです。大きなミーティングが入らない分、集中して作業に取り組めるようになりました。スピード感アップの一因になっています。

――もう一点は何ですか?

藤田:1ヶ月に1回行っていたタスクの振り返りを1週間に1度に変更。それに伴って1週間のスケジュールも細かく立てるようにしました。期間を短く設定したことで、「自分が今日何をすべきか」が見える化され、指示を待たずとも自ら進んで作業を行えるようになったのです。まさに自己組織化の第一歩です。

――クラウドでスケジュールを共有することによって、「誰が何をしているか」というのも見えやすくなったのですね。

藤田:そうですね。タスクの到達度を相対見積もりで表すようにしたことで、「○○さんは今日は4進んだんだな」「あれ、□□さんは2しか進んでないから何か困っているのかな?」とメンバーの進捗度にも意識が向くようになりました。これは嬉しい発見でしたね。「個人として、エンジニアとして施策を行う」ということから、「チームとして」という主語が新たに増えたのですから。チームでプロダクトを向上させるために、何ができるか?チームの一員としてパフォーマンスを上げよう、というのは結果的にアウトプットの質も向上させているように思います。

f:id:diverse-tech:20210915162454j:plain

Fail Fastを飛躍的に進める、大きなきっかけになった

――改めて、スクラム開発を導入して得られたメリットは大きいものがありますね。

藤田:そうですね、思い切って導入してよかったと思います(笑) これまではスケジュールを目算で決めていたこともあって、納期も3~4週間押してしまうことも多かったのですが、そういった不確実性は格段に減りました。やはりこれは、自分自身のキャパシティに応じて、1週間のスケジュールを細かく組んでいることが大きいと思います。仮にひとりがスケジュール通りの進行が難しい場合、他の誰かがヘルプに入るといった、チーム全体の意識も確実に変わってきています。

この段階まで到達するのに、さまざまな失敗パターンを積み重ねてきました。まさにFail Fast, Go Higherの精神を持ってメンバーみんなで作り上げてきたのです。そういった意味でも、スクラム開発を通してメンバー同士のつながりはより強いものになったのではないでしょうか。

――順調に進んでいるスクラム開発ですが、今後の課題や展望を教えてください。

藤田:直近の課題は「ベロシティ」の安定化ですね。まだまだブレが大きいのでスプリントを繰り返して改善していきたいですね。 また、スクラム開発の大前提であるコミュニケーションはもっと大事にしていきたいと思っています。slack文化ということもあり、テキストによるコミュニケーションがベースでしたが、「これ、ちょっとミーティングセットしたほうが早くない?」というのは、10分前後のミーティングを設けてすぐその場で解決するようにしています。そういう意味でもミーティングの最適化はどんどん進めていきたいですね。その先に、Diverseの目指しているMission/Visionを軸として、サービスの根幹にある価値をもっと掘り下げていきたいと思っています。やることはいっぱいありますね(笑)

――コミュニケーションを活発にするために、雑談を取り入れたとも聞いています。

藤田:はい(笑) 4月のスクラム開発から、朝のあいさつとともに今日のひとことを入れてもらうようにしました。「暑い」とか、「お昼ごはんはラーメン」でもなんでも良いのですが(笑)、その何気ないひとことから会話が始まり、「そういえば○○のことなんですけど…」といった仕事上のコミュニケーションにもアクセスしやすくなったように感じています。 スクラム開発はまだまだ道半ばですが、すでにスクラム開発を導入しているyoubrideチームと横の連携も強めながら、YYCらしさを構築できればいいなと考えています。

――藤田さん、お話ありがとうございました!

f:id:diverse-tech:20210915162858j:plain

さて、Diverseでは現在下記の職種を積極的に採用しております!

ご興味のある方はぜひお気軽にエントリーください!

herp.careers

herp.careers

herp.careers

マッチングサービスの「いいね!」パターンの管理とは?見せびらかし会レポート #3

こんにちは!Diverse広報担当です。

Diverseでは週に1回、エンジニア同士で「見せびらかし会」を開催しています。

f:id:diverse-tech:20210914172916p:plain
「見せびらかし会」とは、各エンジニアが1週間の中で頑張った開発Tipsを、仲間に向けて発表する場。
各自の知見やノウハウの共有を目的に行っています。”見せびらかす”という仰々しい名前ですが、ちょっとした工夫や改善も大歓迎!というラフな発表会です。

▼過去の見せびらかし会ブログ
見せびらかし会 カテゴリーの記事一覧 - Diverse developer blog

第3回目は、第1回目でも登場したエンジニア菊池さん(@kikuchy)の”見せびらかし”を紹介します。
ぜひお読みください!


▼目次

  • 「いいね!」ダイアログのパターンをEnumで管理したい
    • 菊池さんの今週の見せびらかしについて
    • どんな仕組みを作ったのか?
      • ―(一同拍手)ここから質問タイムです!
  • 最後に

「いいね!」ダイアログのパターンをEnumで管理したい

菊池さんの今週の見せびらかしについて

菊池:私の見せびらかしは、youbrideで新しく実装する「メッセージ付きいいね!」Dialogの管理方法を工夫したこと。
パターンが多いのですが、Dialogは1つなんです。

全く違うDialogとしてパターンの数だけクラスを作ることも可能ですが、そうすると再利用できるパーツをどうするかという問題が出てきます。

結局再利用可能なパーツはほぼ全体。パターンによって変わる部分は、タイトル・送信ボタンの色・テキストです。
その他、フットノートが異なってきます。

f:id:diverse-tech:20210914173236p:plain
また内容によっては、ファースト質問(「いいね!」を押す前に質問を設定できる機能)があったり、「プレミアムいいね!」化することができるという違いがあったりします。

続きを読む