DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

複式簿記のアーキテクチャを理解する

この記事ははじめてのアドベントカレンダー Advent Calendar 2023 23日目の記事です。

こんにちは。DIGGLEのCTO水上です。アドカレのために何を書こうか悩んでいたら投稿前日になってしまいました。 悩んだ末、社内勉強会で一度話した内容を書くことにしました。(省力化・・)

まえがき

フリーランス等で、少し複式簿記をかじったことがある方は、とりあえずネットで調べた方法で仕訳を書いてみたりしたものの、結局本質的に何をやっているのかがよく分からないという方も多いのではないでしょうか。 私も過去にフリーランスだった時期があり、自分で理屈を理解するためにスプレッドシートで実装してみたりしていました。色々と書いてみた経験を通して、私は自分なりに本質的な仕組みがどういうものなのかを理解することができましたが、軽く触れてもそれを理解しづらかった要因として、検索しても普遍的な仕組みの説明にたどりつくことはほぼなく、財務的なルールに関する記事がほとんどだからではないかと感じています。

そこで、(私の知る限りで)複式簿記のシステムのうち最も基礎となる部分だけにフォーカスして、技術的(あるいは数理的)に抽象化して切り離し、データベース上で実装することを通じて、エンジニアにとって馴染み深い形で仕組みを理解する手助けになるような記事を書いてみました。

※ その道の専門家ではないので、細かい部分での誤りがありましたらご指摘いただけると幸いです。

定義と設計

データベース上で実装するといっても、設計なしには何も記述できません。まずはいくつかの概念を定義しながら、具体の実装方針を検討していきます。

※定義が難解に感じるようであれば、実装の部分まで読み飛ばしていただいて構いません

お金を定義する

ひとまずお金という概念を定義します。お金は、

  • 定量的に認識できる(つまり、100や200など私たちの知っている数字表記で表せるということ)
  • この世界のお金の総量は、増えたり減ったりしない

という性質をみたすものとします。 総量が変わらないという制約はともかく、技術的にはInteger型を採用することにすれば要件を概ね満たすものとなるでしょう。実は、総量が変わらないという制約は複式簿記と最も大きく関係する特徴ですが、それは最後に触れます。

箱を定義する

つぎに、 を定義します。箱は、

  • 箱の中には、お金が無数に存在し(しかし無限ではない!)、誰かがそこからお金を動かしたとしても無くなるようなことはない
  • この世界のすべてのお金は、いつもどれか一つの箱に入っている
  • この世界には箱が複数存在できる

とします。

各箱は、常に無数のお金が入っているわけですが、操作をして底が尽きることがないということを考えて、初期値を0と考え、マイナス値になっても扱えるということにしてしまいましょう。(体重計に箱を載せた状態でONにするようなイメージ)

たとえば世界に箱が3つしかないとき、その箱の全体は、(0, 0, 0)と表すことができそうです。ただ、単に底を突く心配をしなくても良いように扱いたいだけなので、初期値がいくらなのかというのは重要ではありません。

(お金の)移動を定義する

次に、箱から箱へのお金の移動について定義します。移動は

  • 移動元の箱
  • 移動先の箱
  • 金額

で定められます。

素朴にコードに落としてみるとすると、イメージとしては { from: Box, to: Box, amount: Integer } のような構造体が想像できると思います。実装上はこの方式を採用します。

一方で、別の表現方法として、各箱の増減分を並べたものでも表すことができます。たとえば、3つ箱がある世界で、1つめの箱から2つめの箱へ、50移動することを表すには (-50, +50, 0) と表すことができます。箱と同じようにInteger型のタプルで表現可能になります。数学でいう点とベクトルみたいなものですね。

集計結果を定義する

移動が何度も起きるわけですが、結果として各箱がどれくらい増減したのか、というものを 集計結果 ということにします。 集計結果というものを具体的にどういうユースケースで、何を意味するものとして使うのかは後半の例で紹介します。

ここで、移動をタプル=ベクトルで表現できるメリットとして、ベクトルの自然な加法に基づいて、足し合わせを表現することができる点があります。つまり移動Aと移動Bを両方行うことを、単にA+Bで表現できます。実際、1つめの箱から2つめの箱へ、50移動し、2つめの箱から3つめの箱へ、30移動することは (-50, +50, 0) + (0, -30, +30) = (-50, +20, +30) で表すことができます。その意味で、移動の足し合わせもまた広義には移動ということにします。この世界のお金が増えたり減ったりしないという条件は、この移動を表すベクトルの要素の総和が0になっていることから保証されます。

上記の (-50, +20, +30) という移動は、それぞれの箱について50減少、20増加、30増加という増減があったということになります。

後のために少しだけ符号の扱いについて取り決めておきます。

  • ある集計結果を ある箱の 増加分 であると定義する場合、移動の合計を計算した上で、当該の箱の増減値そのものを指すとします
  • ある集計結果を ある箱の 減少分 であると定義する場合、移動の合計を計算した上で、当該の箱の増減値から符号を反転した値を指すものとします

実装上では移動はタプルではなく { from: Box, to: Box, amount: Integer } の表現形式を使うので、これを足し合わせた結果に相当する集計結果は少しややこしくなりますが、下記の擬似コードのようなイメージで計算します。

N個目の箱の増加分 の場合

箱に入ってきた分(to)から出ていった分(from)を引きます

擬似コード例: moves.filter(m => m.to == N).sum() - moves.filter(m => m.from == N).sum()

N個目の箱の減少分 の場合

箱から出ていった分(from)から箱に入ってきた分(to)を引きます

擬似コード例: moves.filter(m => m.from == N).sum() - moves.filter(m => m.to == N).sum()

実装する

お金はInteger、そして箱は識別可能ならば十分であることを考えて、箱は単に文字列リテラルで表現するとしましょう。そうすると、さっそく移動から実装し始められます。

CREATE TABLE moves
( "from" TEXT NOT NULL
, "to" TEXT NOT NULL
, "amount" INTEGER NOT NULL
)
;

集計結果は、次のようなSQLで表現できます。

箱"foo"の増加分 の場合

SELECT
( (SELECT SUM(amount) FROM moves m WHERE m."to" = 'foo')
- (SELECT SUM(amount) FROM moves m WHERE m."from" = 'foo')
)

箱"foo"の減少分 の場合

SELECT
( (SELECT SUM(amount) FROM moves m WHERE m."from" = 'foo')
- (SELECT SUM(amount) FROM moves m WHERE m."to" = 'foo')
)

以上で実装の基本形は完了です!

ここまでの実装だけでは、一体何が実現できるのか全くわかりません。そこで、次から簡単な例をもとに模擬的なデータを構成してみます。

例①:3つの箱でP/L(損益計算書)を表現する

P/L損益計算書)とは、ある期間における収益と費用、そしてその差し引きの利益を表示した表のことです。

まずは、3つの箱 売上現金費用 を用意します。

次に移動データを使って、事業活動を次のルールで表現してみましょう。

  • 売上が発生した場合、 売上 から 現金 への移動 で表現する
  • 費用が発生した場合、 現金 から 費用 への移動 で表現する

以下はサンプルのデータです。 (摘要欄のようなカラムがないので、代わりにコメントを入れていますが、どのようなデータであるか何となくイメージできると思います。)

INSERT INTO moves("from", "to", "amount")
VALUES
  ('現金', '費用', 600) -- 諸々の初期費用がかかりました
, ('現金', '費用', 500) -- 仕入れがありました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 300) -- 300円のクッキーが売れました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 500) -- 500円のケーキが売れました
, ('売上', '現金', 200) -- 200円のコーヒーが売れました
, ('売上', '現金', 300) -- 300円のクッキーが売れました
, ('現金', '費用', 600) -- 給料を支払いました
;

最後に、簡単なP/Lとして、収益、コストを集計結果として以下で定義します。

  • 収益 = 売上 の箱 の 減少
  • コスト = 費用 の箱 の 増加

使いまわしやすくするためにVIEWにしてしまいましょう。

-- 収益 = 売上 の箱 の減少分
CREATE VIEW sales(value) AS
  SELECT
  ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '売上')
  - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '売上')
  ) AS value
;
-- コスト = 費用 の箱 の増加分
CREATE VIEW cost(value) AS
  SELECT
  ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '費用')
  - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '費用')
  ) AS value
;

最後に上記の差し引きとして利益を算出してみます。結果を整形すると次のようになります。

収益1,900
コスト1,700
利益200

しかし、ここまでの話ならば、箱の移動などと面倒なことを考えず、単に売上と費用をそれぞれ別々に計上すればよさそうですよね。そこで次の例を紹介します。

例②:5つの箱でB/S(貸借対照表)を表現する

