for Startups Tech blog

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

意外と知らないRubyの基礎知識(と少し応用)

こんにちは。フォースタートアップス株式会社、エンジニアの石田です。

前回の記事では「主にバックエンドのエンジニアをしています。」と自己紹介しましたが、今はバックエンドとフロントエンド両方を担当しています。

特に直近数ヶ月はフロントエンド(React/Next + TypeScript)ばかりやっています。ですが、私が一番好きな言語はRubyですので、今回も前回と同じくRubyの話をしたいと思います。

今回の内容はRubyの基礎知識+豆知識的な内容になります。後半になるにつれて認知度が低く、マニアック度が高くなる気がします。お付き合いください。

ゲッター/セッターメソッド

Rubyで頻繁に使うattr_reader, attr_writer, attr_accessor の正体について説明します。

class Hoge
  attr_reader :foo # ゲッターを定義
  attr_writer :bar # セッターを定義
  attr_accessor :baz # ゲッター/セッターを定義

  def initialize(foo, bar, baz)
    @foo, @bar, @baz = foo, bar, baz
  end
end

これはよく見るRubyのコードですね。ゲッター・セッターを定義しています。Rubyにおいてこの「ゲッター・セッター」とは何でしょうか?実はこれはただのメソッドです。

上のコードは下のコードと同じ意味です(注意: 文字通り「意味的に」同じという意味です。Rubyインタプリタがどのようにこれを実行するかは別です。後述)。

class Hoge
  def initialize(foo, bar, baz)
    @foo, @bar, @baz = foo, bar, baz
  end

  # ゲッターを定義
  def foo
    @foo
  end

  # セッターを定義
  def bar=(val)
    @bar = val
  end

  # ゲッター/セッターを定義
  def baz
    @baz
  end
  def baz=(val)
    @baz = val
  end
end

hoge = Hoge.new(1, 2, 3)
hoge.bar=(100) # 普通のメソッド実行
hoge.bar= 100  # カッコが省略できる
hoge.bar = 100 # 特別ルールでメソッド名の「=」の前にスペースが入ってもOK

Rubyインスタンス変数は全て必ずprivateスコープです。外部からアクセスするためにはpublicスコープのメソッドをつかう必要があります。

attr_〇〇 を使うとこれらのメソッドが定義されます。特に注目なのがセッターメソッドです。実際にはメソッドの実行なので、bar=という名前のメソッドを実行しているだけですが、hoge.bar = 100 というふうにあたかもhoge.barに値を代入しているように書くことができます。この辺がRubyマジックですね。

注意

attr_〇〇を使うのとゲッター/セッターメソッドを自力で書くのは意味的には同じですが、実行速度は違います。(少なくともCRubyでは)attr_〇〇はCレベルの専用処理が動くのでattr_〇〇を使ったほうが実行速度は速くなります。特に理由がなければattr_〇〇の方を使いましょう。

privateメソッドは「外部からアクセスできないメソッド」という意味ではない(?)

以下のコードがあったとします。これを実行するとNoMethodErrorが発生します。privateなので当たり前ですね。

class Hoge
private

  def say_foo
    puts 'foo'
  end
end

hoge = Hoge.new
hoge.say_foo # private method `say_foo' called for #<Hoge:0x00007f86bb887f40> (NoMethodError)

では以下のコードの場合はどうなるでしょうか?

class Hoge
  def public_say_foo
    self.say_foo
  end

private

  def say_foo
    puts 'foo'
  end
end

hoge = Hoge.new
hoge.public_say_foo # どうなるでしょう?

この実行結果は実はRubyのバージョンによって変わります

# Ruby 2.7より前
hoge.public_say_foo # private method `say_foo' called for #<Hoge:0x00007f86bb887f40> (NoMethodError)

# Ruby 2.7以降
hoge.public_say_foo #=> foo

なぜRuby 2.7より前のバージョンではNoMethodErrorが発生したのでしょうか?それは以下のような理屈です。

  • もともとprivateメソッドは「レシーバを明示的に指定して実行できないメソッド」という意味だった
  • レシーバのselfは省略できる
  • privateメソッドはレシーバを省略しなければならない = レシーバは必ずself

上記コードはレシーバselfを明示的に指定しているので、Ruby 2.7より前ではNoMethodErrorが発生しました。

Ruby 2.7ではこれにルールが追加されました。

なのでRuby 2.7以降ではエラーにならなかったんですね。 レシーバselfを明示的に指定しない場合はバージョンに関わらずエラーにはなりません。

