for Startups Tech blog

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

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人では到底できないことです。チームの皆さんにはこの場を借りて感謝申し上げます。

採用

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

採用ページはこちら

ドメイン駆動設計の中核は「Design」である。近い未来に訪れる組織変化に「DDD」は最適なソリューションになり得るのか

こんにちは、2022年4月にフォースタートアップスにジョインしたエンジニアの八巻(@hachimaki37)です。主にタレントエージェンシー支援システム(SFA/CRM)のシステム開発を担当しております。現在所属するチームでは、サーバサイド(Ruby,RoR)、フロントエンド(Vue.js)の役割を分けず、2週間のスプリントを切って開発を行なっております。

少し前から興味が湧いていたドメイン駆動設計(以下、DDDと呼ぶ)、ありがたいことに外部研修の参加を募るアナウンスがあったため、DDD Boot Campという外部研修を受講してきました。

詳細は後述しますが、きっかけは、近い未来に訪れる当社の一課題をDDDで解決できないか?と思ったことです。また私自身、DDDについて言葉や概念をなんとなく知っている程度であったため、実践に役立つ知識を養いたいという思いで参加してきました。

今回のテックブログは、近い未来に訪れる組織変化を考えながら、DDDとは何かを初め、DDD Boot Campを受講して見えた学びや見解、そしてアクションなどについて執筆していきたいと思います。

※注記:タレントエージェンシー支援システム(SFA/CRM)とは?

時より、この言葉を使用しておりますが、社内のさまざまな部門の方々が使う業務システムとご認識頂ければと思います。

DDD Boot Campとは?

本題に入る前に、DDD Boot Campとは何かについて簡単に説明します。

講師情報:和智 右桂さん

  • 独学ではなかなか全容がつかみにくいドメイン駆動設計についてわかりやすく学べる
  • ワークショップ形式での体験を通じ業務にいかせる実践力を身につける

業務フローやシナリオは多くの現場で使われていますが、漫然と書いていても、今ひとつぴんと来ない、ということになりがちです。 単に手を動かして成果物を作るだけでなく、きちんと理解してそれを共有するためにはどうすればよいのか、ドメイン駆動設計(DDD)のバイブルでもある『エリック・エヴァンスのドメイン駆動設計』(翔泳社刊)翻訳者でもある和智右桂氏を講師に迎え、DDDにヒントを得ながら、ワークショップ形式でポイントを学ぶ講座です。

引用元:https://event.shoeisha.jp/cza/ddd

私が参加した回は、参加者20名のオンライン実施。講義に参加してみて、DDDの概念を一から知れたこと、ワークショップがあったことで、知識が一人歩きせず、業務との接続イメージが沸き、全体を通して非常に学び多い時間となりました。

近い未来に訪れる変化と課題

それは、システム利用ユーザーの変化によるコミュニケーションの肥大化です。

以下、当社 2023年3月期 通期決算説明資料から情報を抜粋しています。

引用元:https://pdf.irpocket.com/C7089/bU43/fyL8/CPUq.pdf

入社から現在にかけて、タレントエージェンシー支援システム(SFA/CRM)のシステム開発を担当しておりますが、組織拡大に伴い、今後様々な問題が出てくることが想定されます。その一つが社員数純増に伴う問題です。上記資料を見てわかるように、社員数が前期末比 +51名純増であること、そして4月以降更なる純増が見込まれます。

そこで、現在もなお顕在化しつつある問題の一つが、入社比率の変化です。具体的には、業務システムに慣れてない新人の方々が社員数全体の約40%となる近い未来が見えており、生産性の低下が大きな課題となってきています。

引用元:https://pdf.irpocket.com/C7089/bU43/fyL8/CPUq.pdf

現行システムでは機能しないのか?

決してそう言うことではありません。この先を見据えた進化、それはもっと未来にフィットさせたプロダクトへの進化が必要なのではないかという事です。

あくまで個人的な考えですが、タレントエージェンシー支援システム(SFA/CRM)は、ローンチされてからすでに数年が経っております。当初のプロダクトビジョンと今向かうべきビジョンにギャップが出始めているのではないかと考えております。それもそのはずで、会社としてのフェーズ・組織デザイン(人数・体制)が変われば、システムの課題や価値も自ずと変わっていくことでしょう。