B/S貸借対照表)とは、ある時点における資産の内訳と資産の帰属する先(あるいは、調達方法)をそれぞれ左右に並べた表のことです。この表の左右それぞれの合計は一致(バランス)するべきで、資金の提供元(右側)から見て、資金が現在どういう状態になっているのか(左側)を表します。

前の例と同じ要領で、5つの箱 売上費用資産負債純資産 の箱を用意します。

事業活動の内容を次のように表現します。

  • 売上が発生した場合、 売上 から 資産 への移動 で表現する
  • 費用が発生した場合、 資産 から 費用 への移動 で表現する
  • (銀行などからの借り入れにより)資金の貸し手に帰属する資産が発生した場合、 負債 から 資産 への移動 で表現する
  • (資本金や株式による調達により)株主に帰属する資産が発生した場合、 純資産 から 資産 への移動 で表現する

以下はサンプルのデータです。

INSERT INTO moves("from", "to", "amount")
VALUES
  ('純資産', '資産', 10000000) --  事業開始のための初期投資
, ('負債', '資産', 5000000) -- 事業開始のための借り入れ
, ('資産', '費用', 3000000) -- 費用発生など
, ('売上', '資産', 1000000) -- 売上
, ('資産', '費用', 50000) -- 借り入れ利息の支払い
, ('資産', '費用', 1500000) -- 費用発生など
, ('売上', '資産', 1300000) -- 売上
, ('資産', '費用', 50000) -- 借り入れ利息の支払い
, ('資産', '費用', 1200000) -- 費用発生など
, ('売上', '資産', 2000000) -- 売上
, ('資産', '費用', 50000) -- 借り入れ利息の支払い
;

以上を用いて、ひとまずP/Lを出力してみましょう。実は、前の章のP/Lを出力するときのSQLから形を変える必要は一切ありません。

収益4,300,000
コスト5,850,000
利益-1,550,000

これに加えて、いわゆるB/Sを作ってみます。集計結果を次のように定義します。

  • 資産 = 資産 の箱 の増加
  • 負債 = 負債 の箱 の 減少
  • 純資産 = 純資産 の箱 の 減少

同じくVIEWを作ってみます。

-- 資産 = 資産 の箱 の増加分
CREATE VIEW assets(value) AS
  SELECT
    ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '資産')
    - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '資産')
    ) AS value
;
-- 負債 = 負債 の箱 の減少分
CREATE VIEW liabilities(value) AS
  SELECT
    ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '負債')
    - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '負債')
    ) AS value
;
-- 純資産 = 純資産 の箱 の減少分
CREATE VIEW equities(value) AS
  SELECT
    ( (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."from" = '純資産')
    - (SELECT COALESCE(SUM(amount), 0) FROM moves m WHERE m."to" = '純資産')
    ) AS value
;

これらの結果を集計すると、次のようになります。

資産13,450,000負債5,000,000
純資産10,000,000
資産合計13,450,000負債+純資産合計15,000,000

はい、バランスしていませんね。差額の1,550,000はどこにいるかというと、実はP/L側の利益にいます。P/Lで利益がマイナスであるということは、純資産が縮小しなければいけません。そこで、P/Lの利益は純資産に振り替える(一般には利益剰余金という純資産科目に)移動が必要があります。このためのルールを新たに2つ追加します。この移動は、実際に決算振替仕訳と言われています。

  • 売上を確定する場合、 純資産 から 売上 への移動で表現する
  • 費用を確定する場合、 費用 から 純資産 への移動で表現する

売上と費用を確定させるため、P/Lの結果を参照しながら次のようなデータを作成します。

INSERT INTO moves("from", "to", "amount")
VALUES
  ('純資産', '売上', 4300000) -- 決算振替仕訳による売上の確定
, ('費用', '純資産', 5850000) -- 決算振替仕訳による費用の確定
;

上記を追加した上で、改めてP/LとB/Sを計算してみると、次のようになります。

P/L

収益0
コスト0
利益0

B/S

資産13,450,000負債5,000,000
純資産8,450,000
資産合計13,450,000負債+純資産合計13,450,000

これでB/Sがバランスしました。一方でP/Lが0になってしまいました。 変に思うかもしれませんが、もし再度P/Lを表示したい場合には、決算振替仕訳を含めないで集計すればいいだけなので、P/LとB/Sを表示するという観点からいえば、これで十分といえるでしょう。

複式簿記との関係性

実は、この記事で定義した 移動 が複式簿記における 仕訳 と同じものになります。さらに 移動元移動先 はそれぞれ 貸方借方 に相当します。(貸し手から借り手への移動だと考えると、分かりやすいですよね)

通常の簿記ではより細かく勘定科目が分かれているものですが、単に箱を細分化していくだけで同じことを説明することができます。

さらに、 貸借平均の原理 とよばれる次の式があります。

資産 + 費用 = 負債 + 純資産 + 収益

ここで実は、左辺に来ているものは、箱の 増加 分で、右辺に来ているものは、箱の 減少 分として本記事において定義したものでした。減少分であるということは符号を反転させれば増加分を表しているわけなので

資産 + 費用 + (-負債) + (-純資産) + (-収益) = 0

と書き換えると、これは各箱からの(広義の)移動があったとしても、お金が増えたり減ったりはしないという制約条件そのものを指していると言えます。

まとめ

それで、一体このアーキテクチャのなにが嬉しいのでしょうか?まず一つが、P/LもB/Sも、この仕訳の集合体を唯一の入力として写像できるという点にあります。今回例には載せていませんが、集計の仕方や箱の設定を工夫するだけで、例えばP/Lといっても発生主義・実現主義・現金主義など、それぞれ微妙に異なる見方に写像できたりします。このように、入力データのシンプルさを保ちつつ、多くのアウトプットに対応することができるのが強い利点になります。もう一つは一貫性を持てることです。売上の帳簿と財布の帳簿がそれぞれ別々のデータだと、売上の発生と財布への追加が一致しているようにデータを担保しないといけませんが、全てにおいて移動を根拠にしている場合はそのような心配をする必要はありません。

実は、複式簿記の本質は、開発者にとっては単に自分自身で会計処理をする際の理解をスムーズにできることだけではなく、一つのデータ表現手段として他の領域にも設計上応用できる可能性を秘めていると思います。例えば、お金以外の定量データの場面や、フローとストックの表現が必要な場面などがあります。

さいごに

この記事で紹介している実装をsqliteで実行可能な形でGitHub上に公開しておきました。 GitHub - mizukami234/simple-DEB

DIGGLEでは、管理会計という(複式簿記とは遠からずも近からずな)領域において、最高のプロダクトを作るメンバーを募集しています。興味がありましたら、是非お声がけください!

herp.careers

自前のReact UI コンポーネントライブラリを Tailwind CSS で再構築した話

"The logo for the React open source JavaScript library" © Facebook (Licensed under CC BY 1.0)
"Tailwind CSS Logo" © Tailwind CSS (Licensed under CC BY 4.0)

この記事はReact Advent Calendar 2023 23日目の記事です。

はじめに

弊社DIGGLEではフロントエンド開発に向けたUIコンポーネントライブラリ*1を用意しています。

開発者はUIコンポーネントライブラリ上に用意されているコンポーネントを用いてUIを構成することができるため、 作業の効率化や一定以上のデザインの一貫性を担保できます。 私自身用意されたコンポーネントベースにUIの構築を行うことでデザインに関しての悩み事が削減でき、より本質的な部分について取り組むことができていると感じています。

UIコンポーネントライブラリはDIGGLEの持っているデザインシステムの一部として、デザイナーとエンジニア両名にとって重要な役割を担ってくれています。

以前まで、DIGGLEではそのUIコンポーネントライブラリをSemantic UI Reactをベースに組んでいました。 react.semantic-ui.com

ですがシステムや組織が大きくなるにつれて、Semantic UI Reactでは対応が難しい部分が見えてきました。 現在その難しい部分の解消に向けてデザインコンポーネントをTailwind CSSを用いて刷新する取り組みを行なっています。

tailwindcss.com

今回は、なぜDIGGLEではUIコンポーネントライブラリのベースをSemantic UI ReactからTailwind CSSに移したのか?という点についてお話しさせていただきます。

Semantic UI ReactベースのUIコンポーネントライブラリについて

Semantic UI React は Semantic UI というCSSフレームワークをベースに構築されたUIコンポーネントライブラリです。

semantic-ui.com

DIGGLEにおいての利用方法は、基本Semantic UI ReactをベースにDIGGLEでの使い方にマッチするよう薄くラップしたり、UIで頻出するコンポーネントを用意しておくといったものでした。

DIGGLEでの使い方にマッチするよう薄くラップ

