for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

6ヶ月間取り組んできたアジャイルチームの改善活動を紹介します

こんにちは、フォースタートアップス株式会社のエンジニアの八巻(@hachimaki37)です。主にタレントエージェンシー支援システム(SFA/CRM)のシステム開発を担当し、フルスタックに開発を行なっております。

半年間に渡りチームの業務改善(以下、改善と呼ぶ)をリードして参りました。今回の記事では、具体的なHOWを中心に、どんな課題がありどのような狙いを持って改善に取り組んできたのかについて紹介します。

目次

8000文字を超える記事です。各項目で内容は完結しているため、一から熟読する必要はありません。気になる目次から飛んで拝読頂ければと思います。

以下の方々を対象としております

  • 改善をはじめて行う方
  • 何から手をつけていいか模索している方
  • 改善のHow Toを知りたい方
  • 具体的にどんな改善案があるのか知りたい方
  • 改善の勘所を知りたい方
  • 改善を始めた経緯・モチベーションを知りたい方

チームについて

メンバー

  • PDM: 1名
  • Engineer: 3名
  • SRE: 1名
  • Designer: 1名

計6名のチームです。小規模なチームのため、PDMが開発Issueを取ったり、エンジニアがプロジェクトリードしたり、SREがフロントエンドのIssueを取ったりと、ポジションはあれど横断的に開発を進めています。

開発スタイル

スクラムを採用しております。ただし、スクラムイベントをすべて行うのではなく、デイリースクラム、リファインメント、レトロスペクティブなど、必要なイベントをかい摘み実施しています。スプリントの期間は2週間です。

プロジェクト管理ツール

ZenHubを使用し、Issuesを管理しております。

改善に対するアンケート

半年間に渡り行ってきたチームの活動に対して、アンケートをとりました。アンケートは6項目になっています。そのうち、特に改善に対する結果がわかりやすかった3項目を紹介します。

※私を除く、5名のアンケート集計です。

アンケート結果に対する感想

今回の改善を通じて、チームが「良い方向に変化した」と感じられたメンバーが多くいたことはポジティブに捉えています。一方で、個々人について考えた際、仕組み化による負荷・新たに露見された課題など、まだまだ改善できることはあるなと思いました。

これまでに至ったストーリーを紹介します。

なぜ改善に取り組んだ?

「スプリントごとに価値のある有用なインクリメントをもっと生み出したい」と思ったためです。

チームには、EM・スクラムマスターはおらず、チーム全員で意見を出し合い、改善サイクルを回しております。価値あるインクリメントをもっと生み出していきたい、そんな想いをチームで持ちながら、現実は日々の業務に追われ、なかなか改善に手が回らないという状況でした。日々ルーティン化されたイベント、意義や目的を失ったミーティング、形骸化されたスクラムイベントなどに課題感を持ちました。

たとえば、デイリースクラム。本来の目的は、「スプリントゴール達成に向けた進捗を確認し、進捗にボトルネックがあれば、何かしらのリカバリーを図る」と認識しております。しかし、実際は「作業」をチーム内で共有するだけのイベントになっていました。

伸び代があるこの状況を改善すれば、もっと価値のあるインクリメントをチームで生み出せると思い、まずは半年間、積極的にチームの改善・チームビルディングを担い、取り組みました。

ベロシティの変化

あくまで参考値ですが、改善指標としてチームのベロシティ推移を見ていきました。

注記

ベロシティ増加を目的としておりません。図の波形は個々のスキルアップを始め、人数比や入れ替わり・CD改善なども起因していると考えるため、あくまで参考値としてご覧頂ければと思います。

結果的に、改善実施前から数ヶ月が経過した今、チームのベロシティ平均は上昇傾向になってきました。

やったこと

改善の存在意義を決める

最初に存在意義を定義します。それは、活動を進めていくにつれ、迷いや目的を見失うシーンが発生すると考えたからです。なぜ時間を割いて改善を行う必要があるのか?そのような状況下で支えになるのが、この存在意義です。物事の判断や方向性を示す羅針盤を作り、走り出しました。

アジャイル宣言の背後にある原則 では、「顧客満足を最優先し、価値のあるソフトウェアを早く・継続的に提供すること」を思想としております。これを目指していくためには、「安定的にパフォーマンスを出せるチーム構築」が重要だと考えました。そこで、これを改善の存在意義としました。

チームの課題感を探る

チームのボトルネックを探り、何を改善すれば水の流れは良くなるのかを考えました。

改善の2つのキーワード

組織改善についていろいろ調べていると、2つのキーワードに辿り着きました。1つは「自己組織化」、もう1つは「バーナード組織の3要素」です。

  • 自己組織化とは

    物質や個体が、系全体を俯瞰する能力を持たないのに関わらず、個々の自律的な振る舞いの結果として、秩序を持つ大きな構造を作り出す現象のことである。

引用元:自己組織化 - Wikipedia

つまり、変化し続ける外部環境や発生する課題に対して、組織を構成するメンバー一人ひとりが、主体的・自律的に適応することができる組織・チームを指します。

  • バーナード組織の3要素

    アメリカの経営学者チェスター・バーナードが提唱した、組織が成立するために必要な3つの条件のことを言い、(中略)「コミュニケーション」「貢献意欲」「共通目的」の三要素が不可欠であり、どれか一つでも欠けている場合には不完全な組織として、組織が健全に機能しなくなると定義づけました。

引用元:バーナードの組織の三要素とは?組織に必要な3つ要素を理解し導入するためのポイントを解説 | オンライン研修・人材育成 - Schoo(スクー)法人・企業向けサービス

つまり、「コミュニケーション」「協働の意欲」「共通の目標」の3つを一定水準満たした組織・チームを指します。

ご自身のチームは、どの領域にプロットされる?

これらについて、私の見解を以下にまとめました。