未来に向けた新たなプロダクトビジョン

実は最近、「シンプル」と「安全」をキーワードにした新しいプロダクトビジョンが定義されました。つまりそれは、次の未来へ繋げる「意志」と「価値提供」がプロダクトに吹き込まれたという事です。そして、私どものチームでは、以下テーマを設定し、開発を進める形となりました。

  • 人員数の増加(業務システムに慣れてない新人比率の上昇)に対応できる環境
  • 業務プロセスの改善(効率的な業務進行)

これらの問題と課題を鑑みた上で、方向性を決め、システム開発に繋げていく必要があります。

どう解決していくのか

とある日、Bizからアイディアを募る連絡が所属チーム内にありました。

エンジニアチームで見える、タレントエージェンシー支援システム(SFA/CRM)の改善ポイント、ブレストレベルで構いませんので、ぜひアイディアをよろしくお願いします!

的外れなアイディアも多々ありますが、いくつか考え、Bizへ提案してみました。

テーマ:情報を「探す」プロセスを改善する

数ヶ月前に情報の「集約」と「透明化」を目的とした、比較的大きいリリースが行われました。次に課題となるのは「情報伝達」だと考えました。情報を人へ伝え届けるためには、「探す → 見つける → (自身が)理解する → (論理や体系を立てる) → 伝える → (相手が)理解する → 伝わる」といったプロセスがあると考えており、そもそも「探す」行為に相当なコストがかかっている(当社ではUXリサーチを実施しており、そこで得た情報から見えてきた課題感)のではないかと考えました。

情報収集のプロセスを改善することで、業務プロセスの改善(効率的な業務進行)に繋がると考えました。

テーマ:ユビキタス言語を使う

人の増加や入社時期の違いから、コミュニケーションコストが以前に比べ、倍以上に増加することが想定されます。なぜなら、バックグラウンドが人によって異なるため、言葉の定義や理解の違いから意思疎通が困難になるからです。

つまり、本来使うべき時間的コストが失われるため、共通言語化を図り、コミュニケーションコストを極力減らしていく策が良いのではないかと考えました。

など、さまざまなアイディアを考えてみました。

うっすら見えてきた課題感と解決の光

Bizとの共通認識である課題、それはユビキタス言語(共通言語)の活用です。「共通言語が少ない、もしくは合っていないか」そういった課題をBizは抱えておりました。私どもの新たなプロダクトビジョンであるシンプルとは何か。詳細は割愛させて頂きますが、一つの意味合いには「言葉の意味整理と統一(ユビキタス言語)」という概念があります。

少し糸口が見えてきた気持ちでした。そして、その課題解決を実現する手段の一つがDDDであると考えました。ようやく、DDDの話です。

ここからは、DDDとは何かを初め、参加してみての気づきや学びについて書いていきたいと思います。(具体的な方法論は述べていません)

重要な観点

ドメイン駆動設計は、「Design」である。よく見かける〇DD(例えば、TDD, MDD, UCDD…)は、Development(開発やテスト)の話ですが、Domain-driven design(DDD)は、開発手法ではなく、デザイン手法・設計の話であることを念頭に置く必要があります。よく出会う問題とそれにうまく対処するための設計であり、将来の変更や発展性に耐え得るかというアーキテクチャ的観点が必要です。

学んだこと

大きく下記4点です。

1. ソフトウェアには、業務知識を反映させる

DDDとは、エリック・エヴァンス氏により提唱され、一言で言うと「ソフトウェアには、業務知識を反映させましょう」という概念です。具体的には、頭の中に構成している業務知識を抽象化して反映させることです。ただし注意点があり、単なる業務知識を反映させるのではなく、不要な概念や知識が省かれ、「選び抜かれている」点がポイントです。

2. 隠れた概念を見える化し、ドメインモデルに(境界線を)反映させる

「どういう業務であるか」という概念の見える化を行う手法を様々知ることができました。例えば、業務フロー図、ユースケース図、ロバストネス図などです。ここでのポイントは、選び抜かれた業務知識の業務構造に関する理解を、「そのままソフトウェアで表現する」ことです。 業務知識を表現することで、業務理解とプログラム設計を直接的に関連づけられることで、プログラムがわかりやすく整理させ、ソフトウェアを柔軟に拡張したり、安全な変更を可能とします。