Semantic UI ReactはとてもよくできたUIコンポーネントライブラリではあるのですが、 汎用的なものである以上細かい部分などどうしてもDIGGLEとマッチしていない部分があります。

そのためDIGGLEでは薄くラップする形でカスタマイズをしています。 どのようなカスタマイズを入れていたかについて具体的な例を列挙したいと思います。

DIGGLEではDropdownをさまざまな場面で利用しています。

react.semantic-ui.com

Semantic UI ReactではDropdownコンポーネントも用意してくれており、 Semantic UI ReactのデフォルトではtrueになっているselectOnNavigationというpropsのデフォルトを、DIGGLEでは false に変更して利用しています。

こちらのパラメータはDropdownを開いている際に矢印キーを押すと、矢印で移動した先を選択状態にするかどうかというパラメータです。 DIGGLEではDropdownで選択が行われるとすぐにAPIを叩き、選択内容を反映するUIを多く採用しています。その影響で、selectOnNavigationtrueになっているとユーザーが選択のために矢印キーを連打すると連打した分だけ反映のAPIを叩いてしまいます。

そのため多くの場合はselectOnNavigationというpropsをfalseに変更して利用しています。 コンポーネントを利用する側で対応することもできますが、新規参画していただいた方や久しぶりにDropdownコンポーネントを触る際に間違ってselectOnNavigationtrueのままにしてしまわないようUIコンポーネントライブラリ側でデフォルトの値を変更しています。

TextAreaの高さを自動で調整できるように変更

説明やコメントなど長い文章を書く際にTextAreaを活用しています。

react.semantic-ui.com

Semantic UI Reactで用意されているTextAreaは高さを指定して利用するためユーザーの入力に応じてコンポーネントの高さを変えることができませんでした。

DIGGLEではユーザーの入力に応じて高さを変えたい場合はreact-textarea-autosizeライブラリを利用するよう変更しています。

www.npmjs.com

UIで頻出するコンポーネントを用意

テーブルのヘッダーを固定

DIGGLEでは巨大なテーブルを扱う機会が多く、UXの観点からテーブルのスクロール時に行/列を固定したいことがありました。

固定の方法も2行まとめて固定したい、ヘッダーとフッター両方を固定したいなど多岐に渡ったため、Semantic UI ReactのTableを拡張して複雑な固定をできるコンポーネントをSSTable(Sugoi Scroll Tableの略。記憶に残るコンポーネント名で弊社エンジニアのセンスが光ります)という名前で用意していました。

SSTable

Semantic UI React が だんだん DIGGLE にマッチしなくなってきた話

色々なカスタマイズを加えながら大切に育てていたUIコンポーネントライブラリですが、だんだんとDIGGLEの方向性にマッチしなくなってきました。

だんだん表現したいこととSemantic UI Reactで整合性が取れなくなってきた

DIGGLEのUIはSemantic UI Reactのデザインをベースに構築していたのですが、 P/L ( 損益計算書*2 ) を表示するレポートのデザインなど一部Semantic UI Reactを一切使わないコンポーネントが現れました。

つまり、DIGGLEが表現したいUIの中にSemantic UI Reactの表現ではカバーできないコンポーネントが現れ始めたのです。

カスタマイズが複雑になるコンポーネントが増えてきた

DIGGLEで実装したいUIを実現するために複雑なカスタマイズが必要なコンポーネントが出てきました。

例えば、計算式に関わるコンポーネントです。 DIGGLEではさまざまな場面で計算式を利用することができ、 計算式を使うことで勘定科目A勘定科目Bの月次の値を表示した行の下に A+B の計算結果の行を表示させるといったことができます。

こちらの機能はとても活用いただいているのですが、実装時にはユーザーにどういったUIで入力をしていただくのか?という点が問題になりました。 結論としては、Semantic UI ReactのDropdownをカスタマイズで表現したのですが、結果として複雑なカスタマイズが必要になりました。

複雑なカスタマイズは保守性を下げてしまいますし、そもそもSemantic UI Reactで表現することが適切なのか?という疑問を抱かせることになりました。

Tailwind CSSベースのUIコンポーネントライブラリについて

Tailwind CSSはSemantic UI と同様にCSSフレームワークと呼ばれるフレームワークです。

tailwindcss.com

A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.

と記載されている通り「ユーティリティファースト」なCSSフレームワークであり、flex, pt-4, text-centerといったTailwind CSS側が用意したユーティリティクラスを利用することでスタイルを適用することができます。 Tailwind CSSを既存CSSと比較した際の有用性については色々な記事で言及されています*3

DIGGLEでは Tailwind CSSを使うに当たって Tailwind UIの実装をベースにしています。

tailwindui.com

Tailwind UI はまとまったコンポーネントを提供しているわけではなく、Tailwind CSSとHeadless UIを組み合わせてどのようなUIコンポーネントが構築できるかを見せてくれるものになっており、各種コンポーネントのページでは基本的なhtmlタグとユーティリティクラスを組み合わせたコードを見ることができます。

例えば、バッジでは下記のコードを見ることができます。

<span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">Badge</span>

そのため、UIコンポーネントライブラリ側では上記のコードをコピーしつつ、DIGGLEにマッチするよう調整するような形になります。 Semantic UI Reactよりも自前で実装する必要がある部分が増えるものの、同時にカスタマイズ性が上がっています。

自前実装のコストを払ってカスタマイズ性を上げることで、よりDIGGLEの求めるUIを実現できるようになります。 さらに、基本的なhtmlタグとユーティリティクラスを組み合わせたものをベースとしているため計算式を入力するためのDropdownを作る際にも、 ライブラリ側の実装に振り回されるといったことなく、シンプルな実装で実現できるようになると思っています。

Tailwind CSS を使うにあたって工夫している部分

Tailwind CSSが現在のDIGGLEの方向性に合っていそうではあったのですが、Tailwind CSSはそのまま使うには自由度が高すぎました。

そのため一旦下記の運用を行なっています。

ユーティリティクラスの分割

Tailwind CSS を使うと発生する問題の一つに、ユーティリティクラスの肥大化があると思います。

DIGGLEもすでにその問題に当たっており、先ほどのバッジではどのユーティリティクラスがどういった影響を与えているのかをぱっと見で判断することは困難です。 ReactコンポーネントのPropsで見た目を後から調整しようと思った際に、想定と異なるスタイルに影響が出てしまう恐れがあります。

そのため、各種コンポーネントのpropsに注目してユーティリティクラスの切り分けを行うようにしています。

const getSizeClasses = (
  size?: 'x-small' | 'small' | 'large' | 'medium' | 'x-large'
) => {
  switch (size) {
    case 'x-small': {
      return 'px-2 py-1 text-xs';
    }
    case 'small': {
      return 'px-2 py-1 text-sm';
    }
    case 'large': {
      return 'px-3 py-2 text-sm';
    }
    case 'x-large': {
      return 'px-3.5 py-2.5';
    }
    // "medium"
    default: {
      return 'px-2.5 py-1.5 text-sm';
    }
  }
};

lintによるユーティリティクラスの順番整理

ユーティリティクラスを人間が綺麗に管理するには限界があります。

順番などを定めようものならコードレビューのたびに間違いがあるかもわからない間違い探しが始まってしまいます。

DIGGLEではprettierとeslintのルールを導入することである程度はlinterに任せるようにしています。

github.com

github.com

まとめ

今回はUIコンポーネントライブラリを置き換えるにあたって、Tailwind CSSを採用しました。 DIGGLEを運用していく中で求めるUIコンポーネントの姿がSemantic UI Reactを導入したタイミングよりもクッキリと見えるようになったことでTailwind CSSの採用に踏み切りました。 そのため、Semantic UI Reactを採用した以前の意思決定にも間違いはないと思っています。

これからTailwind CSSを本格的に運用していく中でさまざまな問題に直面すると思いますが、フェーズごとに適切な対応が何かを模索することでユーザーにより良い価値を提供できると信じて取り組んでいこうと思います。

この記事が、プロジェクトのUIコンポーネントライブラリの構築に悩んでいる方の一助となれば幸いです。

DIGGLEのエンジニアのitoがお送りしました。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

*1:UIコンポーネントライブラリは、UIを実装する際に利用できるReactコンポーネント群のことを指しています。GitHubでいうPrimerのようなものを社内向けに構築/公開しています。

*2:DIGGLEは管理会計のSaaSのため、ここでの損益計算書は変動損益計算書などの管理会計向けのものになります。

“変動損益計算書”の確認が優良企業への第一歩 | 情報誌「戦略経営者」 | 経営者の皆様へ | TKCグループ

*3:https://udemy.benesse.co.jp/development/system/tailwind-css.html