私の考えでは、チームが第4領域に達してから改善をスタートすべきです。自己組織化が目的ではありませんが、自己組織化されたチームでなければ改善を行ったとしても改善の効果を実感しづらいと考えます。第4領域は、主体的・自律的に適応することができる組織・チームです。第4領域にプロットされるチームの場合、改善の方向性を決め、ボトルネックを解消していくことで、大きく水の流れを改善できると考えます。

ご自身のチームがどの領域にプロットされるかにより、課題は異なり、改善内容も大きく変化します。チームの日々の振る舞いや観察、メンバーとのコミュニケーションからご自身のチームがどの領域にプロットされるかを考え、仮説を立てることがまずは重要だと考えます。

改善方針を決める

私たちのチームは、ある程度自己組織化されたチーム状態にあると考えたため、第4領域にプロットし、改善を進めていく形をとりました。改善方針としては、開発プロセスで弊害となる問題や課題を中心に「発見と解決」を繰り返すことがチームを前進させる上で最も重要だと考えました。新たに何かを取り組むよりも、まずは今行っているイベント群を徹底的に改善する方針としました。

具体的に行ったこと

※実施結果を書いておりますが、定量的に計ったものではありません。

※以下、抜粋した改善例です。これら以外にも小さな改善を含め、さまざまな改善サイクルをチームで回しております

ストーリーポイントの基準を再定義する

  • 課題

    • ポイントの基準が不明確
    • ポイントの基準理解が個々人で異なっていた
  • 影響

    • ポイントが大きいストーリーを見積もるときほど個々人でポイントの基準がズレていた
  • 打ち手

    • ポイントが2, 5, 13の課題となるストーリーポイントの基準を定義した

  • 結果
    • 複数の基準を定義したことで、ポイントの認識齟齬が減少した。それによってリファインメントのスムーズな進行が可能となった。また、見積もり時にメンバー間でポイントに差が生まれても基準の認識について議論する時間が減り、作業内容に着目した議論がよりできるようになった

Issueのテンプレートを作成する

  • 課題

    • Issueの記述内容にばらつきがある
  • 影響

    • 私達のチームではリファインメント開催ごとに書記をランダムに決めている。これは良い点もあるが、一方でIssueの詳細や内容にばらつきが見られ、情報の過不足に繋がり、実装漏れや仕様確認が適宜発生する悪い点もある
  • 打ち手

    • 開発者が「やりたい(実現したい)こと」を理解できるよう、Issueのテンプレート化とルールを作成。「日付」「ref」「チケットの作成理由」「チケットの完了定義」など、記述内容をテンプレート化し、Issueのデフォルトに設定した
    • 設定方法:リポジトリ用に Issue テンプレートを設定する - GitHub Docs

  • 結果
    • テンプレート化したことで、従来発生していた仕様確認や実装漏れが減少。その結果、Issueアサインから着手までの時間やコミュニケーションコスト、アウトプットの認識齟齬減少に繋がった

Velocity Trackingをチームに導入する

  • 課題
    • ベロシティに波がある(以下、参考データ)

  • 影響

    • スプリントゴールを安定的に達成していくことがもっとも重要であると考えている。ベロシティに波があることは一定ペースで価値を提供しきれていないということ
  • 打ち手

    • ベロシティの推移をチームで認識・理解する必要があると考え、ZenHubのVelocityTrackingをチームに導入、説明の機会を作った

  • 結果
    • 運用自体を再検討する運びとなり、失敗に終わる。人数比を始め、突発的なIssue対応やチームへの共有範囲、ベロシティ増加が目的ではなかったことなど、懸念点が多くペンディングとなった。当時をふりかえると、ベロシティを上げることに目が行きがちで、品質よりもリリースを先行してしまう心理状態となってしまったことが反省点。別の打ち手を現在模索中

チームメンバーの相互理解を深める機会を作る

  • 課題

    • 新規メンバーが既存メンバーへの隔たりを感じている
  • 影響

    • チームでは、新しくジョインするメンバーの自己紹介の場を作っていたが、古株になるにつれ、既存メンバーについて知る機会(誰が何をやってきて、どんなことが得意不得意で、何に興味があるのかなど)がなく、新規メンバーからすると既存メンバーへ隔たりを感じていた
  • 打ち手

    • 自己紹介の場に目的を作り、コンテンツを実施。相互理解を深める機会をチームに作った

  • 結果
    • 以前よりも意思疎通をストレスなくおこなえるようになった。また副次的な効果として、対話量が増加したことで、ミーティングなどで議論が活発になる機会が増加した

Issues整理とプロジェクト管理ツールの運用ルールを再定義する

※Pipelineとは

Issueの流れを表すレーンで、そのissueが完了するまでのステータスを表します。私達のチームでは、New Issues→Sprint Backlog→In Progress→Review/QA→Closedとしています。

  • 課題
    • 2〜3年前に切ったIssuesが散乱し、緊急度・重要度・対応すべきIssueなのか否かが不明確だった
    • 使用していないPipelineが多数存在する
    • プロジェクト管理ツールにルールがなく、スクラムボードが「スクラムボードの役割」を果たしていない

過去のPipelineの全体像

  • 影響

    • Pipelineの形骸化、一向にCloseされないIssuesが散乱し、無駄な確認作業が適宜発生する。Issueの優先順位や緊急度、および重要度をスクラムボードで理解することが難しく、適宜PDMに確認する必要があった
  • 打ち手

    • 既存Issues(150ほど)をIceBox or Closeに振り分けるスクラムボード大掃除会を数回に渡り実施
    • スクラムボードで「実行可能性を瞬時に伝達する」ことを目的に、Pipelineの改善と運用ルールを定義した

改善後のPipelineの全体像