class Hoge
  def public_say_foo
    say_foo
  end

private

  def say_foo
    puts 'foo'
  end
end

hoge = Hoge.new
hoge.public_say_foo # どのバージョンでも foo が返る

子クラスからのprivateの実行

さて、次は以下のコードです。

class SuperHoge
private

  def say_foo
    puts 'foo'
  end
end

class Hoge < SuperHoge
  def public_say_foo
    say_foo
  end
end

hoge = Hoge.new
hoge.public_say_foo # どうなるでしょう?

これは"foo"が返ります。

hoge.public_say_foo #=> foo

先述の通りprivateメソッドはレシーバを省略すれば実行できます。そのクラスに対象メソッドが定義されていなかった場合、Rubyは自動的に先祖クラスを順番に探しに行きます。この場合SuperHogeクラスにsay_fooが存在しているので無事実行できました。

「あれ?」と思った方へ

ここまで読んで、一部の方(とくにJavaオブジェクト指向を学んだ方)はこう思うかもしれません。「子孫クラスで先祖クラスのprivateメソッドを実行できるならprotectedスコープって何?」と。これについては次の章で話しますが、RubyprotectedJavaprotectedは全く別の概念です。

とりあえず今は「子孫クラスで先祖クラスのprivateメソッドを実行できる」とおぼえて、例えばテンプレートメソッドパターン(リンクは弊社ブログ)protectedキーワードが使われているのを見つけたら、そっとprivateに置き換えてあげてください

protectedメソッドは「自身と子孫のクラスからしかアクセスできないメソッド」という意味ではない

先述の通り、RubyprotectedJavaprotectedは全く別の概念です。 Rubyのprotectedメソッドは「自身と子孫のクラスからしかアクセスできないメソッド」という意味ではありません。

Ruryのprotected は公式ドキュメントでは以下のように説明されています。

protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せます。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#limit

これだけ読んでもよく意味がわかりませんね。簡単な例を用意しました。

protectedを使ったサンプルコード

サンプルの要件は以下です。

  1. FullNameという値オブジェクトクラスが存在している
  2. FullNameクラスのインスタンスは2つの値を持っている
    • first_name
    • family_name
  3. FullNameクラスのインスタンスの比較は「持っている値がすべて同じ場合」はtrueにしたい
  4. FullNameクラスが持っている値は外部から隠蔽したい(この要件が存在する理由は不明ですが、とにかくそういう要件があると思ってください)。

この要件を満たすFullNameクラスを作っていきます。

まず単純に要件1と要件2を満たすクラスを作ってみます。

class FullName
  def initialize(first_name, family_name)
    @first_name = first_name
    @family_name = family_name
  end
end

foo = FullName.new('foo', 'bar')
bar = FullName.new('foo', 'bar')

puts foo == bar #=> false <- これがtrueになるようにしたい

このままでは要件3を満たしません。要件3を満たすために==メソッドをoverrideしましょう。

class FullName
  def initialize(first_name, family_name)
    @first_name = first_name
    @family_name = family_name
  end

  def ==(other)
    self.first_name  == other.first_name && \
    self.family_name == other.family_name
  end

  def first_name
    @first_name
  end

  def family_name
    @family_name
  end
end

foo = FullName.new('foo', 'bar')
bar = FullName.new('foo', 'bar')

p foo == bar #=> true <- 要件3を満たす

foo.first_name #=> "foo" <- 要件4違反

これで要件3を満たすようになりました。しかし要件4に違反しています。どうすればいいでしょうか?#first_name, #family_name をprivateにすることはできません。other.first_name, other.family_nameNoMethodErrorになってしまうからです。

ここでprotectedの出番です。

class FullName
  def initialize(first_name, family_name)
    @first_name = first_name
    @family_name = family_name
  end

  def ==(other)
    self.first_name  == other.first_name && \
    self.family_name == other.family_name
  end

protected

  def first_name
    @first_name
  end

  def family_name
    @family_name
  end
end

foo = FullName.new('foo', 'bar')
bar = FullName.new('foo', 'bar')

p foo == bar #=> true <- 要件3を満たす

foo.first_name #=> NoMethodError <- 要件4を満たす

これですべての要件を満たすFullNameクラスができました!

問題は「そもそもこんな要件が実際に存在するのか(とくに要件4)?」ということです。想像の通りprotectedを使わなければいけない場面は相当少ないと思われます。私は実務でも私的なプロジェクトでもprotectedを使ったことが一度もありません。