Pythonエンジニアが感じたRailsの戸惑いポイント

この記事ははじめてのアドベントカレンダー Advent Calendar 2023 21日目の記事です。

初めまして。 2023年10月にDIGGLEにエンジニアとして入社したaki0344です。

DIGGLEではバックエンドにRuby on Railsを利用しています。
そこで今回は、小学生の娘に『ルビィのぼうけん』を買ったぐらいしかRubyとの関わりのなかった私が、Ruby on Rails(以下Rails)を学習する過程で感じた内容について書いてみたいと思います。
また、それだけだとただの感想文になってしまうので、Rails初心者をチームに受け入れるためにこんな用意をしておくとキャッチアップが早まるかもしれませんというポイントも書きたいと思います。

前提

本題に入る前に、私のバックグラウンドについて書いておきたいと思います。
DIGGLE入社前は中小独立系SIerでバックエンドエンジニアとして働いていました。
業務で利用していた言語は直近から順におおよそ以下の通りです。

  • Python(5年)
  • Java(10年)
  • C(1年)
  • C++(2年)

DIGGLEに入社するにあたり、主にProgate、およびRuby on Railsチュートリアルで学習を進めました。
上記のJavaから下に記載した言語は業務から離れて久しいため、今回は直近のPythonのメジャーなフレームワークDjangoと比較した際のRails特有の書き方にフォーカスを当てていきたいと思います。

Railsの便利な点

まずはDjangoと比較して便利だと思った点を書いていきます。

MVCモデル

RailsはMVCモデルでの開発を行うためのフレームワークとなっています。
DjangoもMVCモデルに似たMTVモデルを採用しており、Djangoでの開発経験があれば比較的スムーズにRailsにも入っていけるのではないかと感じました。
また、次の項目にも書く通り、Djangoよりもより厳格な運用となっており、プロジェクトによる差異が起きにくい仕組みとなっています。
そのため、一度Railsによる開発を経験すると、Railsで開発を行っている他プロジェクトに移った場合でもDjangoに比べてキャッチアップが早いのではないかと思います。

ディレクトリ構成に強力な制限がある

Railsはmodels/views/controllers/helpers等、プロジェクトを作成した時点で役割の決まったディレクトリが多く作られます。
それぞれのディレクトリに格納されたファイルが何をするかが明確に決められており、それに従って実装を行うのが最も効率的になるような設計になっていると感じました。
Djangoの場合、プロジェクト作成時にファイルは数個しか作られません。
開発プロジェクト毎にどこに何を置くかを決めて実装を行っていくことになります。
したがって、効率的な開発が行えるかどうかは開発プロジェクトの設計に大きく依存することになります。

書くコード量が少ない

これは実際に開発を行ってから強く実感したことですが、他言語に比べてコード量が少なくなります。
他言語と比べて開発効率の面で優位性があると感じました。

Ruby/Railsの戸惑いポイント

次はPython/Djangoと比較して戸惑ったポイントを書いていきます。

とにかく多い記法

Rubyは大量の組み込みライブラリが用意されています。
PythonとRubyで同じ処理を実装した時の一般的な書き方で比較してみます。

まずはPythonで処理を書いてみます。

import random
random_numbers = [random.randint(1, 1000) for _ in range(10)]

# リストの中身を標準出力
[print(x) for x in random_numbers]

# リストの要素を2倍する
double_numbers = [x * 2 for x in random_numbers]

# リストから偶数のみを取り出す
even_numbers = [x for x in random_numbers if x % 2 == 0]

全てfor文を使って書くのが一般的かと思います。
Pythonのコードを書いたことがなくても、何となくやっていることがわかるのではないかと思います。

一方、同じ処理をRubyで書くと以下のようになります。

random_numbers = (1..1000).to_a.sample(10)

# リストの中身を標準出力
random_numbers.each { |num| p num }

# リストの要素を2倍する
double_numbers = random_numbers.map { |num| num * 2 }

# リストから偶数のみを取り出す
even_numbers = random_numbers.filter(&:even?)

each(要素を取り出して処理), map(処理結果の配列を返却), filter(処理結果が真となる配列を返却)と全て異なるメソッドが登場します。
さらに、偶数のみを取り出す式ではfilterに対して&:even?という、Rubyを知らないとなぜこう書くのか想像するのが難しい値が渡されています。
&:についてはQiitaの@kasei-san(かせいさん)様の記事に詳しく書かれているためここでは割愛しますが、今回の場合はrandom_numbersの要素に対してメソッドeven?で偶数かどうかを判定する、となります。
Rubyの場合はどのような処理をしたいかで最適な文法が変わってくるため、Pythonに比べて覚える内容が多くなります。

あなたは変数?メソッド?

Python、Rubyいずれも以下のような書き方が出来ます。

next_number = previous_number + index

それぞれの名前から前回値にインデックスを加算して次に使用する値とすることが想像できますが、Pythonの場合ここに出てくるのは全て変数となります。
Pythonはメソッドを変数に設定することも可能ですが、実行する際は必ず括弧()が必要となります。

def print_a():
    print("a")

var_print_a = print_a
print("b")                 # => b
var_print_a()              # => a

一方、Rubyの場合メソッドの括弧()を省略可能という仕様があり、ここを見ただけでは変数なのかメソッドなのか判断が出来ません。

def print_a()
  p "a"
end

var_print_a = print_a       # => a
p "b"                       # => b
var_print_a                 # ⇒ a ("var_print_a"という変数に入っている、print_aの戻り値"a"が表示される)

そのため、最初に書いた式はprevious_number, indexという変数を加算したのかもしれないし、メソッドの結果を加算した可能性もあります。
実際にそれぞれが変数なのかメソッドなのかは前後の処理を追って確認する必要があります。

returnを書かない

Pythonはメソッドから値を返却する場合、returnの記載が必須となります。
returnが無いメソッドは戻り値無しとなります。
Rubyの場合、メソッド内で最後に行った処理の結果が戻り値となるため、必ずしもreturnを記述する必要はありません。
途中で値を返却したい場合のためにreturnを書くことは可能ですが、何故か省略可能なところでは省略するのが一般的なようです。
一貫してreturnを明記する言語で開発を行ってきた私にとってはちゃんと想定通り動くんだろうかと漠然とした不安を感じる要素ではあります。

DBモデルが見えない

Djangoのmigration機能を利用している場合、パっと見でモデル構成が理解できるようなコードになります。

from django.db import models
from django.db.models import Q


class User(models.Model):
    # カラム定義
    first_name = models.CharField(128)
    last_name = models.CharField(128)
    age = models.IntegerField
    email = models.CharField(256)
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    created_at = models.DateTimeField()
    updated_at = models.DateTimeField()

    class Meta:
        constraints = [
            # ユニーク制約
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="fullname"
            ),
            # チェック制約
            models.CheckConstraint(
                check=Q(age__gte=18),
                name='age_gte_18'
            )
        ]

実際にDBにmigrateが実行されるとテーブルにはidのカラムが作成されますが、それ以外はモデルを定義したファイル(一般的にはmodels.py)で確認が可能です。

一方、Railsでもデフォルトではschema.rbにDBモデルの構成が保存され、こちらも可読性は高いです。
しかしながら、DIGGLEでは以前プロシージャ導入にあたり、ファイルをstructure.sqlに変更しています。

diggle.engineer

structure.sqlはDBインスタンスを作成するために必要な全てのSQLが記載されており、モデルを把握するためにこのファイルを利用するのは現実的ではありません。
トリガ、シーケンス、プロシージャ等DB固有の機能を利用する場合、Railsの標準機能ではモデルの可読性が下がってしまうというのが問題としてあると感じました。

gemの記法で完全に迷子

Pythonは一部の標準ライブラリを除いて、ライブラリを使用する際はimportが必要になります。
また、処理の中で利用する際はimportで宣言した名前を指定する必要があります。 Djangoのプロジェクトを作成した際に自動生成されるファイルurls.pyでさえ、Djangoの機能を利用するためにDjangoのライブラリをimportしています。

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

ワイルドカード指定でimportすることは可能ですが、非推奨となっています。

from django.db.models import *

したがって、推奨された書き方をするとそのファイルで使用されるライブラリは必ずそのファイル内に同じ名前でimportされることになります。
このルールは一見すると開発効率が落ちるように映るかもしれませんが、そのライブラリの存在を知らない開発者が参画した場合に、自ら調査して解析するための足掛かりになります。
また、最近はIDEの進化によりいきなりライブラリを使用した処理を書き始めても、自動で補完してimportを追加してくれるため、importを書かなくても良いフレームワークと比較した際の開発効率の悪さも解消されつつあります。