以下運用ルールを定義

  • New Issues

    • 新しいIssueを配置
    • 次のスプリント計画を立てるまでに、極力Issueをレビューし、優先度(Icebox, Product Backlog, Sprint Backlog)を振り分ける
    • リファインメントを通じて、このパイプラインを極力空にする
  • Icebox

    • やった方が良いが着手する優先度は低いIssueを配置
    • スプリント中に想定されたIssuesが完了した場合などに着手する
  • Product Backlog

    • 今スプリントでは対応しないが、次回以降にやるべき優先度の高いIssuesを配置
    • 次のスプリント計画に使ってもOKとする
  • Sprint Backlog

    • スプリント期間中に終わらせる優先度が高いIssuesを配置
    • 次のスプリントまでに空になっていることが望ましい
  • In Progress

    • 進行中のIssueを配置
    • 必ずアサインされた担当者(このIssueを完了にする責任を負う)が設定される
  • Review/QA

    • EngineerによるReview期間にあるIssueを配置
    • 開発はすでに完了している状態とする
  • Design Review

    • Engineer Reviewが完了し、DesignerによるReview期間にあるIssueを配置
    • UI周りを中心にReviewがなされる
  • Closed

    • PRがmain branchにMergeされたIssueを配置
    • 対応不要だったIssueなどを配置
  • 結果

    • スクラムボードで、Issuesをマネジメントできるようになり、自律的な判断が可能となった。結果、主体的にIssueに取り組める仕組みができ、(体感ではあるが)Issueアサインから着手までの時間短縮に繋がった

ふりかえり

半年間にわたり、チームを巻き込みながら改善を進めて参りました。チームの協力がなければ進めることはできません。

また、チームの時間を使うということは、チームにとって良い影響を生み出していかなければなりません。もっとこうしたら良いのではないか?こうするべきではないか?メンバーからさまざまなアイディアを頂く中で、結局成し遂げたいことは何なのか。

それは、「安定的にパフォーマンスを出せるチーム構築」です。改善の存在意義を決めていたことで、これを達成するためには何が必要か、何を優先とすべきかが明確になったように思います。

たとえば、OpenAPIの更新負荷や使用ツール代替案の検討という提案がありましたが、改善の存在意義の『安定的にパフォーマンスを出せるチーム構築』とは違う問題だと判断し、実施を見送りました。やらないことを判断することができ、改善の本質に時間を割くことができたと考えます。

チームとしてどうあるべきか、どういうバリューを持って突き進むべきか、まだまだ改善できることはたくさんあると思います。

うまくいかないという名の成長痛を感じながら、チームでアイディアを出し合い、より良いチームへの変貌を遂げられるよう、これからも日々精進していきたいと思います。

あとがき

最後に採用情報です。

当社では、まだまだ採用募集中です。ぜひ一緒に成長していきませんか。ご興味ありましたらぜひ一度カジュアルにお話できたらと思います。

採用ページはこちら。(冒頭のTwitterにDM頂いてもOKです!)

Mojoってみた

初めまして、2023年2月にフォースタートアップス株式会社に入社したモジョモジョドレミことモジョリアンの江種(@hairinhi)と申します。現在、主にRuby on Railsで作られている社内向けプロダクト「タレントエージェンシー支援システム(SFA/CRM)」の開発、運用を担当しております。

はじめに

Mojoとは、Python構文の長所とシステムプログラミングおよびメタプログラミングを組み合わせることによって生み出された、研究と運用の間のギャップを埋める新しいプログラミング言語です。公式ドキュメントには、Mojoを使用すると、C言語よりも高速でPythonのエコシステムとシームレスに相互運用できる移植可能なコードを作成できると書かれています。

また、公式サイトではPythonよりも最大35000倍速く計算できると謳われています。Pythonのエコシステムを引き継ぎつつ高速に実行できるということで、AI分野での活躍が特に期待できますね。

今回はこのMojoPythonと比較して、本当に35000倍速く計算できるのか確認してみたいと思います。

Mojoを試すには

現在(2023年7月時点)、Mojoは開発元のModular社が用意しているJupyterHub環境にあるMojo PlayGroundでしか試すことができません。ここでは、利用できるようになるための手順を簡単にご紹介します。

1. ユーザー登録

まず、こちらのサインアップページからユーザー登録を行なってください。

2. 招待メールを受け取る

ユーザー登録を行なって、数日待つと登録したメールアドレスに招待メールが届きます。早ければ、登録したその日に招待メールが来ることもあります。

3. Mojo Playgroundにアクセスする

招待メールにある「Access Mojo Playground」のリンクにアクセスすると、JupyterHub環境が起動して、Mojoを利用できるようになります。

性能比較をしてみました

このMojoPythonよりもどのぐらい高速なのか気になったので、3つの簡単なアルゴリズムで性能比較をしてみました。

FizzBuzz問題
皆さんお馴染みのFizzBuzz問題で検証してみました。1 ~ 100までの数で試しています。

フィボナッチ数列
フィボナッチ数列もご存じの方が多いと思います。スクラム開発で各タスクにストーリーポイントを割り振る時にも使われますね。今回は30番目の値を求めてみます。

アッカーマン関数
アッカーマン関数とは計算可能であるが原始再帰的ではない、well-definedTotal Functionの最も単純な関数であり、さらに1900年代初期に信じられていた「計算可能な関数はすべて原始再帰的でもある」という考えに対する反例でもあります。この関数は指数関数や多重指数関数よりも急速に増大していく関数であることが知られています。今回は第一引数を3、第二引数を12にして試してみました。

ご注意!
今回、MojoについてはMojo Playgroundで、Pythonについては私が普段業務で使用している以下のようなスペックのマシンで試しています。

MacBook Pro 2021
チップ: Apple M1 Max
メモリ: 64GB
macOS: Ventura 13.0
Pythonのバージョン: 3.11.1

よって、実行環境がMojoPythonで違うので厳密な比較にはなっておりません。また、処理時間の計測にPythonではtimeitモジュールを使用し、Mojoでは処理開始時刻と終了時刻の差を計算しています。

あくまで参考程度にご覧ください。

Python

FizzBuzz問題

from timeit import timeit

def fizz_buzz(n):
    if n % 15 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

def benchmark():
    for i in range(1, 101):
        fizz_buzz(i)