Rubyの親であるMatz氏もprotectedを実装したことを後悔している様子です

注意

このFullNameは値オブジェクトのクラスとしては完全ではありません。HashのキーとしてFullNameインスタンスが使えないからです。Hashのキーの同一性は==メソッドではなくeql?メソッドを使って比較します。詳しくは述べませんが、Hashのキーとしても使える値オブジェクトを実装したい場合、上記のコードは参考にしないでください

Bool/Boleanクラスは存在しない

どの言語でもありそうなBool/Bolean型・Bool/BoleanクラスというものはRubyには存在しません。確かめてみましょう。

p true.class  #=> TrueClass
p false.class #=> FalseClass

TrueClassFalseClassクラスは全く別のクラスです。共通の親クラスも(すべてのクラスの共通の先祖クラスであるObject/BaseObjectクラスを除いて)ありません。

ちなみに、TrueClass/FalseClass はシングルトンパターンで実装されているのでインスタンスtrue/falseただひとつです。

Procとlambdaの違い

RubyにはProclambdaというかなり似ているけどちょっと違う2つの実装があります。

下のコードを見てみましょう。

proc = Proc.new { |x| x + 1 }
lamda = lambda { |x| x + 1 }

p proc.call(1) #=> 2
p lamda.call(1) #=> 2

p proc.class #=> Proc
p lamda.class #=> Proc

はい、全く同じに見えますね。しかし、細かく見ると違いがあります。

ちなみにProc.newlambdaにはそれぞれエイリアス(のようなもの)があります。

  • Proc.new -> proc
  • lambda -> -> (Ruby1.9より)

書き換えると以下になります。

proc = proc { |x| x + 1 }
lamda = -> (x) { x + 1 }

Procとlambdaはどう違うか

returnの挙動が違う

def say_foo1
  proc = Proc.new { return 10 }
  proc.call
  puts 'foo1'
end

def say_foo2
  lambda = lambda { return 10 }
  lambda.call
  puts 'foo2'
end

puts say_foo1
puts '-----'
puts say_foo2

このコードを実行すると以下の出力になります。

10
-----
foo2
nil

どういうことかというと、Procの場合はreturnした時点で#say_foo1から抜けてしまうんですね。それに対してlambdaはreturnしてもブロックから抜けるだけです(このブロックというものの概念の説明も難しいのですが、余計ややこしくなるので今回は説明しません)。

引数の扱いが違う

以下のコードを見てください。

proc = Proc.new { |a, b| [a, b] }

p proc.call(1) #=> [1, nil]
p proc.call(1, 2, 3) #=> [1, 2]

# 足りないところをnilで埋められる
# 余ったところは切り捨てられる

lambda = lambda { |a, b| [a, b] }

p lambda.call(1) # ArgumentError
p lambda.call(1, 2, 3) # ArgumentError

上記のようにProcとlambdaでは引数の扱いが違います。

Procとlambdaの識別方法

Procとlambdaは#lambda?をつかって識別できます。

prop = Proc.new { |x| x + 1 }
lamda = lambda { |x| x + 1 }

p prop.lambda? #=> false
p lamda.lambda? #=> true

(Procとlambdaはすくなくともruby 1.8のころからあったのに)#lambda? が実装されたのはruby 2.1です。それまではProcなのかlambdaなのか区別するのは困難でした。

Procとlambdaの違いについてまとめ

正直、Procとlambdaの違いは「Rubyの闇」「Rubyの技術的負債」のように感じます。

とりあえずProcは使わずにlambdaのみを使うようにすればトラブルは少なそうです。

ちなみにブロックというものはProcともlambdaとも挙動が違うのですが、それについて話すと長くなりすぎますし、ますます混乱するので割愛します。1つだけ言うとブロックはProcやlambdaと違い、なにかのインスタンスではないので変数に入れたり、引数で渡すことはできないものです。

なぜdef self.hogeと書くとクラスメソッドになるのか

Rubyではクラスメソッドを定義するときに以下のように書くのが普通です。

class Foo
  def self.what_is_your_name?
    puts "my name is Foo"
  end
end

Foo.what_is_your_name? #=> my name is Foo

でもなんでdef self.what_is_your_name?と書くとwhat_is_your_name?はクラスメソッドになるのでしょうか?それを理解するためには段階を踏む必要があります。

Step1. インスタンス固有のメソッドを定義できる

クラスメソッドからは一旦離れて、以下のコードを見てください。

foo = "foo"