一方のRailsでは、Bundlerを使えばbundleコマンドでインストールしたgemを勝手に読み込んでくれるという機能があります。
開発の際には煩わしいrequireを定義する必要が無くなるというメリットがありますが、gemを知らない者が処理を読み進めていると突然良く分からない名前のクラスが出現することになります。

my_password = BCrypt::Password.create("my password")

上記の例ではBCryptという固有の名称があるためまだ検索して確認することが可能ですが、factory_botのようにgemの機能を利用するための定義を書くファイルとそれを利用する処理を書くファイルが分割されている場合、処理を書くファイルから読み始めると唐突に一般的な単語1つが出現します。

user = create(:user)

こうなってしまうと、どう調べれば良いのか見当もつかず完全に行き詰ってしまいます。

キャッチアップを早めるための準備

最後に、Rails初心者がキャッチアップを早めるために、こんなものが用意されていると良いと思うものを書いておきます。

Lintの導入

そもそも導入していない開発プロジェクトの方が少ないかとは思いますが、Rails初心者にとっては記法を覚えるための良き指導者にもなると感じています。
DIGGLEではPR発行時のCIでrubocopによるチェックを行っていますが、今のところ私は毎回rubocop先生に怒られています。

モデル図

DBの構成を把握できるかはシステムを理解するスピードに直結すると私は考えています。
ridgepoleannotate等、モデルの可読性が上がるgemを利用する、プロジェクト自体に手を入れたくない場合はtbls等のツールを利用してモデル図の作成/更新が行われていると新規参画メンバーの理解度が上がるのではないかと思います。

主要なgemの使用例

個人的にはここが最も自己解決の難しい部分ではないかと思います。 使用頻度の高いものや処理を追うだけではgem全体の仕様の把握が難しいものについては、使用例等と合わせて一覧化されていると、解析の一助になるのではないかと思います。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

Rails7環境での最適なJavaScriptビルドツールの選択ガイド

この記事は Ruby on Rails Advent Calendar 2023 の20日目の記事です。

はじめに

Rails7は、JavaScriptのデフォルトツールとしてimport-mapsを採用しました。これにより、ブラウザから直接JavaScriptモジュールをインポートすることが可能になりました。また、Node.jsやWebpack、npm、Yarnの必要性がなくなりました。ただし、選択肢はこれだけではありません。例えばjsbundling-railsではトランスパイルやバンドルがサポートされています。

この記事では、Rails7環境下でのJavaScriptビルドツールについて、選択肢を明確にし、それぞれの特徴を解説します。

Rails7のJavaScriptビルドツールの比較

Rails7には、JavaScriptのビルドに使用できる複数のツールが用意されています。(参考) これらのツールはそれぞれ異なる特徴と利点を持っており、プロジェクトの要件に応じて選択することになります。

種類 特徴 使用シナリオ
import-maps Node.js不要で、JavaScriptモジュールをブラウザから直接インポート可能。 トランスパイルが不要なシンプルなプロジェクト向け。
jsbundling-rails Node.js要。esbuild、Bun(Node.js不要)、Webpack等を選択可。HMR*1は標準機能ではない。 Reactを含む複雑なフロントエンドのビルドが必要な場合、またはHMRを利用するプラグインを追加可能。
Shakapacker Webpackerの後継でHMRサポート。 高度なビルドプロセスとHMRを要するReactプロジェクト向け。
Sprockets CoffeeScriptやSCSS/Sassのトランスパイル。JSXやTypeScriptにはWebpackやBabelが必要。最小化・難読化も可能。 複数のツールと併用して、アセット管理を行う場合に適している。

選択のポイント:

  • Reactを利用する場合、import-mapsは適切ではありません。代わりにjsbundling-railsやShakapackerを検討してください。
  • Reactを使用せず、トランスパイルやバンドルが必要ないシンプルなプロジェクトの場合、import-mapsが適しています。
  • Sprocketsは、他のツールと併用してアセットの管理を行う場合に有効です。

ちなみに弊社ではRails7でReactを利用するので、その前提で選定を進めます。

jsbundling-rails vs Shakapacker

Rails7でReactを使用する際の主要な選択肢として、jsbundling-railsとShakapackerがあります。これらのツールは異なるアプローチを提供し、それぞれ独自の長所と短所があります。

Rails: Webpacker(Shakapacker)とjsbundling-railsの比較(翻訳)

jsbundling-rails

長所

  • シンプルな設定: jsbundling-railsは設定が簡単で、迅速にプロジェクトを立ち上げることができます。
  • 柔軟性: esbuild、Bun、Rollup.js、Webpackなど、複数のバンドラーをサポートしており、プロジェクトのニーズに合わせて選択可能です。

短所

  • HMRのサポート不足: 標準ではHot Module Replacement(HMR)をサポートしていませんが、プラグインを使用することで対応可能です。

Shakapacker

長所

  • HMRサポート: ShakapackerはHMRをネイティブでサポートしており、開発プロセスをより効率的にします。
  • Webpackerの後継: Webpackerユーザーにとってなじみ深く、既存のWebpackベースの設定やプラグインを容易に統合できます。

短所

  • 設定の複雑さ: Webpackに基づいているため、設定が複雑になる傾向があり、初心者にはやや敷居が高いです。
  • ビルド速度: Webpackを使用しているため、esbuildなどの新しいツールに比べてビルド速度が遅い場合があります。

弊社ではビルド速度なども勘案して、jsbundling-railsを選択しました。

jsbundling-railsのバンドルの選択肢

jsbundling-railsにはBun, esbuild, Rollup.js, Webpack の選択肢があります。

以下、Google Trendsでの検索結果になります。
(Bunについては別のものが対象になりそうなので、Google Trendsの対象から除外しました)

esbuildが最近少し検索が増えてきたというのと、Rollup.jsがほとんど検索されてないという傾向にあるようです。

それぞれについて以下になります。

Bun

Bunは、JavaScriptおよびTypeScriptプロジェクトのための高速なオールインワンツールキットです。これには、ランタイム、バンドラー、テストランナー、Node.js互換のパッケージマネージャーが含まれています。また、Node.jsの多くのAPIをネイティブに実装しており、Node.jsの代替として設計されています。

esbuild

esbuildは、JavaScript、TypeScript、CSSファイルなどを高速にバンドルするためのツールです。その主な特徴は、非常に高速なビルドとバンドルの速度で、多くの既存のJavaScriptバンドラーよりも速く動作します。

Rollup.js

Rollup.jsは、JavaScriptのモジュールバンドラーです。ES6モジュール構文を使用してファイル間の依存関係を解析し、それらを一つのバンドルにまとめます。効率的なTree shaking(不要なコードを除去する最適化手法)をサポートしています。これにより、最終的なバンドルのサイズが小さくなり、パフォーマンスが向上します。

Webpack

Webpackは、JavaScriptのモジュールバンドラーです。コードベースのモジュールをマッピングし、一つまたは複数のバンドルにまとめることで、ブラウザで使用できるようにします。

比較

Bun esbuild Rollup.js Webpack
ビルド速度 1倍 1.76倍 188倍 224倍
ES6
TypeScriptのトランスパイル
JSXのトランスパイル
Tree shaking ×

△としている所は単体での機能提供はなく、プラグインなどが別途必要になります。 ただ、できることの基本的な機能に差異は余りありません。

違いが出ているのはビルド速度でBun調べのパフォーマンス計測では、Bunを1倍とすると最大200倍以上の差が発生しているようです。ちなみにBunはZigベース。esbuildはgolangベースで書かれているそうです。

Bunがまだjsbundling-railsで採用されたばかりという所から様子見も含めて、今回は jsbundling-rails + esbuildの利用としました。

esbuild利用の補足

jsbundling-rails + esbuild + npmを利用している場合に ./bin/rails assets:precompile のコンパイル実行はエラーとなります。

理由は、jsbundling-rails内でビルド時にpackage-lock.jsonの存在確認をしていないからです。

def build_command
  return "bun run build" if File.exist?('bun.lockb') || (tool_exists?('bun') && !File.exist?('yarn.lock'))
  return "yarn build" if File.exist?('yarn.lock') || tool_exists?('yarn')
  raise "jsbundling-rails: No suitable tool found for building JavaScript"
end

github.com

バグっぽいのでプルリク出ていないかなと覗いてみたら、丁度出されたみたいでした。 なので今後は大丈夫そうです。 github.com

SprocketsとPropshaft

Sprocketsの後継としてPropshaftが出ています。PropshaftはSprocketsにあったトランスパイルなどの機能はjsbundling-railsなどの他のgemに任せ、配信に必要な最小限の機能だけ提供することで、シンプルに保とうというコンセプトのライブラリのようです。

まとめ