result = timeit(lambda: benchmark(), number = 1)
print(result * 1000000000, "[ナノ秒]")

結果

1
2
Fizz
4
Buzz
Fizz
〜〜途中省略〜〜
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz
136792.19409823418 [ナノ秒]

フィボナッチ数列

from timeit import timeit

def fib(n):
    if n < 2:
        return n
    else:
        return fib(n-1) + fib(n-2)

def benchmark():
    print(fib(30))

result = timeit(lambda: benchmark(), number = 1)
print(result * 1000000000, "[ナノ秒]")

結果

832040
98048625.04824996 [ナノ秒]

アッカーマン関数

import sys
from timeit import timeit

# Pythonの場合再帰回数の上限を上げておく必要があります
sys.setrecursionlimit(200000000)

def ack(m, n):
    if m == 0:
        return n + 1
    elif n == 0:
        return ack(m - 1, 1)
    else:
        return ack(m - 1, ack(m, n - 1))

def benchmark():
    print(ack(3, 12))

result = timeit(lambda: benchmark(), number = 1)
print(result * 1000000000, "[ナノ秒]")

結果

32765
45427333666.943016 [ナノ秒]

Mojo

FizzBuzz問題

from Time import now

fn fizz_buzz(n: Int):
    if n % 15 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

start = now()
for i in range(1,101):
    fizz_buzz(i)
end = now()

print(end - start, '[ナノ秒]')

結果

1
2
Fizz
4
Buzz
Fizz
〜〜途中省略〜〜
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz
362569 [ナノ秒]

フィボナッチ数列

from Time import now

fn fib(n: Int) -> Int:
    if n < 2:
        return n
    else:
        return fib(n-1) + fib(n-2)

start = now()
print(fib(30))
end = now()

print(end - start, "[ナノ秒]")

結果

832040
2563558 [ナノ秒]

アッカーマン関数

from Time import now

def ack(m: Int, n: Int) -> Int:
    if m == 0:
        return n + 1
    elif n == 0:
        return ack(m - 1, 1)
    else:
        return ack(m - 1, ack(m, n - 1))

start = now()
print(ack(3, 12))
end = now()

print(end - start, "[ナノ秒]")

結果

32765
4242469743 [ナノ秒]

比較

実行結果を表にまとめてみました。(単位はナノ秒)

アルゴリズム Pythonの結果 Mojoの結果 比較
FizzBuzz問題 136792.19409823418 362569 Pythonのほうが約2.6倍速い
フィボナッチ数列 98048625.04824996 2563558 Mojoのほうが約38倍速い
アッカーマン関数 45427333666.943016 4242469743 Mojoのほうが約10倍速い

FizzBuzz問題については、意外にもPythonのほうが速かったです。これはMojoの標準出力がボトルネックになっていると思われます。それ以外については、期待通りMojoのほうが速い結果となりました。

考察

公式サイトにあるようにMojoのほうが 35000倍速い! という結果にはなりませんでした。そこで、もう一度公式サイトを見ると、比較表の下に注意書きでInstance AWS r7iz.metal-16xl Intel Xeonと書かれていました。AWSのEC2インスタンスの中でR7izインスタンスというと、全コア最大ターボ周波数が3.9GHzの第4世代Intel Xeonスケーラブルプロセッサを搭載した初のEC2インスタンスとして知られています。またR7izインスタンスは、最大1,024GiBのメモリと最大128のvCPUを搭載しているので、金融・保険業界やデータ分析など高いパフォーマンスが求められる環境において最大限そのパフォーマンスを発揮することができます。

このことから言えるのは、Mojoは実行環境が良くなればなるほど、その真価を発揮できる可能性があるということです。今回の検証ではJupyterHub環境とMacBook Proを使用しました。「Pythonよりは速い」という程度の結果を得ることができましたが、一方で「まだまだ俺の力はこんなものじゃない」とMojoが語りかけているようにも思えました。

まとめ

FizzBuzz問題の結果は意外でしたが、概ねMojoのほうが速いということが分かりました。普段の業務では、AI関連の業務に関わることは少ないですが、個人的に非常に興味がある分野でもあります。これからもMojoについてはキャッチアップしていきたいと思います。

採用

最後に採用情報です。 当社では、まだまだ採用募集中です。ぜひ一緒にMojoについて語り合いましょう!ご興味ありましたらぜひ一度カジュアルにお話できたらと思います。 採用ページはこちら

参考資料

https://zenn.dev/turing_motors/articles/e23973714c3ecf
https://www.kurims.kyoto-u.ac.jp/~cs/cs2011_terui.pdf

Rubyでテンプレートメソッド活用してみた

こんにちは、エンジニアの杉谷です。
普段はSTARTUP DBチームにて、主にバックエンドエンジニアとしてSTARTUP DBの開発をしています。
今回はSTARTUP DBの開発において、自身が最近学習していたGoFデザインパターンの1つであるテンプレートメソッドを活用して新機能の実装を行なったので、その取り組みについてお話しさせて頂きます。

テンプレートメソッドとは?

そもそもテンプレートメソッドとは何なのかお話しします。

テンプレートメソッドとは、オブジェクト指向プログラミングにおいて、抽象クラスにアルゴリズムの骨格を定義し、具象クラスで具体的な実装を定義する為のデザインパターンです。

下記の図を用いて、より具体的な説明と使用する際のイメージを説明したいと思います。

template_method概略図

図左記にある、AbstractClassというのが抽象クラスに当たります。

ここではtemplate()というメソッドが定義されており、さらにその中でprimitive_template1()primitive_template2()が定義されています。

このtemplate()アルゴリズムの骨格を表しています。

続いてConcreteClassAConcreteClassBですが、これらが具象クラスに当たります。 それぞれAbstractClassを継承していることが図から見て分かるかと思います。

これらのクラスでは、抽象メソッドprimitive_operation1primitive_operation2を実装することで、共通のアルゴリズムをそれぞれ具体化しています。