def foo.add_ban
  self + "!!!"
end

p foo.add_ban #=> "foo!!!"

bar = "bar"
p bar.add_ban # NoMethodError

実はRubyではインスタンスに固有のメソッドを定義することができます。これを一般的に「fooインスタンスの特異クラスのインスタンスメソッド」と呼びます。単に「fooの特異クラスメソッド」や「fooの特異メソッド」と呼ばれることもあります。(公式の呼び方はなさそう?)

Rubyの「.」には少なくとも3つの意味があります。

  1. メソッド実行: 「.」の左がレシーバインスタンスで右がメソッド名
  2. 特異メソッドの定義: 「.」の左がインスタンスで右がメソッド名
  3. 小数点の「.

上記のdef foo.add_banは2の意味ですね。1の意味では無いので注意です。

Step2. 全てのクラスはインスタンスである

Rubyでは「全てのクラスはインスタンス」です。言っている意味がわからないかもしれませんが、とりあえず以下のコードを見てください。

class Hoge
end

hoge = Hoge.new
p hoge.class #=> Hoge

p Hoge.class   #=> Class
p String.class #=> Class

hogeのクラスはHogeです。これは当たり前ですね。実はHogeのクラスはClassという名前のクラスです(言い換えるとHogeClassクラスのインスタンス)。Hogeに限らすStringなど、全てのクラスはClassクラスのインスタンスです。

なのでHogeクラスは以下のように定義することもできます。

Hoge = Class.new # こうやってクラス定義できる

hoge = Hoge.new
p hoge.class  #=> Hoge

これは余談になりますが、Rubyでクラス名の頭が大文字なのはクラスが定数だからです。Rubyでは変数の頭の文字によってそれがどんな変数なのかを識別します。

  • 先頭が$ : グローバル変数
  • 先頭が@ : インスタンス変数
  • 先頭が@@ : クラス変数(こいつのことは忘れてください)
  • 先頭が大文字(全部大文字である必要はない): 定数
  • 先頭が小文字: ローカル変数

Step3. クラスの特異メソッドを定義できる

「クラスはインスタンス」ということは、Step1のように「クラスに特異メソッドが定義できる」ということです。これがクラスメソッドの正体です。

class Foo
  p self #=> Foo

  def self.what_is_your_name? # `def Foo.what_is_your_name?` と同じ意味
    puts "my name is #{self}"
  end
end

Foo.what_is_your_name? #=> "my name is Foo"

つまり、Rubyには「クラスメソッド」というものは実は存在せず、全てはインスタンスメソッドです。「クラスに所属するインスタンスメソッド」か「特異クラスに所属するインスタンスメソッド」かの違いしかありません。

変数名やメソッド名はASCII文字じゃなくてもいいんだよ

注意: 下記はソースコードファイルをUTF-8で保存して動作確認しています。それ以外の文字コードを使った場合は動作確認していません。

変数名やメソッド名に使える文字にはルールがありますが、「ASCIIコード文字に限る」のようなルールは存在しません。日本語だろうがUnicode記号だろうが使うことができます。

日本語 = "Japanese"

p 日本語 #=> "Japanese"

Q これなんの役にたつの?

A 特になんの役にもたちません。ただしユビキタス言語を定義するときにどうしても英語にできないものがあったときはつかえるかも?(例は思いつきませんが。)

「なんの役にもたちません」と言いましたが、やろうと思えば以下のような事もできます。

def √(x)
  Math.sqrt(x)
end

p √ 2 #=> 1.4142135623730951

def i(num)
  Complex(0, num)
end

e = Math::E
π = Math::PI

# オイラーの公式
p e ** (i π) + 1 == 0 #=> true

世の中にはこういうコードが「美しい」と思う人が一定数いるようで、こういうのがたくさん定義されたgem(例:unicode_math)が存在します。

おわりに

「基礎知識」と言いながらRubyの深淵を撫でるような内容になってしまいました。ボリュームも多いですね。しかし、これでも省略した内容が複数あります。

  • ブロックとはなにか
  • def には戻り値がある
  • なぜprivateと書くとその下のメソッドがprivateになるのか
  • 評価戦略

これらはまた別に機会があれば話せたらよいな、と思います。

お付き合いいただきありがとうございました。

参考文献

メタプログラミングRuby

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ダウンロード機能が改修される際に、理解のし易い構造にしたいという思いがあったので、空メソッドで定義し具象クラス側で必要な処理を定義する方針で実装しました。

実装してみて

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

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

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

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