3. 1と2を実現するには、ユビキタス言語(共通言語)の認識と理解が重要である

Devだけで、完結するものではありません。Bizが選び抜いた業務知識を反映するだけでは物足りません。DevとBiz、双方がドメインモデル(選び抜かれた業務知識)への共通認識と理解が重要なのです。そのためにはまず、業務フローの中で使われている言葉の定義と認識を合わせることが、ファーストステップとして必要です。

4. 大切という感覚は、抽象化した自分の解釈における価値観である

言葉スケッチというアイスブレイクを冒頭に行いました。1対Nの関係で、1がお題となる写真を言葉だけでNに伝え、その情報を元にNがスケッチするといった内容です。私は、Nの立場で参加しましたが、思いの外難しい。1の立場からすると、そもそも描かれている全ての情報を説明することは困難であり、目に映るモノの中で、大切だと思うモノを自身で選択して説明しています。つまり、伝えている情報は、そのモノ自体ではなく、自分の理解(=モデル)なのです。

ユビキタス言語の認識と理解を合わせるためには、言葉だけのコミュニケーションではなく、上記で紹介した〇〇図などを使い、双方の概念やモデルの見える化を行い、共通のイメージを合わせる過程が必要です。どの言葉を選択し、どのように相手の頭の中を整理しながらモノごとを伝えていくのか、このような「言葉で作りたいモノを伝える」ことは非常に重要なスキルです。

実践してみようと思うこと

1. 言葉の意味整理と統一(ユビキタス言語)

ここは欠かせないと感じました。DDDをやるやらないに関わらず、言葉の共通理解の重要性を改めて感じました。まずはチーム内から、そしてユーザーとのコミュニケーションをユビキタス言語で図れる世界に近づけるよう、ファーストステップを踏んでいきたいと思います。

2. 業務フロー図、ユースケース図、ロバストネス図、ホワイトボードなどを使って、図でイメージを伝える

私自身、(例えばロジックなど)テキストや言葉だけで結構伝えていたなぁと痛感しました。つまり、イメージの理解がおそらく合っていないということです。伝え方もあるかとは思いますが、根本的に各々が見ている視界や視点(DevとBizなども含めて)が異なるため、捉え方・理解の仕方がバラバラになっていると感じました。共通のイメージや認識を持てるよう、図や表現でイメージを伝え、見ている世界がより鮮明になるようにしていきたいと思います。

3. ソフトウェアには、業務知識を反映させる

共通言語の認識を合わせた上で、設計してみるといった内容です。今までは、Dev視点でDevにとってわかりやすいER、クラス、命名などにしておりましたが、少なからず業務フローで使用している言葉を使い、共通認識を持って進めていきたいと思います。その前のファーストステップは、ユビキタス言語やコンテキストマップ作成などから計画的に進められると良いかもしれません。

まとめ

これら3点が少しずつ浸透し、形になって行くだけでも、前述した組織の一課題解決に近づいていくのではないかと考えます。

講義を聞いていて、DDD=オブジェクト指向ではないか?と思った節はありましたが、オブジェクト指向との違いは、やはり「ドメインモデルがエンティティに反映されているかどうか」が大きな違いであることを知りました。何度も言いますが、DDDとは、ソフトウェアに業務知識を反映させることが重要です。つまり、「現実 → モデル → ドメインモデル → コード」を実践することで、ソフトウェアの価値を高めることを目指すものなのです。

ただし、全てDDDにすればいいじゃん!というものでもありません。不向きなシステム、不向きな開発サイクル、場合によっては損益分岐点を下回る可能性もあります。つまり、ドメイン駆動設計は、「Development」ではなく、「Design」であることを忘れてはいけないということです。

何をしたいのか、そのために何が必要なのかを考え、どう実現するかの順番が重要です。

あとがき

現状から見る課題のみならず、未来を見据え、何ができるのか、何をしなければならないのかを考え、一つ一つ課題解決に取り組んでいきたいと思います。またDDDは、まだまだ実践できるほどの知識ではないので、引き続き学んでいきたいと思います。

最後に採用情報です。

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

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

参考資料