例えばConcreteClassAを使用する際の処理の流れとしてはこのようになります。

  • ConcreteClassA.new().templateConcreteClassAインスタンスを作成しtemplateを呼び出す。
  • templateの中で、ConcreteClassAprimitive_operation1を呼び出す。
  • ConcreteClassAprimitive_operation1が実行される。
  • templateの中で、ConcreteClassAprimitive_operation2を呼び出す。
  • ConcreteClassAprimitive_operation2が実行される。
  • ConcreteClassBを使用する際は、ConcreteClassB.new().templateConcreteClassBに書かれたprimitive_operation1primitive_operation2の定義内容が処理されます。

以上のことからテンプレートメソッドには下記のようなメリットがあることが伺えます。

再利用性が高い:共通のアルゴリズムを抽象クラスに定義することで、各サブクラスで具体的な処理を実装することができます。このため、アルゴリズムの流れは共通化され、再利用性が高くなります。

拡張性が高い:新しいサブクラスを作成することで、アルゴリズムの流れを拡張することができます。具体的な処理が異なるだけで、アルゴリズムの流れは共通しているため、既存のアルゴリズムに新しい処理を追加することも容易です。

カプセル化が実現できる:共通のアルゴリズムを抽象クラスにカプセル化することで、各サブクラスで具体的な処理を実装するだけで済みます。この為、アルゴリズムの詳細は各サブクラス内に内包され、カプセル化が実現できます。

このテンプレートメソッドは、アルゴリズムの流れを共通化する必要がある場合・具体的な処理を異なるサブクラスで実装する必要がある場合において活用されます。

このテンプレートメソッドを活用する場面としては、例えばログイン認証などが挙げられます。ユーザー名とパスワードで認証する場合と、OAuthで認証する場合とでは、認証の具体的な処理が異なります。共通のアルゴリズムであるログイン処理を抽象クラスに定義し、各認証方式で異なる具体的な処理をサブクラスで実装することが考えられます。

何故テンプレートメソッドを用いたのか?

まず初めに、どんな新機能を開発していたのか概要と経緯を簡単にご説明します。

今回実装したのは「PDFダウンロード機能」というもので、STARTUP DBに掲載されいてる企業の詳細情報をPDFで出力することを可能にする機能になります。「スタートアップならびに投資家ページにて、詳細データをエクセルでダウンロードができるものの、多くの情報をエクセルで確認し、社内共有するには適していなかった」といったお声を多く頂き、今回の実装に取り組みました。

下記がSTARTUP DBが提供するPDFのサンプル画像になります。

pdf_sample

今回テンプレートメソッドを用いて実装に取り組んだのには大きく2つの理由がありました。

1つ目に、各項目間で大まかなアルゴリズムが共通していたので重複箇所をなるべく減らしたかった事が挙げられます。

下記が具体的な共通するアルゴリズムの内容になります。

  • 大見出しを出力する
  • 項目名を出力する
  • 項目毎にそれぞれ情報を入力する
  • 間隔を空ける

サンプルPDFを再度見て頂くと分かるかと思いますが、デザイン自体は各項目ほぼ統一されており、違いとしては出力される情報のみでした。テンプレートメソッドを活用することで、大まかなアルゴリズムの流れが定義されたクラスを作成することが出来、テンプレートメソッドのメリットを十分享受出来ると考えました。

2つ目に、保守性・拡張性が高い機能を実装することが出来ることが挙げられます。

PDFダウンロード機能はSTARTUP DBにおいて、今後も改修される可能性が比較的高い機能ということもあり、実装にあたり保守のし易さや掲載する情報が増えた際の拡張のし易さが設計において重要な点でした。

テンプレートメソッドを活用すれば、抽象クラスを参照することで大まかな処理の内容を理解する事が出来るので、保守がし易くなります。また新しい情報を追加する場合は、その情報に関する処理内容を抽象クラスを継承したサブクラスを新たに作れば済みます。前述の2点からテンプレートメソッドを活用する事で重要な点をカバーすることが出来ると判断しました。

以上の2点から今回の実装でテンプレートメソッドを用いることを決めました。

テンプレートメソッドを用いて実装してみた

下記が実装した抽象クラスと具象クラスのコードになります。

<抽象クラス>

class Download::Company::Pdf::Contentdef sequence
    # 大見出しを出力する
    output_headline
    output_total_amount
    # 項目名を出力する
    output_item_name
    # 項目毎にそれぞれ情報を入力する
    output_contents
    output_limit_number_notification
    # 間隔を空ける
    output_interval
    ...
  end

  def output_headline; end

  def output_total_amount; end

  def output_item_name; end

  def output_contents; end

  def output_limit_number_notification; end

  def output_interval; end

  def output_horizontal_line; end

  def sequence_for_outline; end

  def sequence_for_finance_topics; end

  def sequence_for_funds; end

  def sequence_for_news; end# NOTE: タイトル名出力
  def headline(text_size, name, down_space)
    font_size(text_size)
    @pdf.text name.to_s
    move_down(down_space)
  end

  def total_amount(method)
    move_down(5)
    font_size(TEXT_SIZE[:small_headline])
    @pdf.text method
    move_down(10)
  end
  
  # NOTE: 下幅間隔調整
  def move_down(down_space)
    @pdf.move_down(down_space)
  end

  # NOTE: リンクのURL作成と活性化
  def embed_link_and_activate_link(url_name, url)
    return '-' if url_name.blank?
    return url_name if url.blank?

    @pdf.make_cell(content: to_html(url, url_name), inline_format: true)
  end

  # NOTE: 複数の要素を1つの文字列に結合
  def compile_to_a_list(multiple_hashes)
    return '-' if multiple_hashes.blank?

    hashes_list = multiple_hashes.pluck(:name)
    hashes_list.join(',')
  end

  # NOTE: 境界線
  def horizontal_line
    move_down(30)
    @pdf.stroke_color 'e3e3e3'
    @pdf.stroke_horizontal_rule
    move_down(30)
  end

  # NOTE: 備考欄の表示形式の整形
  def compile_to_a_list_for_note(notes)
    return '-' if notes.blank?

    notes_list = notes.map { |note| note }
    notes_list.join('/')
  end

  def investor_type_classification(lead_investors)
    return '-' if lead_investors.blank?

    company_lead_investors = []
    officer_lead_investors = []
    fund_lead_investors = []
    lead_investors.each do |investor|
      case investor[:investor_type]
      when 'Company'
        company_lead_investors << investor
      when 'Officer'
        officer_lead_investors << investor
      else
        fund_lead_investors << investor
      end
    end
    companies = convert_to_an_array_of_link_tags(company_lead_investors, 'Company')
    officers = convert_to_an_array_of_link_tags(officer_lead_investors, 'Officer')
    funds = convert_to_an_array_of_link_tags(fund_lead_investors, 'Fund')
    content = [companies, officers, funds].flatten.uniq.join(",\n")
    @pdf.make_cell(content:, inline_format: true)
  end

  # NOTE: 日付の出力整形
  def convert_date_format(date)
    return '-' if date.blank?

    format_date = date.to_s&.gsub(%r{/|\.|-}, '')
    if format_date.length >= 8
      if format_date[4, 2] == '00'
        format_date[0, 4]
      elsif format_date[6, 2] == '00'
        format_date[0, 6].insert(4, '.')
      else
        begin
          I18n.l(format_date&.to_date, format: :hyphon)
        rescue StandardError => e
          Rollbar.delay.warning(e.message)
        end
      end
    else
      format_date
    end
  end