今回、弊社はRails7 + Reactで、jsbundling-rails + esbuildを採用しました。 ただし、プロジェクトの要件によって最適なツールは変わります。 また、この辺りのビルドツールは、実際に利用してみてはじめて分かるようなハマりポイントもあったりします。

この記事が、プロジェクトに適したツールを選択する際の一助となれば幸いです。

DIGGLEのエンジニアのchikugoがお送りしました。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

*1:Hot Module Replacementの略。ウェブアプリケーションの開発中にページの全体リロードをせずに、特定のモジュール(例えばCSS、JavaScriptのコード)をリアルタイムで自動更新する仕組み。

DIGGLEが行った今年の技術広報まとめ

技術広報 Advent Calendar 2023の16日目の記事になります。

はじめに

当ブログをはじめ、今年のDIGGLEは色々な技術広報活動を行ってきましたので、今年一年で何を行ったのかを振り返りたいと思います。

やったこと

今年一年で行ったこととしては、こんな感じになります。

ということで、早速それぞれの詳細を記載していきます。

RubyKaigi Platinum Sponsors & ブース参加

今年行った技術広報で一番の目玉は何といってもコレだと思います。 2023年1月当時、エンジニアがCTOを含めても7名しかいない中、半数以上の4名をアサインして参加してきました。

出しもの(シューティングゲーム)、当日の速報、振り返りと、それぞれ詳しく書いてありますので、気になった方は以下のブログも読んでいただけると幸いです。 また、後述で説明するイベント登壇でもRubyKaigiについて話をするなど一年を通してRubyKaigi関連ネタで擦りに擦ってきました。

diggle.engineer

diggle.engineer

diggle.engineer

イベント登壇

ここ数年、弊社エンジニアが登壇する機会がなかったのですが、今年は2本登壇することができました。年初に「今年は2つくらい登壇したい!」と、ムーンショットなことを私自身言っていた記憶があるのですが、まさか達成できるとは思いませんでした。そして、2本とも私が適任だろうということで、1年間で2本も登壇させていただけるとは夢にも思いませんでした。

イベントに登壇しないかと声を掛けてくださった方、ロジ周りを行ってくれたバックオフィスメンバー、当日聞きに来てくれた参加者の方などなど多くの方にこの場を借りて感謝申し上げます。

1. RubyKaigiスポンサーの裏話

各社の技術広報が明かす「RubyKaigiスポンサーの裏話」運営ノウハウやコミュニティへの想いというイベントで、RubyKaigiにスポンサーブースを出した際のことを話してきました。

弊社以外の登壇企業は有名企業ばかりで、普通のことを話しても埋もれる!という危機感から、「エンジニアだけでスポンサーブースを出したら大変なことになった」というインパクト重視なタイトルで攻めてみました。

イベント当日に「これを見に来た!」とコメントされている参加者の方がいらっしゃったのを見たときは非常に嬉しかったです。

登壇時の資料は以下になります。

speakerdeck.com

2. AWS Startup Meetup Online

上記登壇の2営業日後、AWS Startup Meetup Online ~ スタートアップの熱量を直接感じてみよう ~というイベントで、DIGGLEのアーキテクチャーについて話してきました。

時間の都合上、アーキテクチャの概要部分しか話せなかったので、もし興味を持っていただいた方がいらっしゃればカジュアル面談でお話させてください。

登壇時の資料は以下になります。

speakerdeck.com

動画も残っているようなので貼っておきます。 www.youtube.com

エンジニアブログ

昨年と同じく、今年も以下の目標で一年間走ってきました。

  • 毎月1本以上ブログを公開する
  • 12月はアドベントカレンダーに参加して、1人1本ブログを書く

まず、月1本のブログ公開についてですが、公開が少し遅れる月があったものの、月1本ペースで今日まで走ってくることができました。

そして、RubyKaigiのあった5月には、開催前日に出しもの(シューティングゲーム)についてのブログを公開し、翌日のイベント初日には当日レポートも公開!ということで、月に計2本公開することができました。 当日レポートは、イベント当日の隙間時間を縫っての執筆となり、かなりタイトな状況でしたが、実現してくれたメンバーには感謝しきりです。

あとは今月のアドベントカレンダー用のブログを1人1本書ききれるか次第ですが、弊社エンジニアのみなさん頑張っていきましょう!

what we useへの寄稿

what we useの中の方からお声がけいただき、技術的負債の解消をDIGGLEではどのように行っているかについて寄稿させていただきました。

画像素材は提供したのですが、アイキャッチや組織図に関しては先方側でめっちゃいい感じのものを用意していただきました。

私自身はブログと変わらない感じで執筆したのですが、文章やレイアウトの体裁を整えたり、良い感じのイラストを差し込むことで、こんなに完成度が高い記事になるんだということを勉強させていただきました。やはりプロは凄い!

whatweuse.dev

エンジニアインタビュー

HRチーム主導で動いてもらっているのですが、エンジニアインタビュー記事を公開しています。 既に2本公開されておりまして、今年度中にあと1本公開するべくエンジニアにインタビュー & 記事執筆を行っていただいています。

note.com

note.com

まとめ

今年はRubyKaigiへのスポンサー参加という(弊社の中では)かなり大きなイベントがありましたが、それ以外でも登壇や記事公開など色々とアウトプットできた1年だったかと思います。 技術広報は継続することが重要なので、息切れせず来年以降も継続して活動していければと思っています。

以上、zakkyでした。

プロパティベーステスト (Property Based Testing) を Ruby で書き雰囲気を味わう

2023 年 10 月 30 日に『実践プロパティベーステスト ― PropErとErlang/Elixirではじめよう』(以下 実践プロパティベーステスト本)という本が出版されました。 プロパティベーステストというのは、テストの一手法なのですが、これまでとは違う範囲をカバーするテストです。 今回はそれを Ruby に適用するとどうなるか検証、また似ている既知との概念と対比して理解を深めました。

これは Ruby Advent Calendar 2023 15 日目の記事です。

実践プロパティベーステスト本は 2023 年 12 月現在、テストの一手法であるプロパティベーステストを理解することを主眼においた、日本語になっている唯一の商業本だろうと思っています。プロパティベーステストは、ユニットテストがそうであるように言語を問わない手法です。もちろん Ruby でもできます。

Ruby でも既にいくつかライブラリがあるようです

今回は、最近も更新されている点、README の参考文献に実践プロパティベーステスト本の原著が挙げられていた点をふまえて ruby-prop_check のほうを使いました。

TypeScript に馴染のある方は、TypeScript で実施された記事を書いた方がおられたので、こちらも参考になるかもしれません https://qiita.com/kiwa-y/items/354744ef7393d07a8928

プロパティベーステストとはどのような形式か

まずどんなものか触れてみましょう。 ruby-prop_check は Ruby のテスティングフレームワーク minitest や RSpec と統合して利用する例が公式の README に記載ありました。たぶん test-unit でもできると思います。ここでは RSpec を使いました。

このコードは https://gist.github.com/niku/3f445fa36d241724f84d2fecae6b5054 に置いてますので、お手元でも試せます。

今回、配列に入っている値の平均を取る、以下の関数について考えます。

# 全ての要素が整数の配列を引数にとり、その要素の合計を要素数で割った平均を整数で返す
def naive_average(array)
  array.sum / array.length
end

これに対応するプロパティベーステストは、

require 'rspec'

RSpec.describe "#naive_average" do
  G = PropCheck::Generators

  it "returns an integer for any input" do
    PropCheck.forall(G.array(G.integer)) do |numbers|
      result = naive_average(numbers)

      expect(result).to be_a(Integer)
    end
  end
end

となります。ふだんの rspec に存在するものと、見慣れないもの PropCheck::GeneratorsPropCheck.forall がいますね。

「任意の整数 ( G.integer ) を要素にもつ配列 ( G.array ) の全て ( PropCheck.forall ) は、 naive_average に引数として渡すと必ず Integer 型を返す」と読めたりするでしょうか。

実行は普通の RSpec と同じように bundle exec rspec naive_average_spec.rb といった形になります。実行してみると以下の結果が得られました

$ bundle exec rspec naive_average_spec.rb

#naive_average
  returns an integer for any input (FAILED - 1)

Failures:

  1) #naive_average returns an integer for any input
     Failure/Error: result = naive_average(numbers)

     ZeroDivisionError:

       (after 71 successful property test runs)
       Failed on:
       `[]`

       Exception message:
       ---
       divided by 0
       ---

       (shrinking impossible)
     # ./naive_average.rb:2:in `/'
     # ./naive_average.rb:2:in `naive_average'
     # ./naive_average_spec.rb:10:in `block (3 levels) in <top (required)>'
     # ./naive_average_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.01036 seconds (files took 0.10241 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./naive_average_spec.rb:8 # #naive_average returns an integer for any input