end

<具象クラス>

# NOTE:サービス情報
class Download::Company::Pdf::Contents::Services < Download::Company::Pdf::Content
  
  def initialize(pdf, company)
    @pdf = pdf
    @company = company
  end

  private

  def output_headline
    headline(TEXT_SIZE[:large_headline], PDF_SHEETS[:services][:title], 20)
  end

  def output_item_name
    item_name(PDF_SHEETS[:services][:fields].values, COLUMN_WIDTH[:services])
  end

  def output_contents
    @company[:services].each do |service|
      @pdf.table(
        [
          [
            embed_link_and_activate_link(service[:name], service[:url]),
            service[:description],
            convert_date_format(service[:published_at]),
            compile_to_a_list(service[:tags])
          ]
        ], column_widths: COLUMN_WIDTH[:services]
      ) do |table|
        table.cells.size = TEXT_SIZE[:default]
      end
    end
  end

  def output_horizontal_line
    horizontal_line
  end

end

抽象クラスであるDownload::Company::Pdf::Contentに共通するアルゴリズムsequenceメソッド内で定義しています。Download::Company::Pdf::Contents::Servicesではsequence内のメソッドをオーバーライドしそれぞれの処理を記載しています。sequenceメソッドでは、一部の具象クラスだけで使用する例外的なメソッドも定義しています。

例えば下記サンプル画像にある資金調達情報ではサービス情報では定義されていなかったoutput_total_amountが呼び出されています。

<具象クラス>

# NOTE:資金調達情報
class Download::Company::Pdf::Contents::Investments < Download::Company::Pdf::Content

  def initialize(pdf, company)
    @pdf = pdf
    @company = company
  end

  private

  def output_headline
    headline(TEXT_SIZE[:middle_headline], PDF_SHEETS[:finance][:investments][:title], 5)
  end

  def output_total_amount
    # NOTE: 合計資金調達金額
    total_amount(funding_total_amount)
  end

  def output_item_name
    item_name(PDF_SHEETS[:finance][:investments][:fields].values, COLUMN_WIDTH[:investments])
  end

  def output_contents
    @company[:investments].each do |investment|
      @pdf.table(
        [
          [
            convert_date_format(investment[:funded_at]),
            output_investment_amount(investment[:funding_amount]),
            investor_type_classification(investment[:lead_investors]),
            investor_type_classification(investment[:investors]),
            compile_to_a_list_for_note(investment[:notes_romaji])
          ]
        ], column_widths: COLUMN_WIDTH[:investments]
      ) do |table|
        table.cells.size = TEXT_SIZE[:default]
      end
    end
  end

  def output_interval
    interval_for_each_item
  end

  def funding_total_amount
    funding_amount = Download::Company::CellValueCorrector::Detail::Pdf.funding_amount(is_funded: @company[:is_funded],
                                                                                       unsigned_million_unit_amount: @company[:funding_amount])
    ['百万円未満', '-'].include?(funding_amount) ? "合計資金調達金額:#{funding_amount}" : "合計資金調達金額:#{funding_amount}(百万円)"
  end

  def output_investment_amount(million_unit_amount)
    Download::Company::CellValueCorrector::Detail::Pdf.investment_amount(million_unit_amount)
  end

end

本来は下記のように定義し、具象クラスにオーバーライドを義務付けをすることで処理の統一性を保ちます。もし、抽象クラスに定義されているメソッドを具象クラスが定義していない場合は、実行時エラーが発生します。

def template
  primitive_template1
  primitive_template2
end

def primitive_template1
  raise "#{__method__} must be implemented"
end

def primitive_template2
  raise "#{__method__} must be implemented"
end

しかし今回の実装では、大体の枠組みをテンプレートメソッドで定義し、抽象クラスを手順書のように扱い今後PDFダウンロード機能が改修される際に、理解のし易い構造にしたいという思いがあったので、空メソッドで定義し具象クラス側で必要な処理を定義する方針で実装しました。

実装してみて

「今日の自分のコードは明日の他人のコード」という言葉があるように、自身の書いたコードを振り返った際に読み返せないようでは良い設計とは言えないです。

実装を終え、数ヶ月ぶりに改めてコードを読み返し、抽象クラスに処理内容がまとまっていることで直ぐに処理の概要を掴むことが出来る点はテンプレートメソッドを活用して良かったなと改めて感じました。

また各具象クラスが何をしているのか、クラス単位でまとまっているのでそれぞれの項目でどのような処理が行われているのかも、理解しやすいと感じました。ありがたい事に、レビューをして頂いた方からは処理内容が理解し易かったというお言葉も頂きました。

一方で、自身でコードの再レビューを通して改善したい点が出て来ているので、引き続き自身のコーディングスキルを向上させるべく精進していきたいと思います。

Rubyを2.7.1から3.1.4にアップデートした話と3.2.2を諦めたわけ

こんにちは。2022年12月入社の石田です。STARTUP DBの主にバックエンドのエンジニアをしています。今回はSTARTUP DBのバックエンドのRubyのバージョンアップをした話をしたいと思います。

2023/03/31をもってRuby 2.7系のサポートが終了しました。当プロジェクトでは Ruby 2.7.1を使っていたため、急遽Rubyのアップデート作業が必要に。どうせアップデートが必要ならということで、可能な限り最新のバージョンまでアップデートすることになりました。

アップデートは以下のように段階を踏んで行うことにしました。

2.7.1 → 2.7.8 → 3.0.6 → 3.1.4 → 3.2.2

以下は、Rubyのアップデートをしていく中で遭遇したエラーとその対応の記録です。

Ruby 2.7.1 → Ruby 2.7.8

何事もなくアップデート完了。 動作も問題なし。

Ruby 2.7.8 → Ruby 3.0.6

URI.escapeが削除された影響

当プロジェクトでは未だにpaperclipを使っています。このpaperclipがエラーを出しました。 Ruby 3.0でURI.escapeが削除されたのですが、paperclipではまだ使われているようです。 このgemはすでにメンテナンスされていないため、他のgemに移行が必要なんですが、工数が必要なため今回は諦めてモンキーパッチを当てて対応することにしました。 以下、モンキーパッチです。こちらからお借りしました。

config/initializers/uri.rb

# frozen_string_literal: true

require 'uri'

#
# Ruby 3.0.0でURI.escapeメソッドが削除された。
# paperclipがこれに対応していないため、他のgemに移行するまでモンキーパッチを当てる。
# https://stackoverflow.com/questions/68174351/undefined-method-escape-for-urimodule
#

module URI
  class << self
    def escape(str)
      alpha = 'a-zA-Z'
      alnum = "#{alpha}\\d"
      unreserved = "\\-_.!~*'()#{alnum}"
      reserved = ';/?:@&=+$,\\[\\]'
      unsafe = Regexp.new("[^#{unreserved}#{reserved}]")
      str.gsub(unsafe) do
        us = Regexp.last_match(0)
        tmp = ''
        us.each_byte do |uc|
          tmp += format('%%%02X', uc)
        end
        tmp
      end.force_encoding(Encoding::US_ASCII)
    end
  end
end

rss gemの追加

Ruby 3.0から rssライブラリがbundled gemsになりました。これは素直にGemfileに追加することにしました。

Gemfile

gem 'rss'

elasticsearch gemのアップデート

elasticsearchが以下のようなエラーを出しました。