71 回プロパティベーステストが成功した ( after 71 successful property test runs ) あとに、 [] を渡したとき ( Failed on [] ) に ZeroDivisionError で失敗したようですね。ひとまず 70 回テストするのに 0.01 秒で実施できるなら、100 回以上のテスト生成でも実用に耐えそうです。

さて、ほんとうに [] で失敗するでしょうか。確かめてみましょう。 リポジトリを Gist にするためにディレクトリ構成がいつもと違って lib/ にいないので -I . というのをコマンドに足していますが、プロパティベーステストには関係ないところなので、あまり気にしないでください。

確かに [] を渡すと divided by 0 (ZeroDivisionError) でエラーになっていますね。空配列は #length0 になるため 0 / 0 となり起きたようです。

$ bundle exec irb -I .
irb(main):001> require 'naive_average'
=> true
irb(main):002> naive_average([])
/Users/niku/src/property_based_testing_with_ruby_sample/naive_average.rb:2:in `/': divided by 0 (ZeroDivisionError)
        from /Users/niku/src/property_based_testing_with_ruby_sample/naive_average.rb:2:in `naive_average'
        from (irb):2:in `<main>'
        from <internal:kernel>:187:in `loop'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/gems/3.3.0+0/gems/irb-1.9.0/exe/irb:9:in `<top (required)>'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/bin/irb:25:in `load'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/bin/irb:25:in `<top (required)>'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/cli/exec.rb:58:in `load'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/cli/exec.rb:58:in `kernel_load'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/cli/exec.rb:23:in `run'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/cli.rb:492:in `exec'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/vendor/thor/lib/thor/command.rb:28:in `run'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/vendor/thor/lib/thor.rb:527:in `dispatch'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/cli.rb:34:in `dispatch'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/vendor/thor/lib/thor/base.rb:584:in `start'
        from /Users/niku/.asdf/installs/ruby/3.3.0-preview3/lib/ruby/3.3.0+0/bundler/cli.rb:28:in `start'
        ... 5 levels...

さて、仕様では「全ての要素が整数の配列を引数にとり、その要素の合計を要素数で割った平均を整数で返す」ということを考えていました。空配列は「全ての要素が整数の配列」といえるでしょうか?言葉の意味的にどうかはさておき、このプログラムを提供する側と、利用する側で考えが異なるとトラブルになるので、未然に防ぎたいところです。プロパティベーステストは、こういった開発時に意識から外れている範囲のことに気づくきっかけを得られるのがいいところですね。

今回は仕様を「全ての要素が整数の、 要素を 1 つ以上持つ 配列を引数にとり、その要素の合計を要素数で割った平均を整数で返す」へと更新して、そのテストを行うことにします。

PropCheck::Generators.array をみると、こういった指定にもオプションで対応できるようになっています。 最低要素数、最大要素数、空配列を許可するか、ユニークであることが指定できそうですね。 G.array の引数に empty: false を追加して、以下のコードになりました。

require 'naive_average'
require 'prop_check'
require 'rspec'

RSpec.describe "#naive_average" do
  G = PropCheck::Generators

  it "returns an integer for any input except empty array" do
    PropCheck.forall(G.array(G.integer, empty: false)) do |numbers|
      result = naive_average(numbers)

      expect(result).to be_a(Integer)
    end
  end
end

実行しましょう。

$ bundle exec rspec naive_average_spec.rb

#naive_average
  returns an integer for any input except empty array

Finished in 0.017 seconds (files took 0.10255 seconds to load)
1 example, 0 failures

うまくいったみたいですね。 (デフォルトではテストは forall あたり 100 回実施する設定になっていました。n_runs という設定で変えられます

既知の概念との対比

ここらへん、私の理解や解釈が書いてあるので、まちがっているかもしれないです。そのときはやさしく教えていただけると感謝しつつ直します。

以下に列挙するように、他の概念と重なりつつも、異なるアプローチでソフトウェアを形づくろうとしています。だからこそ今までにない知見が得られて有用で、だからこそ今までの知見が使いにくく書きにくいのだと思っています。

普段書いているテストと、プロパティベーステスト

プロパティベーステストの立場にいるときに、ふだん私たちが行っているテストを何とよぶのが適切でしょうか。 英語だと "example based testing" と呼ぶようです、実践プロパティベーステスト本だと「事例ベースのテスト」など、事例ベースという表現の訳になっていました。

事例ベースのテストでは、具体的な事例を扱います。たとえば "abc" + "d""abcd" になるといった形ですね。

プロパティベースのテストでは、プロパティを扱います。プロパティというのは何でしょうか。1 章(p.8) には

「どのような入力値を与えても常に同じであるような振る舞い」を記述するルールを見つけて、それを実行可能なコードとして書き表すのです。

とありました。ここまで見てきたような、常に真となるようなルールを並べながら仕様を形作っていくことになります。 たとえば

  • (ascii なら) 足す前の左側、右側の文字数合計と、足した後の文字数は等しい
  • 左側の文字は、足したあとの文字に前方一致する。右側の文字は、足したあとの文字に後方一致する

などです。

1 章(p.11) には

事例ベーステストは「コードが自分たちの想定した通りに実行されるかを確信する」ための助けになり、プロパティベーステストは「プログラムに何ができて何ができないかを確認するためにその振る舞いを探索し、そもそも自分たちの想定が正しいかどうかを判断する」ための助けになります

とあり、私はこの例えがしっくりきました。プログラマの想像外を見つける手助けになるのがプロパティベーステストなんですね。助けになる種類が違うので、どちらかだけだけでなく、両方行うことが肝要だと感じました。

データ(テーブル)駆動テストと、プロパティベーステスト

test-unit だと、こんな感じに test_plus で利用する引数と期待する結果を定義して、一つのテストに複数のデータを流してテストできます。この例だと 4 = 3 + 1 と -1 = 1 + (-2) ですね。

    class TestDataSet < TestCalc
      data("positive positive" => {:expected => 4, :augend => 3, :addend => 1},
           "positive negative" => {:expected => -1, :augend => 1, :addend => -2})
      def test_plus(data)
        assert_equal(data[:expected],
                     @calc.plus(data[:augend], data[:addend]))
      end
    end

Go でも公式の Wiki に項目があるくらい有名な手法です。

var flagtests = []struct {
    in  string
    out string
}{
    {"%a", "[%a]"},
    {"%-a", "[%-a]"},
    {"%+a", "[%+a]"},
    {"%#a", "[%#a]"},
    {"% a", "[% a]"},
    {"%0a", "[%0a]"},
    {"%1.2a", "[%1.2a]"},
    {"%-1.2a", "[%-1.2a]"},
    {"%+1.2a", "[%+1.2a]"},
    {"%-+1.2a", "[%+-1.2a]"},
    {"%-+1.2abc", "[%+-1.2a]bc"},
    {"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        t.Run(tt.in, func(t *testing.T) {
            s := Sprintf(tt.in, &flagprinter)
            if s != tt.out {
                t.Errorf("got %q, want %q", s, tt.out)
            }
        })
    }
}

このテスト形式と、プロパティベーステストは、一つのテストに対して多数のデータを入力して検証するというところは似ています。 ただそれでも、データ駆動テスト形式はあらかじめプログラマが定めた値そのものを検証するという部分がプロパティベーステストとは異なります。

ファジングと、プロパティベーステスト

Go には標準機能でファジングが行え、テスティングフレームワークに統合されるぐらい、ファジングは市民権を得ているようです。 ファジングと、プロパティベーステストはどう違うのでしょうか。https://go.dev/doc/tutorial/fuzz#write-the-code-2 を読むと、

The unit test has limitations, namely that each input must be added to the test by the developer. One benefit of fuzzing is that it comes up with inputs for your code, and may identify edge cases that the test cases you came up with didn’t reach.

「unit テストは開発者によって入力が加えられなくてはならないのに対し、ファジングはコードへの入力を生成してくれる」とあり、似ているように見えます。

わたしもここの理解があやふやな所もありますが、値生成によるテストで検証するという手法は同じでも、ファジングはセキュリティの担保や脆弱性の検知といったように、壊れない頑健さを確かめるもので、プロパティベーステストは期待の動作が行われることを確かめるものと、目的が異なるのかなと思っています。

プロパティベーステストではファジングのようなことも行うので、プロパティベーステストの方がより広い概念を扱っていると言えそうです。

訳者まえがきにも

なお、プログラムがクラッシュするよう なテストケースに関して「ファジング」と呼ばれる手法を耳にしたことがある方もいるかもしれませんが、プロパティベーステストはファジングよりも広範囲のシナリオで活用できる仕組みといえます。