wrong number of arguments (given 1, expected 0)
/usr/local/bundle/gems/activerecord-6.1.6.1/lib/active_record/relation/batches.rb:128:in `find_in_batches’
/usr/local/bundle/gems/elasticsearch-model-5.1.0/lib/elasticsearch/model/adapters/active_record.rb:105:in `__find_in_batches’
/usr/local/bundle/gems/elasticsearch-model-5.1.0/lib/elasticsearch/model/importing.rb:122:in `import’

これはRuby 3.0の位置引数とキーワード引数の変更の影響のようです。 こちらのコミットで修正されていますので、このコミットが入ったバージョンまでelasticsearchをバージョンアップしました。

elasticsearch gemのダウングレード

先程、elasticsearch gemを最新までアップデートしたのですが、これが良くなかったようです。ローカルでは動作するのですが、STG環境で以下のエラーが発生しました。

Elasticsearch::UnsupportedProductError: The client noticed that the server is not a supported distribution of Elasticsearch.

当プロジェクトではSTG環境及び本番環境はAWSを使っておりElasticsearchもAWSが提供するものを使っています。

AWSはあるバージョンからElasticsearchの呼称をやめ、OpenSearchと呼び改めました。またElasticsearchはクライアントライブラリに「接続先がElasticsearchじゃないとエラーになる」修正を加えました。この変更が加えられたのがelasticsearch gemのバージョンでは7.14系になります。なのでelasticsearch gemのバージョンが7.14以上のものを使ってOpenSearchに接続しようとするとエラーが発生したんですね。これについては以下の文献などが詳しかったです。

解決方法は文献のとおりにelasticsearch gemのバージョンを14より下のバージョンを使うことです。

以下のように対応しました。

Gemfile

gem 'elasticsearch', '7.13.3'
gem 'elasticsearch-model', '7.2.1'
gem 'elasticsearch-rails', '7.2.1'

fixture_file_uploadのDEPRECATION WARNING

以下のようなWarningが出るようになりました。

DEPRECATION WARNING: Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.

これは以下のような単純な置換で対応できます。

# before
fixture_file_upload('150x150.png', 'image/png')

# after
Rack::Test::UploadedFile.new('spec/fixtures/150x150.png', 'image/png')

Ruby 3.0.6 → Ruby 3.1.4

bundled gemsの追加

Ruby 3.1から以下のgemがbundled gemになりました。bundlerで使う場合はGemfileに明示的に指定する必要があります。

  • net-ftp 0.1.3
  • net-imap 0.2.2
  • net-pop 0.1.1
  • net-smtp 0.3.1
  • matrix 0.4.2
  • prime 0.1.2
  • debug 1.4.0

当プロジェクトでは上記のうちnet-ftpとprime以外は使っていたのでGemfileに追加しました。

psych 3系の追加

db:migrate時に以下のエラーが発生しました。

Psych::BadAlias: Unknown alias: default

これはpsychというYAML解釈用のGemの4系と3系で解釈が変わっているのが原因です。psychのバージョンを3系に固定してあげれば解消します。

Gemfile

gem 'psych', '~> 3.1'

feedjira gemのアップデート

当プロジェクトではrssでデータを収集するためにfeedjiraというgemを使っています。もともとこのgemのバージョンが2.1.4のものを使っていたのですが、Ruby 3.1に対応していなかったので最新の3.2.2に上げました。それに伴って使い方も変わったのでコード修正を行います。

# 古い書き方
feed = Feedjira::Feed.fetch_and_parse(url)

# 新しい書き方
xml = HTTParty.get(url).body
feed = Feedjira.parse(xml)

ここで問題が発生しました。一部のurlでHTTParty.get(url)をしたときに403エラーが返ってきてしまうのです。ブラウザで開いてもcurlでたたいても正常なのになぜ?調査してもちゃんとした原因はわかりませんでしたが、以下のようにheaderをつけると回避できることがわかりました。

HTTParty.get(url, headers: { 'User-Agent' => 'Chrome/58.0.3029.110' }).body

bundlerのupdate

以下のようなWarningが出るようになりました。

Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.

これはbundlerを最新バージョンにアップデートすれば出なくなります。

bundlerのアップデート方法は以下です。

bundle update --bundler

Dockerを使っている場合などでbundlerのバージョンを固定したい場合は以下のようにします。

Dockerfile

ENV BUNDLER_VERSION=2.4.12
RUN gem install bundler --no-document -v 2.4.12

config.action_mailer.delivery_jobの設定

メーラーが動作しないバグに遭遇しました。メソッドの引数の数が正しくない旨のエラーだったのですが、ソースコードを見た限りでは間違いは無さそうでした。結果としては同時に出ていた以下のWarningに対応したら正しく動作するようになりました。

DEPRECATION WARNING: Sending mail with DeliveryJob and Parameterized::DeliveryJob is deprecated and will be removed in Rails 6.2. Please use MailDeliveryJob instead. (called from block in execute at /usr/local/lib/Ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:281)

対応方法は以下の通りです。

config/application.rb

config.action_mailer.delivery_job = 'ActionMailer::MailDeliveryJob'

ただ、この修正の方法が良かったのかは正直わかりません。とりあえず動くようにはなりましたが、相変わらず使っているのはMailDeliveryJobではなくDeliveryJobなので、ここは別途対応が必要だと思います。

Ruby 3.1.4 → Ruby 3.2.3

unicornの不具合

Rubyを3.2.3にあげて、いくつかのgemをバージョンアップしたところCIも通り、ローカルでは問題なく動作するようになりました。そこでSTG環境で動作確認したのですが、問題が発生しました。一部APIが正常に動作していないようなのです。しかもrailsのログにはエラーなどは出力されていません。色々原因を探ったのですが結果としてはunicornが原因でした。railsにはエラーが出ていませんでしたが、unicornはエラーを出していたのです。

unicorn_error.log

E, [2023-04-13T17:42:39.079818 #29845] ERROR -- : app error: undefined method `=~' for 5:Integer (NoMethodError)

unicornはもうメンテナンスされていないgemです。Ruby 3.2には対応していません。そのためunicornをpumaなどに置き換えない限りRuby 3.2へのアップデートは不可能です。今回はRuby 3.2へのアップデートは諦めることにしました。

2023/07/06 追記

unicornはもうメンテナンスされていないgemです。」と書きましたが。現在も開発が続いているようです。 以下のリポジトリは開発中のリポジトリGitHub上にミラーリングされたものになります(これはrubygems.orgのページからはたどり着けないので注意)。

GitHub - defunkt/unicorn: Unofficial Unicorn Mirror.

このことは中村 涼(r7kamura)さんから教えてもらいました。 この場を借りてお礼申し上げます。誠にありがとうございます。

また、unicorn_error.logの内容について、レスポンスヘッダーの値に本来利用すべきではない5などの値が入っていないかどうか確認することも助言頂きました。 重ねてお礼申し上げます。

ただ、上記GitHubリポジトリの最新が本番運用できるものなのかどうか私では判断できませんでした(バージョンタグなどもついていない)。 そのため、件のGitHubリポジトリの最新unicornを使うようにするのか、pumaなどに置き換えるのかはチーム検討中です。

追記終わり。

終わりに

Ruby 3.2まではアップデートできませんでしたが、もともとの目的であるメンテナンスされなくなった2.7系から脱却することは達成しました。

ここまで3週間とちょっとかかりました。

ここにたどり着くまでには、ここでは書ききれなかった困難がたくさんありました。一部を箇条書きで概要だけ残しておきます。

  • アップデート作業中にRubyのパッチバージョンのアップデートがあった
  • bundlerのアップデート作業が終わった直後にbundlerのアップデートがあった
  • bundlerの再アップデート作業が終わった直後(翌日)にまたbundlerのアップデートがあった
  • Ruby のバージョンを上げた場合はunicornをUSR2ではなくQUITで終了して立ち上げ直さなければならない仕様
  • Ruby 3.1をSTG完了にデプロイしたところMySQLで謎のLost connectionやmalformed packetが発生。しかもRuby 2.7に戻しても直らなくなった。(これはRESET QUERY CACHEを実行することで解消することがわかりました。)
  • feedjiraのエラー対応は、さも事前に対応した風に書いているが、実は本番リリース後に発覚&対応した。

数々の困難がありましたが無事にアップデートが完了できたのはチームの皆さんのおかげです。私はひたすらバグを潰していただけですが、その裏では

  • rspecで拾いきれないバグのためにQA表の作成と実施
  • ダウンタイムが発生しないリリース手順の作成と実行
  • その他トラブル対応

などの作業がありました。これは私1人では到底できないことです。チームの皆さんにはこの場を借りて感謝申し上げます。

採用

当社ではエンジニアを募集中です。ぜひ一緒に成長していきませんか。ご興味ありましたらぜひ一度カジュアルにお話できたらと思います。

採用ページはこちら