とありました。

型と、プロパティベーステスト

最初に挙げていた例の「全ての要素が整数の、要素を 1 つ以上持つ配列を引数にとり、その要素の合計を要素数で割った平均を整数で返す」は静的な型で表現できそうです。 強い制約を持つ静的な型があれば、プロパティベーステストの意義というのは低いでしょうか。 確かにこの本のターゲットである Erlang, Elixir 共に動的型付言語ですし、Ruby もそうですね。

実際には、プロパティベーステストは Haskell の QuickCheck から発生したもので、強い制約を持つ静的な型があるシステムでも有用です。 私が最初に挙げていた例は、簡素化のために型で表現していましたが、プロパティベーステストは型をチェックする他にも様々な検証方法があります。 検証方法は、本の 3 章「プロパティで考える」を読むと理解が深まります。

強い制約を持つ静的な型があれば、プロパティベーステストの一部は省略できるけれども、プロパティベーステストはより広い範囲の事象を扱うと言えそうです。

まとめ

  • プロパティベーステストを Ruby に適用するとどうなるか、コードを動かしながらお知らせしました
  • プロパティベーステストと似ている既知の概念と、プロパティベーステストはどこが同じで、どこが違うのかお知らせしました

DIGGLE でもプロパティベーステストを導入して役に立つか検証を行う段階です。 Ruby に限らず、フロントエンド側の TypeScript でも始めようとしています。 もしこの記事を読んで興味を持った方がおられれば、一緒にやりませんか。

ご応募をぜひご検討ください。お待ちしています!

herp.careers

一人PdMでもできる!「事業開発」はじめの一歩

BtoB事業開発アドカレの記事です

このnoteは「BtoB事業開発アドカレ 」の8日目の投稿です。面白かったらハッシュタグ「 #BtoB事業開発アドカレ 」を付けてシェアいただけますと幸いです。 前回は、問いを磨くこと #BtoB事業開発アドカレ|mai@株式会社iCAREでした。

adventar.org

想定読者

  • SaaS立ち上げフェーズでPdMとして活躍されており、全てを馬力で回しているそこのあなたに向けた記事です。
    • 事業開発したい。でも、開発伴走するのに追われて中々手が出せないなー、、と思っている方には特におすすめ。
  • すでに事業開発が仕組み化、チーム化されている皆様には初歩的すぎて味気ないかもしれません、ごめんなさい。

サマリ

  • PdMは顧客と遠くなりがち、一番顧客に近いCS,Salesの声をいかに拾えるか仕組みづくりが大事
  • ビザスクは軽い気持ちでいろんな人の話が聞けていいぞ!

ちょろっとだけでも是非読んでみてください↓

はじめに

こんにちは、DIGGLE株式会社でPdMをやっている本田です。 現状PdMというロールは私のみで担っている形になっています。ありがたいことに直近ご利用いただいている会社数も右肩上がりに伸びており、忙しい日々を過ごさせてもらってます。

そんな「一人PdMあるある」として、足元の開発デリバリーをデザイナー、開発者と伴走することに追われてしまい中々先を見据えた仕込みができなくなってしまう、、、 みたいなことがあると思っています。 私自身が半年くらい前に事業開発に積極的に関わっていきたい!けど時間がない、、というジレンマに陥っていました。

ただ、やはり事業フェーズ的にも事業開発めちゃ大事!!ということを感じたため、半年前くらいから勇気と工数を振り絞ってチャレンジしてみると、様々な取り組みから成果が生まれ始めました!

今回は「事業開発はじめの一歩」というところで、自分が最近トライをしてみた「コスパの良い」、「スモールスタートな」事業開発について少しご紹介させていただければと思います。

ホリゾンタルSaaSにおける「事業開発」とは

この後の話の前提をすり合わせるために、私が今回話す上での「事業開発」について説明します。

我々はホリゾンタルSaaSというところで、あらゆる業種/業態、規模の「予実管理」におけるペインを解決していくSaaSになっています。

予実管理は非常にカスタマイズ性の高い領域になっており、業種/業態、規模によってペインや勘所も微妙に異なってきます。 したがって、「プロダクトの刺さり具合(価値を感じていただけるか)」の度合いは各セグメントごとによって変化してきます。

その中では、「現状の獲得できているセグメント」から「新たにどのセグメントに注力していくか」が大事になっていきます。 今回の「事業開発」は「新たにどのセグメントに注力していくか」をどの方向性にするか決める、実行するということについて話しているということを前提におきたいと思います。

事業開発何から始めたか

①CSチームで行っていたサクセス/チャーン顧客分析MTGに参加

まずは基本の「き」となる部分かもしれませんが、現状顧客をしっかりと知るところからスタートしました。

具体的には、サクセス移行、チャーン移行したお客様の経緯などを分析する座組みをCSチーム内でされていたのですが、そこにお邪魔させていただき担当CSとのディスカッションを行いました。

ここでのポイントとしては、お客様の時間をいただいて直接インタビューするのではなく、担当CSを介した情報を利用するという点です。 一次情報ではないため、多少のバイアスがかかった情報であることを前提に置く必要はあります。しかし、大まかな仮説を考えていく上では短時間でまとまった情報が仕入れられるというメリットがあります。

それぞれの会でヒアリングする方向性は若干異なります。サクセス分析では「勝ち筋」、チャーン分析では「負け筋」の探索をするように意識しています。

N=1の事象なのでつい具体的な良かった、ダメだった施策に目が行きがちですが全体感を捉えることを重要視しています。 具体的には、下記のようなポイントを聞く際には意識しています。

②お客様からのポジティブなフィードバックを収集するチャンネル作成、運用

2つ目の取り組みとしては、ポジティブなフィードバックのデータを収集し始めました。 「フィードバック」というと要望(例.OO機能が欲しいなど)などの比較的ネガティブ寄りなものが一般的に収集されやすい結構にあります。

しかし、先述したように「勝ち筋」を探索する上ではお客様に刺さっている場所を特定することも大事だと考えています。

①のサクセス移行分析だけでは、限定的なお客様が対象となってしまいます。より広範囲でライトにフィードバックをいただくために 「#20-product-voice_of_customer」というチャンネルをslack上に作成しました。

こちらも実際の一次情報ではないため、フィードバックを受けた背景情報など、バイアスがなるべく無いよう見極める必要もあります。しかし、「こんな刺さり方もあるのか!」など意外な気づきもあり、新たな仮説を生み出すことにつながっています!

↓実際のSlackチャンネル(お見せできない情報ばかりで恐縮ですが、、)

③ビザスクを利用して、スポットコンサルを依頼する

最後にスポットコンサルを依頼できる「ビザスク」を依頼し、「ある業界に詳しい方」にインタビューをするということを行いました。 ①、②で得た粗い仮説をもとにより深掘りたい話や、仮説をもとに壁打ち的に検証したいことなどをスピーディに検証したいときにに利用させていただいています。(回し者ではございませんw)

特に我々のようなBtoBサービスにおいては、お客様以外のターゲット層へのインタビューの難易度はBtoCサービスよりも高いということもあり、募集-->日程調整のフローで行えるこの方法を重宝しています。

まとめ

このような3つの具体を実践してみることで、PdMの日常業務を行いながら中長期に向けた仕込みを少しずつ進めることができています。 「事業開発」というと名前からして、とっつきにくい、大変そうといったイメージがある方もいると思います。

しかし実際には、、

  • そもそも社内には事業開発の種になる情報がたくさん落ちているけど意外と拾いきれていない

  • また社外のリソースをうまく活用することで、コストを低く、社内にない情報を部分的に拾い切れる

ということからやり方次第でスモールスタートができるというのが今回の自分の学びでした。

一方で、今回の方法は手早く始められるのですが、深さは足りていないなと感じているところもあります。今後より精度を上げた事業開発を行うにあたっては時間も取組も増やしていかなければいけないと思っています。

また、今回の自分の取り組み内容として上げたものはCSやSalesのメンバーがお客様と密に接しているという前提があったり、それを積極的にフィードバックしてくれる文化があってこそだと社内メンバーへの感謝を改めて感じました。

最後の最後に

私が入社した2022年8月は30人弱だった組織は1年強で50人を超えそうな勢いになっています。 ありがたい悲鳴ですが、それでもまだまだ全然人が足りていません!プロダクトマネージャー領域は戦略から実行まで超少人数制でやっており、最近手が回ってきていない箇所が顕在化するようになってきてしまいました。

ということで全職種全方位採用強化中です!! さらなる成長を止めないために、皆さんのお力を貸してください!よろしくお願いいたします!!

herp.careers

herp.careers

herp.careers