フォルシアのブログ - TECH PLAY

TECH PLAY

フォルシア

フォルシア の技術ブログ

全251件

はじめに こんにちは。 フォルシア株式会社エンジニアの籏野です。 この度新規アプリを構築するにあたって、認証を通してからアプリにアクセスできるようにする必要が出てきました。 認証アプリにはKeycloakを利用し、Kubernetes(EKS)上にアプリをデプロイしています。 Kubernetes上にKeycloakアプリをデプロイするにあたり対応した内容を紹介したいと思います。 前準備 データベースの用意 Keycloakを利用するには、ユーザー情報等を保持しておくためのDBを用意する必要があります。 今回はAWSのマネージドサービスを利用できるため、CloudFormat
こんにちは。広報の見原です。今回の記事は、フォルシアが大切にしている「脱・人月」という考え方についてのお話です。 人月って何? 「人月」という言葉。建築系・IT系のお仕事に関わられていればご存じの方も多いかと思いますが、それ以外の業界や職種に就かれている方、学生の方には馴染みの薄い言葉だと思います。 人月とは、業務の工数を測る際に使う作業量(工数)を表す単位です。たとえば、5人で6ヵ月かかる仕事であれば、「5×6=30」で「30人月」ということになります。 また、「Aさんは一か月あたり100万円」というように労働者の商売単価を時間で測ったものが人月単価となり、人月とかけて費用を見積もったり、請求したりします。古くから、土木・建築業界で利用される一方、比較的新しいとされるIT業界でも多く活用されている尺度です。 フォルシアの価値は人月では測れない? IT業界に身を置いているフォルシアも例外ではなく、お客様のWebサイトの開発を行う際の費用算定に人月を使うことが多々あります。 しかし、フォルシアの開発スタイルは完全な受託ではありません。汎用的な技術基盤があり、それを活用してお客様からの要望に適った開発を行います。そのため、「Aさんが一人で3か月かけて開発したから300万円」というような単純計算がしづらいのです。 創業者の一人であるCOOの屋代(以下、COO)は自身もエンジニアであったことから、この人月の考え方はフォルシアのビジネスにはマッチしにくいと考えました。開発スタイルの問題だけではありません。 人月ではエンジニアが生み出したものに対する適正な価値がわかりにくいため、エンジニアに自身の価値を認めながら生き生きと働いてほしいと願うCOOにとってはもどかしさを感じる指標でした。 「フォルシアのエンジニアが提供する価値を正確に測れるようにするにはどうしたら良いのか」。 創業時からずっと「脱・人月」の思いと向き合い続けているCOOに話を聞きました。 COO屋代哲郎インタビュー 「脱・人月」の構想は前職外資系金融時代の経験が元 ー 日本のIT業界では長年費用算定に人月を使うことが一般的とされており、フォルシアでも使用しています。COOが「脱・人月」の考えをもつきっかけとなった出来事を教えてください。 21年前にフォルシアを起業してIT系の仕事を始めて、ITの世界では「人月」という尺度が一般的に使われているということを知り、驚くと同時に違和感を覚えました。前職での評価軸とはまったく異なっていたからです。外資系金融時代は成果主義が考え方の中心で、問われるのはあくまでも「成果」としての「稼いだ収益」。「時間」が尺度になることはありませんでした。なぜかというと、「成果」を「収益」として定量化しやすい指標として測ることができたから。なので、成果をあげている=たくさん稼いでいる社員は、極端な話、始業時間にオフィスにいなくとも、はたまた終業時間前にオフィスからいなくなっていようとも、それを咎める様な考え方はありませんでした。 ー 「成果」が評価される世界にいたら、「時間」が尺度になるのは「違う」と感じると思います。特に違和感を覚えたポイントはどこですか? お客様から「何日でいくらですか?」と初めて人月での見積もりを求められたときには戸惑いました。本来的に問われるべきなのは、「かけた時間」ではなく、「製作したソフトウェアの品質」ではないのかと考えていたからです。ソフトウェアのような「無形商材」を作る場合において、「かけた時間」というのはどのような意味をもつのか。「〇時間作業しました」と言っても、それがどれくらいの成果につながったのかの尺度として正しいのかは疑問が残ります。 ー COOの前職である金融もITも成果物が「無形商材」という点は同じですね。一方で、ITの世界で製作したソフトウェアやプログラムの出来を見て、客観的に品質の価値を測ることも難しいように感じます。開発する側はどのような点を意識すべきでしょうか? 納品時にお客様の要求仕様を同じように満たしたプログラムでも、保守性・耐障害性・拡張性などの観点での大きな差異が発生することがあります。ただそれは、表面的にはわからないことが多いのも事実です。特に、ソフトウェアの世界においては「将来起こりうることをあらかじめ予見したコードを書くことができる」かどうかが非常に重要です。たとえば今は一見無駄だと思われるようなコードでも、それが将来役に立つときが来ることを予見して書かれたのであれば非常に価値は高い。そういった価値の創出が積み重なってお客様との信頼関係は構築されていきます。 エンジニアにおける「属人性」は必ずしも悪ではない ー 「無形商材」だからこそ、表面的にはわからない価値を生み出すことは他社との差別化につながりますね。ただ、そういった価値を創出できるかどうかは、個々のエンジニアの経験や力量による部分が大きいかと思いますが、開発の上で属人性が発生することは問題ないのでしょうか? 教科書的には「属人性」は排除されるべきであるということが言われますが、それはあくまでも「顧客」もしくは「経営者」「管理者」の立場から言えること。長く第一線のエンジニアとして仕事をしてきた身からすると、「やりがい」は「属人性」から派生することが多いということも感じています。上述したような「将来的な拡張性のことまで考えて一見無駄だと思われるコードをあえて書く」行為、あるいは「このコードは自分にしか書けない」と思える瞬間は、エンジニアにとって一番面白く、快感や達成感があるものなのです。 私自身のエンジニアとしてのキャリアにおいても、「自分にしかできない」コードを書くことこそが仕事の原動力となり、エネルギーとなってきたと言うことが出来ます。一方で属人的なコードを書いてしまうと、将来的な保守性や拡張性の制約となってしまうことも事実です。単純な二元論に陥ることなく、バランスを取ることがソフトウエアの世界でビジネスを進めるうえで大切なことだと考えています。 ー エンジニアのやりがいを感じる瞬間は必ずしも第三者からの評価と直結しているわけではないのですね。現時点では、IT業界における最も優れた指標は人月なのでしょうか? 顧客や株主等、いわば「社外の第三者」に対して、その「成果の客観的な尺度」を示す必要があるという意味では、人月を使うことには妥当性があります。あるいは、現時点において人月以上に「一般的に公正妥当と認められる」尺度は存在していないと言っても過言ではありません。世の中のビジネスの仕組みは人月でまわっているので、やむなくそれをせざるをえないことは現実問題としてあります。しかし、それは必ずしも「最善」というわけではありません。私はフォルシアのポリシーとして「脱・人月」を持っていますし、それを堅持していく姿勢こそが大事だと思っています。 「脱・人月」の考えは社内評価にも ー COOが考える「脱・人月」の思いは社員に十分浸透していると思いますか? 折に触れて、伝えてはいます。さりとて、社員もそれぞれ様々な思いをもって、仕事をして社会生活を営んでいるはずですので、私の考えを一律で強制するつもりはありません。ただ、社員の重要な評価指標として用いている「3Cレビュー(以下、3C)」の考え方は「脱・人月」の思いを具現化したものであると考えています(参考: フォルシア評価制度 )。 ー フォルシア独自の評価制度3Cでは、一般的な360度評価では補いきれない、全社員(全方向)からの評価を総合的に判断して最終的な評価を行います。定量的な面だけでなく、定性的な面も見ることができるという点は、COOの理想の評価指標と言えるのでしょうか? そうですね。「客観性」や「妥当性」は大切ではあるものの、定量的な指標に過度に依存してしまうと、本来は成果を公正に測る為の指標であるべきなのに、「その指標を効率良く改善する為にどうするか」という本末転倒な方法論に陥ってしまう危険性があります。実際、3Cを導入したときに、定量的な指標を入れた方が良いという意見があり、試してみたことがあります。 しかし、エンジニアの定量的な指標としてはコミットの数とか書いたプログラムの行数くらいしか思いつきませんでした。ならば、売上金額であれば成り立つのでは?と思いますが、そうでもない。たとえば、たまたま売上金額としてはそこまで大きくない案件にアサインされたけれども、本人はすごい仕事をしていたり、その逆のことが起こったりして・・・。結局納得のいく結果は出ませんでしたね。長時間働けば良いわけでも、たくさん売り上げをあげれば良いわけでもなく、では何をしたら評価されるのか、という点を追求していくことが目的に近いところにあります。その点で、3Cは「定性指標」と「定量指標」を適度にバランスさせることによって上手く成り立っていると言えます。 フォルシア独自の評価制度3C 「〇〇が言うならそうだよね」と相手を納得させるビジネスセンス ー 定量的な指標はわかりやすいと思うのですが、定性的な指標という点で言うと、評価される側はどのように価値を創出したら良いですか? これは社内の評価制度である3Cにおいても、対外的なビジネスにおいても同様だと思うのですが、「フォルシア(or 社員名)が言うならたしかにそうだよね」と顧客や相手が納得してくれる状態が理想です。 会社自体や本人自身に説得力が付帯する魅力がないと、「なぜこの値段なの?」と聞かれたときに「これ作るの大変だったんですよ」という説明になってしまい、結局人月で価値を測るしかありません。3Cの考え方であれば、普段のその人の働きや振る舞いを相対的に見ることができるので、「〇〇が言うならそうだよね」が通用しやすい。3Cがワークしているのは、社内評価というドメスティックな環境での方法だからであって、「対外的な尺度」として取り扱う事には無理があります。対外的にも、理由を言わずとも「フォルシアが言っているのであればこれだけの価値がある」と納得してもらえるような状態を目指すために試行錯誤していきたいですね。 ー お客様にとっては成果がすべてですからね。お客様に「フォルシアが言うならそうだよね」と思ってもらえるために、具体的に私たちが今できることは何ですか? 「ビジネスセンスを磨く」の一言に尽きます。お客様が何に価値を感じるかを提供する側が知っているということが非常に大事です。お客様に言われたことを「はい、仰るとおりです」と言いながら作るのでは大きな価値を生み出すことはできません。ビジネスセンスがある人は「これを作ったらきっとお客様のビジネスに役立つし、喜んでくれる」という 肝 をわかっている。それを実際にお客様へ提供したときに喜んでもらえることが嬉しいし、仕事をしていて楽しい瞬間でもあります。 関係者みんなが幸せな状態を実現するために ー お客様が何に価値を感じるのかを理屈ではなく感覚でわかるようになるということですね。ビジネスセンスが研ぎ澄まされた社員が増えていったら最強な会社になりそうです!COOは、今後フォルシアをどのような会社にしていきたいですか? どのような会社にしたいかという具体的な姿はまだ見えていません。ただ、働いている人たちみんなが楽しく仕事をして、お客様や株主も喜んで・・・という形を実現するためにどうするかというのを考えるべきだとは常々思っています。そのバランスが崩れてしまうことは良くありません。バランスを保つ手段として人月という評価指標やそうでない指標を活用して試行錯誤しているところなのです。ここには簡単な解決策はない、という前提で関係者みんなで模索し続けていくことが大切なことだと思っています。 全社会議の様子 以上、20年以上「脱・人月」と向き合い続けてきたCOO屋代の思いをご紹介しました。 ここからは、COOの「脱・人月」の考えをもとに現場で働くエンジニア社員Kさんに話を聞きます。Kさんは前職の大手SIerで「THE 人月ビジネス」を経験してきました。エンジニアの立場から見て、働く上での違いはあるのでしょうか? フォルシア エンジニア社員Kさんの声 市場から求められるようなサービスを自分の裁量で作り出せる ー 前職では人月ビジネスを経験されましたか? 前の会社は大手SIerで、請負契約の仕事、準委任契約の仕事どちらもありました。人月型というのかはわかりませんが、どちらの場合も人月が見積もりの際の大きな根拠となっていたことは確かです。 私はずっと準委任契約のプロジェクトに属していたので、一人当たり1か月〇〇円での労働力提供というような人月ビジネスそのものを経験しました。 ー 仕事のやりがいはどのような部分で感じられていましたか? お客様が実現したい機能がたくさんあり、それらに対してスピード感をもって開発を進めていくことを常に意識していました。その結果、お客様に喜んでいただけたので、充実感がありました。また、数十人規模でチームを組んでいて売上額も大きかったので、部の稼ぎ頭チームとしてのやりがいもありました。 ー フォルシアは「脱・人月」をポリシーとしていることを知った上で入社されたのでしょうか? 入社前には大きく意識していませんでした。ただ、前職の仕事では作成するシステムはソースコード含め納品して顧客の所有物ということになるのが一般的でしたが、フォルシアの技術基盤であるSpookはソースコード自体はあくまでフォルシアが資産として所有していて、その利用料としてライセンス料をいただくというビジネスモデルなので、その点に魅力は感じました。そういったビジネスモデルの紹介から、フォルシアが「エンジニアの時間を売る」のではなく、成果物のバリューで勝負できていることは潜在的に感じていたように思います。 ー フォルシアでの「脱・人月」の考え方において、良いと感じる点や難しく感じる点はどこですか? 人月はサービスや製品の価格の根拠として、見積もりを提示されるお客さんとしても納得しやすい便利なツールではあると思っています。「脱・人月」となるとそれは根拠として提示できないため、説得力のある魅力的な製品・サービスを作り続ける必要があり、その部分は難しいと思います。 一方、 市場から求められるような素晴らしいサービスを自分の裁量で作り出せる舞台であるともいえるので、エンジニアとしての達成感ややりがいは十分にある環境だと日々実感しています。 さいごに 今回は、フォルシアで「脱・人月」の思いと向き合い続けるCOOと、その思いを受けてビジネスを推進しているエンジニア社員Kさんの声を紹介しました。 人月は長年ITの世界で使われている指標なので理にかなっていると言えます。すなわち、現在のIT業界においては、人月でないと開発したサービスの価値を測るのは難しい。 それでもフォルシアは、エンジニアに自身の価値を見出してより仕事のやりがいを実感してもらうために、そしてそれをより良いサービス・プロダクトを創り出す活力とするために、何をどうしたら良いのかを追求し、挑戦し続けます。 もっと詳しく話を聞いてみたいという方、フォルシアではカジュアル面談を随時受け付けています。ぜひお気軽に下記のリンクからエントリーをお願いします!
こんにちは。経営企画室の伊藤です。 昨日の 「フォルシアエンジニアの魅力編」 に引き続き、2019年に新卒で入社し、エンジニアのユニットリーダーとしてはもちろん、広報活動や経営者とともに会社課題を検討するチームでも活躍する webエンジニアの谷井 嶺太(たにい・りょうた)さんのインタビューをご紹介します。 (※所属、業務内容は取材時点の内容となります。) 本日は後編「4年目エンジニアの価値観編」です。それではさっそく伺っていきます! 3年目、大規模な開発案件で開発リーダーに! 谷井さんは昨年、年間MVP ※1 も受賞されたとのことでしたが... これまでの仕事の中で一番やりがいのあった仕事はどのようなものでしょうか? 昨年度(2021年度)に行ったJRダイナミックパッケージ販売機能の開発です。 複雑なドメイン知識を要求されるJRの空席照会・予約システムとの結合がある開発で、顧客(旅行会社様)ごとに地域など扱う商品特性に差があり、汎用的な設計を要求されました。約一年間という実装期間、さらには、webコネクトでも初となる大型機能の3社同時リリースという社内では比較的大きな規模の開発案件でした。 3社同時とのことでしたが、同じ機能を3社に提供したわけではなかったのですか? フォルシアが新機能を開発していくときの多くのパターンとして、クライアントドリブンでお客様へのヒアリングをもとに進めていきます。このJRダイナミックパッケージ販売機能を開発するにあたってもニーズを紐解きながらの作業になるのですが、1つの小機能を開発するのに3社分の要望が出てきます。JR様と初期開発時の顧客である3社の旅行会社様との間の契約もぞれぞれまったく異なっており、どこまで開発すれば汎用的な機能として成り立たせられるのかという点が未知数でした。 当時はわからないことだらけだったので、お客様から必要と言われれば全部作るしかないような状態だったり、対面にいるお客様も必ずしもシステムの専門家というわけではないので、お互いが理解している範囲に差があり、なかなか要望の全体像を一発で引き出せずに、五月雨に要望をキャッチアップしていくことになってしまうという――そういう難しさが3社同時並行で発生していました。 フォルシアの開発スタイルはウォーターフォールではなくいわゆるアジャイル開発のため、設計→実装→ヒアリングを相当数繰り返し、次々に出てくるチャレンジングな仕様に対して試行錯誤しながら乗り越えていく過程は、タフで難しくもあり、おもしろかったです。 この案件では、コーディングや要件整理だけでなく、開発リーダーとしてチームメンバーのオンボーディングやタスク管理・アサインなど、より幅広い業務にあたることになったのも印象的でした。業務を分解し、なるべく完結していて切り出しやすいタスクにしてメンバーに渡したり、こまめにドメイン知識の補足を入れたり、どういう状態になっていることが望ましいかの完了条件を言語化することも工夫しながら進めていきました。 ※1 「MVP」 社内表彰制度における、年度内最も活躍した社員としての表彰 MVPへの推薦コメントを拝見しても、やはりこの案件での目覚ましい活躍が伺えました。続いては開発業務以外の面について聞いていきます。 幅広い知見をキャッチアップできる社内の横串組織 谷井さんはエンジニアとしての幅広い活躍だけでなく、社内の横串組織(部門横断型のワーキンググループ)にも積極的に参加されていると聞きましたが... 開発業務以外では、3つの横串組織に参加しています。 まず、3つの中で一番長く携わっているのが「技術広報チーム」※2 です。フォルシアのエンジニアとして大切な「訴求力のある伝え方」に興味があり、約三年前に発足したチームに立ち上げ当初から参加しています。2つ目は、「postgresタスクフォース」。タスクフォースは、エンジニア有志が参加し、特定の技術領域に関する活動を行うものなのですが、これまでの業務で相対的に訓練する機会が少なかったpostgresの知識を身に着けるため参加しました。そして3つ目は異色な活動なのですが、今年度から始まった「sense5」※3 という経営者と会社課題について考えていくチームにお声がけいただいて、社長やCOOと直接議論できる貴重な機会だと感じて参加しています。 ※2 「技術広報」の活動についてはこちらの記事もご覧ください < こちら > ※3 「sense5」の発足のきっかけなどはこちらの記事もご覧ください < こちら > 先ほどの、技術もビジネスもどちらも関わっていきたいということと同じで、なるべく幅広い視野をもっていたいという想いがあります。フォルシアのエンジニアは通常の業務においても広い領域に携わっていますが、均等に習熟できるわけではありませんし、プロジェクトやチームの状況などによっても異なります。横串組織に参加することで、関われる領域を主体的に選んで、幅広いキャッチアップができるように色々やってみたいと思っています。 思い返すと大学1年生の最初の頃は、サークルに3つくらい入っていたんです。それも、色々やってみて本当におもしろいと思ったものを続けようと思って、そういった身の置き方をしていましたね。 これらの横串組織の活動全部が開発業務に直接的に還元されているかといったら、そうでないことも多いですが、目の前の業務に活きることだけをやっていても幅は広がっていかないので、あえて飛び石になっているような部分にも参加して、それらが業務に繋がれば儲けものだし、繋がらなかったとしてもそれはそれで良いかな、くらいに捉えています。 例えば、いま参加しているものの中で「sense5」は開発業務とは離れたものではありますが、活動のコンセプトは自分がやってみたいと思っていたことのひとつでもあります。元々大学4年の時に部活の主将をやっていたのですが、組織を良くするためにどうしたらよいかというのを色々な観点から考えて実行していくプロセスにおもしろさを感じていて、就職活動の際にもコンサルティング業界に興味を持っていました。「sense5」の活動を通じてもっとこうしたら組織として良くなるのではないかということは引き続きやっていきたいと思います。 就職活動のお話も出ましたので、当時のことを深堀させてください。 大切にしたのは「納得感」、「誠実さ」 就職活動ではどのような会社を見られていたのですか? 戦略/ITコンサル、総合商社、金融系、ITベンチャーなど幅広く見ていました。エンジニアとして働くかどうかも含め、仕事内容そのものにはあまりこだわりがなく、働き方や価値観が合う場所を探していました。フォルシアに応募した時もエンジニアか営業か、職種は決めずに応募しました。とにかく納得感を持って仕事に臨みたいという想いが強かったです。 私の価値判断の基準の原体験はだいたい大学の部活なんですが、「これだ!」って思って決めて、自分の中で考えに考え抜いて納得したことに関してはとても馬力が出て頑張れるんです。ただ、人に言われただけとか、納得いっていないこと、筋が通っていないことはなかなかできない。 あとは、「誠実さ」という点も重視していました。自分が部活で主将をやっていたとき、「熱意」と「誠意」というのをキーワードにしていて、自分自身が熱を持って取り組むということと、そうやって熱を持って取り組んでいる仲間や自分自身に対して、きちんと誠意を持って向き合う、ずるいことをしないで向き合うということを大切にしていました。就職先においても、ずるしてお金だけ儲けられればいいや、自分が成長できればいいやという考え方ではなく、誠実な人がいる場所、誠実な働き方ができる場所で働きたいと思っていました。 フォルシアは谷井さん基準の「納得感」・「誠実さ」に合致したということですね。 フォルシアのことは、ベンチャー企業を中心に紹介してくださるエージェント経由で知りました。社長やCOOが語られている内容は自分の中ですごく誠実だと思えたし、その考え方に対して頑張ることができそうだなと感じました。また、一概に納得いかないことはやりたくないという訳では決してなく、正しくないと感じたときに相談・議論できるような土壌があることが大事だと思っていて、自分が就職活動で見た中ではフォルシアが一番そこが担保されているように感じたということも大きかったです。 就職活動の選考過程で、(わがままを言って)たくさんの社員の方と面談させていただいたのですが、落ち着いていて理知的で魅力のある方ばかりで、それぞれ自分の言葉でフォルシアの仕事のおもしろさを語ってくださったことは印象的でした。 最後までどこに行くか迷って内定承諾期間を延ばしてほしいと依頼した自分に対して、人事部長の外山さんが「谷井さんの納得がいくまでお付き合いしますよ」と言ってくださったこともよく覚えています。 自分自身に誠実に向き合い、納得感を大切に進む谷井さんに、これからの姿についても聞いてみました。 webコネクトをSaaSプロダクトとして確立する 今後取り組んでいきたいこととは? webコネクトをSaaSプロダクトとして確立することです。 従来フォルシアは受託開発でのビジネスが主流でしたので、SaaSプロダクトとしての開発フローのノウハウがまだ確立していません。実装面でも、チーム運営の面でも、仕様調整・お客様とのコミュニケーションの面でも改善の余地がまだまだたくさんあると感じています。 これまでは、webコネクトの機能もクライアントドリブンで開発が進み、お客様と一緒に機能をつくってきましたが、そのスタイルはプロダクトが大きくなればなるほど難しくなっていきます。受託開発では、ある意味シンプルにお客様の要望にきっちり寄り添っていけば、いつしか信頼に繋がっていくと思うのですが、SaaSプロダクトの場合はお客様からの要望に対して線引きをせざるを得ないケースも出てきます。そういう状況下でもお客様とフェアネスを築き、信頼を勝ち得ていくことがこれから必要になってくると感じています。 お客様に「SaaSプロダクトとしてのwebコネクトを使っていて良かった」と感じてもらえるように、SaaSプロダクトであることの利点をもっと還元できるようにしていきたいと思っています 。   エンジニア × α で価値を発揮させる 谷井さん個人としての目指したい姿とは? 正直な話すごく悩んでいます。自分は生粋のエンジニアではないと思っていて、単純に手を早く動かして素早くつくるとか、綺麗にわかりやすくつくるという点においては、自分じゃ一生敵わないだろうなと感じる方が社内にもたくさんいます。そこで、何かもっと掛け合わせで、コーディングだけじゃなくこれもできます――というところで価値を発揮していきたいと思っています。ただ、その「何か」が果たして何なのか...それもある意味何でもいいと思っていて、結果的に自分が最大のパフォーマンスを発揮できることが大切なので、「これだったら誰にも負けないぜ」というような領域を見つけていきたいと思っています。 最後に、未来の仲間へメッセージをお願いします! フォルシアに入って4年目になりますが、何をどう作るかの意思決定に携われる環境、幅広い技術領域に自ら関われる横串組織、手厚い福利厚生や柔軟な働き方、社員の自律性を尊重した制度、気軽に質問できるフラットな文化など、エンジニアとして働くにあたってここを超える環境はそうそうないと感じています。 経験の有無を問わず、新しいことに主体的に取り組める方にはもってこいの会社だと思います。一緒に働けることを楽しみにしています。 プロフィール webエンジニア 谷井 嶺太(たにい・りょうた) 入社年度: 2019年新卒入社 所属部署: 旅行プラットフォーム事業部 旅行第5グループ オフの過ごし方: 睡眠・ゲーム・躰道 自分を漢字一文字で表すと...「 整 」 ゼロから何かを創り出すよりも、既にあるものを整える方が得意です。 フォルシアを漢字一文字で表すと...「 信 」 社員一人一人にプロとしての誇りを持つことを求めつつ、信頼して尊重している会社だと思います。   インタビュー後記 フォルシアのエンジニアらしさが詰まった前編の「フォルシアエンジニアの魅力編」、そして谷井さん自身の想いが詰まった今回の後編「4年目エンジニアの価値観編」、いかがでしたでしょうか? フォルシアのwebエンジニアの仕事内容や、谷井さん自身の人柄が伝われば幸いです。 フォルシアでは社員とのカジュアル面談を行っています。少しでも興味を持っていただけましたら、ぜひ下記のリンクからエントリーをお願いします♪
こんにちは。経営企画室の伊藤です。 フォルシアでは通年、積極的にキャリア採用を行っており、大手マスメディアや旅行会社、金融業界の出身者など様々なバックグラウンドをもった社員が活躍しています。 今回は2019年に新卒で入社し、エンジニアのユニットリーダーとしてはもちろん、広報活動や経営者とともに会社課題を検討するチームでも活躍する webエンジニアの谷井 嶺太(たにい・りょうた)さんのインタビューを前後編でご紹介します。 (※所属、業務内容は取材時点の内容となります。) まずは前編「フォルシアエンジニアの魅力編」です。 技術力 × お客様とのコミュニケーション で価値を発揮 谷井さんの現在のお仕事内容を教えてください。 フォルシアのSaaSプロダクトである 「旅行・観光業界向け商品販売プラットフォームサービス『webコネクト』」 の開発に携わっています。 webコネクトの社内の開発体制は、大きく分けて2つのコンセプトでチームが編成されています。1つは、中長期的にプロダクトラインナップを拡充していくチームで、もう1つは、既にwebコネクトを導入いただいたお客様からの要望をもとに、その実装可否を検討し、お客様とコミュニケーションをとりながら開発を進めていくチームです。もちろん綺麗に二分されているわけではなく、都度ケースによって対応するチームを検討する場合もありますが、大きく分けるとこの2つのチームがあり、私は後者のお客様とコミュニケーションをとりながら開発を進めるチームのユニットリーダーをしています。 もちろん前者後者どちらのチームであっても業務のなかで開発業務が占める割合は多いのですが、後者のチームの働き方の特徴として、お客様とコミュニケーションをとる機会が多いのはもちろん、SaaSプロダクトのwebコネクトに実装すべき機能の範囲をしっかりと検討し、見極め、お客様の要望に対してどういった回答を出していくのかをビジネスサイドとともに考えていくというものがあります。 フォルシアにとってSaaSプロダクトをつくっていくのは初めての試みで、プロダクトのつくり方が確立されていないところもあります。 お客様(旅行会社様)から出る要望の多くは技術的には対応可能ですし、言われたことを言われたままに作ることはある意味簡単ではありますが、エンジニアのリソースも有限な中で、SaaSプロダクトとして本当に価値のあるものを作るためには優先される機能を見極める必要があります。お客様のビジネスにどれくらいプラスのインパクトを与えられるのか、開発にどれくらいのコストがかかるかなどを総合的に考え、実装可否の判断やお客様への回答の仕方をITコンサルとともに考え、実際に対応するとなればその開発まで携わります。 エンジニアとは言え、非常にビジネスサイドまで踏み込んだ業務なんですね。ITコンサルとの役割分担はどうなっているのでしょうか。 やはり技術的な観点に関してはエンジニアから意見を出していく必要はあります。 例えば、「この機能は各社ごとのカスタマイズができるレイヤー内で実装が可能かどうか」「この機能を実装することで今後の開発時に考慮すべきコーナーケースが発生しないか」といった観点はエンジニアだからこその部分です。 いわゆる"技術負債"を残さないような設計にするため、実現される機能の価値とは別にシステム全体の美しさを保つための判断もエンジニアが行うべきことです。 また、お客様とのコミュニケーションといった意味では、いわゆる運用保守のような業務もあります。不具合の修正や、設定変更、メンテナンスについてなど...こういった内容はエンジニアが直接お客様と会話して進めていったりもします。 業務の難しさ、おもしろさとは? 幅広く対応されている中での難しさやおもしろさはどういったところでしょうか? 難しさも楽しさも色々な場面で感じていますが、わかりやすい例としては実装が綺麗に横展開できた時でしょうか。webコネクトでは原則的に、機能を追加すると複数のお客様で使えるようになります。それを見越して、B社からある機能要望があった際に、「この要望を叶えるだけであればここまでの開発で良いが、似た要望はほかの会社でもありそうだから、横展開のために汎用的に開発しておこう」というような"読み"が上手くはまって追加コストを抑えた横展開ができると、パズルが解けたときのような気持ちの良さがあります。 また、私自身の志向性として、言われたものをただつくるのではなく、自分がつくるものに納得感を持って開発していきたいという想いがありますし、器用貧乏なので一点集中型のエンジニアというタイプでもありません。そこで、コーディングと、ほかにも自分にできることを掛け合わせることで価値を出していきたいと考えています。 いまの働き方は、エンジニアとして技術のこともわかりますし、お客様と直接コミュニケーションをとることもできるので、伝える・つくるといった両面に携われます。技術についての理解・知見からわかりやすく伝えていくことができ、お客様の声を直接聞いていることで要望の意図を汲み取った実装をしていける――そういう両面から価値を発揮できる環境はおもしろいです。 webコネクト とは? ダイナミックパッケージ型商品におけるリアルタイムな料金計算と高速な一覧表示を実現する旅行・観光業界向けのSaaSプロダクトです。商品のオンライン販売に求められる素材登録(造成)、検索、予約、電子クーポン、外部接続ゲートウェイといった機能群をモジュール化し、顧客ニーズに応じてカスタマイズして必要な範囲で提供することができます。 「webコネクト」を活用することで旅行会社、鉄道会社をはじめとする旅行・観光商品を販売する事業者や素材提供会社は、例えば目的地までの交通手段の予約と、現地での宿泊・レンタカー・アクティビティ等の手配とが一括で済むような仕組みをスムーズかつローコストで導入することができます。 とある日の谷井さんの1日のスケジュール システム全体を俯瞰してつくるためにはチームでの密なコミュニケーションが大切 「チームで開発する」とは? フォルシアでは「チームで開発する」という表現をよく聞きますが、チームで開発とはどういうことでしょうか? いろいろな要素があるのですが、一番は実装方針の相談をしあうという側面が大きいですね。一つの機能を実装するにあたって、そのコードの書き方は何通りもの方法があり、やりようによってはいくらでも"読みづらい"書き方や"見通しの悪い"設計もできてしまいます。そんな中で、どういった書き方にすると他の人にとっても編集しやすいかたちになって、システム全体として綺麗なつくりになるかという部分はチームで相談しながら進めていきます。 例を挙げますね。 webコネクトでは「マイクロサービスアーキテクチャ」という戦略をとっていて、システム全体として1個の大きいアプリというわけではなく、いくつものアプリをつくって、それぞれに役割分担をさせて成り立っています。これによって、それぞれのシステム同士はお互いに絡み合わないようにして、機能に関わる修正範囲をなるべく限定的にできるというメリットがあります。 具体的には、宿泊施設、航空素材、列車素材といった商材ごとのアプリと、画面描画や料金計算などの商材を跨いだ機能ごとのアプリで構成されています。 従って、新しい機能や処理を追加する場合には、どのアプリで処理を行うべきかを判断する必要があります。 こういった判断は様々な粒度で必要になり、多くは絶対的な正解がありません。システム全体でのメンテナビリティやパフォーマンスを総合的に考えながら、チームでコンセンサスを取って進めなければいけません。 「これ、こうやろうと思うんだけどどうかな?」「それだったらこっちの方がいいんじゃないですか?」というような相談をするというのが一番コミュニケーション回数としても多いと思いますね。 チーム内では「宿泊施設のアプリはAさん、列車素材のアプリはBさん」といったような役割分担がされているんですか? いえ、そこは原則チームみんなで全部のアプリを見る形を取っています。 そうはいっても、初期開発時にはアプリごとにある程度特定の人がまとめて開発していったほうが効率的なので、そうなると属人化というか、開発した人が一番詳しくなるものなので、個々人ごとにどの領域に知見があるかという色は出てきます。それが一定以上偏ってしまうと、その人が抜けてしまった時にチームが回らなくなってしまう恐れがあり不健全なので、ある程度、詳しいものとそうでないもののグラデーションはありつつも、なるべく分散させてチームとして健全な形になるよう気を付けています。 属人化させないための工夫とは? お客様の業界のドメイン知識のようなものはなるべくesa(社内の情報共有ツール)に書き出して残すようにしています。あとはチーム内でのアプリング(社内用語)で知識の偏りを減らす取り組みをしています。 アプリングは「アプリについて輪になって話す(app + ring)」という意味の社内造語です(COOが命名されたそうです)。チームによってやり方は異なりますが、webコネクトでは週1,2回行っていて、特定の機能に詳しい人が持ち回りで「ここはこういう形になっていて、元々こういう要望があって、こういう実装になっていて~~」というような説明を30分~1時間程度の枠の中で紹介する形式を取っています。全員で理解を深めることで、元々の開発に携わっていない人も、実装の修正や対応をできるようにするという目的があります。また、説明する側にとっても、人に説明することで構造化されて改めて理解が深まるというメリットもあります。 やはりドキュメントを残すというのは結構大変な作業なので、開発業務で忙しい中だとドキュメントを残す暇があったら自分で修正してしまおうというマインドにもなりがちです。そこで、アプリングの場ではあえてあまり資料は作り込まないようにしてもらっていて、その場で参加者みんなでesaをオンライン編集していくようにしています。講師役の社員の説明を聞いて書き残しておいた方が良いと感じたことを参加者が書き記して全員の共有財産にしていって、負担をかけずにその人の持っている知識を周りに伝播させていくようにしています。 プロフィール webエンジニア 谷井 嶺太(たにい・りょうた) 入社年度: 2019年新卒入社 所属部署: 旅行プラットフォーム事業部 旅行第5グループ オフの過ごし方: なにもしないことが一番のリラックス方法なので好きなだけ寝ています。また、大学時代から始めた躰道(たいどう)という武道の道場で週1回稽古しています。 自分を漢字一文字で表すと...「整」 ゼロから何かを創り出すよりも、既にあるものを整える方が得意です。 フォルシアを漢字一文字で表すと...「信」 社員一人一人にプロとしての誇りを持つことを求めつつ、信頼して尊重している会社だと思います。 前編 フォルシアエンジニアの魅力編 前編「フォルシアエンジニアの魅力編」はいかがでしたでしょうか。フォルシアのエンジニアだからこその仕事の幅や、チームで働くという働き方、谷井さんの想いが伝わりましたら幸いです。明日は後編「4年目エンジニアの価値観編」をお届けします。お楽しみに!
こんにちは!新人エンジニアの宮本です。 みなさんはアルゴリズムを使ってプログラムを高速化していますか? アルゴリズムを工夫するだけでこれまで長時間かかっていた処理が一瞬で終わると感動しますよね。 PostgreSQLでは、与えられたクエリに対してプランナが実行計画を立てますが、ここでもアルゴリズムを使った高速化が行われています。 この記事では、その1つである 移動集約モード について紹介します。 移動集約モードとは? 移動集約モードは、集約関数を含むある種のクエリを高速化する機能です。 対象となるクエリ key カラムと value カラムをもつ10行のテーブル test を用意
こんにちは、経営企画室 広報の伊藤です。 「そもそもSpookとはいったい何なのか?」 、 「Spookの強みとは何なのか?」 に続く第三弾、本企画のラストは「どんな企業で導入されているのか?」、「今後どんな未来を見据えているのか?」について社員の生の声で解説していきます! 今回もこちらのお二人に伺いました。 Spookについて教えてくれた先輩社員プロフィール (右)ITコンサル:DXプラットフォーム部 営業部長 諏訪 (左)エンジニア:旅行プラットフォーム部 グループ長 西山 ITコンサルタント職 DXプラットフォーム部 営業部長 諏訪 俊(すわ・しゅん) /写真右 2017年 キャリア入社 旅行会社での海外渡航手配や法人営業を経て、2017年にフォルシアへ入社 前職での経験を活かし、旅行・観光業向け営業活動に5年間携わり、今年からはDXプラットフォーム部で非旅行業界向け営業・コンサル活動に従事 エンジニア職 旅行プラットフォーム部 グループ長 西山 諒平(にしやま・りょうへい) /写真左 2015年 新卒入社 新卒での入社後 、 大手旅行サイト開発を経て、現在は 旅行・観光業界向け商品販売プラットフォームサービス「webコネクト」 のプロダクト開発に従事 ※所属は2022年7月現在のものとなります それではさっそく聞いていきます! Spookの導入先とは... 検索対象となるデータが膨大であり、検索条件が複雑。そんなデータを取り扱う業界でSpookは本領発揮 Spookは旅行・航空業界での導入が多いですが、それはなぜでしょうか? 諏訪 /旅行・航空業界で導入されているSpookを用いて開発されたシステムは、「宿泊商品販売」や「ツアー商品販売」における検索システムです。いずれも「宿泊」を伴う商品となりますが、宿泊プランというのは同じホテル、部屋であっても季節、空室状況、オプション条件などにより様々な料金設定がされます。 宿の検索画面イメージ このように同じ商品に対して多様な料金パターンが存在すると、検索対象となるデータが膨大・複雑になるため、データの圧縮が得意というSpookの特長が最大限に活かされるというわけです。 さきほど話にあがった通り、特定の宿ではなく希望条件に当てはまる宿を探したいとき、「知らなくても希望にあったものが見つけられる」検索が業界のニーズに当てはまったことがSpookが広く導入されることに繋がっていると思います。 では、旅行・航空業界以外ではどのような業界で導入されているのでしょうか? 一つの仕組み、一つの部品の検索から、一つのものを作るために必要な部品を複数検索するというステージへ 諏訪 /大量の商品数を扱われる商社やECサイトを運営する企業で導入いただいています。最近では工作機械を扱う企業や医科理化学機器を扱う企業様からも引き合いをいただいています。 開発の内容も、複数素材の組み合わせによる見積パターンを条件指定型で実現するという新しいSpookの活用方法にて採用いただいているケースもあります。簡単にいうと、部品と部品の組み合わせによる見積の算出・表示のシステムです。部品Aを使うとなったら、その部品Aに合う部品はこれですといったもので、非常に複雑な検索の制御などが発生してきます。こういった複雑さはまさにSpookの強みが活きるところです。 これまで、一つの仕組み、一つの部品の検索が求められていましたが、いまは、"一つのものを作るために必要な部品を複数検索する"という新しいステージに来ているように感じます。 膨大な商品数があり、商品に対する絞り込みの条件(スペック)が多く存在し、商品によって絞り込む対象スペックが異なるような場合、それを全て活かして検索画面上で表現することは難しく、パッケージで限定的な対応に留めるか、フルスクラッチで自社内製部隊を構えて必要な機能を作り上げていくか、難しい選択が迫られます。 だからこそ、パッケージと自社内製の良いところを併せ持ったサービスを提供できるSpookが高い評価を頂き、導入に繋がっていると考えています。 今後も、旅行業界のように業界全体で導入が進むようなパターンは考えられますか? 諏訪 /旅行・航空業界のように業界全体で広く導入が進むというのは特殊なパターンで、旅行・航空業界の取り扱うデータの膨大さ、組み合わせの複雑さゆえのものです。検索から提案が始まった旅行・航空業界のシステムにおいても、まだまだフォルシアが携わっている領域は部分的でしかありません。これからも深くペネトレイトしていきたいところです。 そのためにも、今後の営業展開として固定の業界をターゲットとしていくということはなく、業界問わずSpookの特長がはまる新しいお客様を探して行ければと思っていますし、今後もSpookへのニーズは新たに出てくるのではないかと考えています。 Spookの今後とは... 価値の源泉であり競争力となる「速さ」を突き詰める Spookは今後どのように発展していくのでしょうか? 西山 /今後のSpookの発展において、エンジニアとして3点注力させていきたいと思っています。 検索本体のさらなる高速化 クラウドを利用して、柔軟なシステム構成(k8s, aws) SaaSとして提供(webコネクトもSpookを用いて開発されています) それぞれ解説しますと、1点目については、検索処理速度・表示速度が速いということが価値の源泉であり、競争力だと思っているのでそこは突き詰めていきたいです。現状のSpookはお客さんのデータをバッチで取り込んで表示しているため若干のラグがあるのですが、速さを突き詰めた「リアルタイム化」は今後のSpookの発展の余地として残っています。 2点目については、昔に比べてインフラの柔軟性が高まり、システム負荷の面でのお客さんのニーズに応えやすくなっているので、より一層、Spookとクラウドのサービスを組み合わせて柔軟なSpookを作っていきたいと考えています。 最後の3点目は、 webコネクト (自社SaaSプロダクト)とありますが、Spookを用いた開発を通して見えた検索に求められる共通点や課題をSaaSのプロダクトの中に組み込んでいき、より多くの顧客・ユーザーに検索サービスを使っていただきたいと思っています。 新しいものを作る機会を増やし、新しい機能が提供できる土壌を整える 営業サイドからはいかがですか? 諏訪 /Spookはフォルシアのこれまでの継続的な挑戦がノウハウとして集積された独自の検索エンジンです。先ほどもお話した通り、Spookに「完成形」としてのゴールはなく、フォルシアが日々お客様へサービスを提供するなかで、常に新しい知見を取り入れて成長していきます。 中核となっている「膨大・複雑なデータに対する検索の強み」は変わることはありませんが、その活用方法や求められる機能に応じて自由に発展を続けることができる仕組みだと思っています。 営業観点ではとにかくSpookを使って新しいものを作る機会を増やしていきたいです。そうすることで、さらにSpookのカスタマイズ(=進化)が進んで、新しい機能の提供ができる土壌が整い、良い循環が生まれていくと思っています。 だからこそ、Spookに興味をもってフォルシアに入社する方が増えてほしいです。 目新しいSaaSサービスを作るということはフォルシアでなく別の会社でもできることです。フォルシアも今では自社のSaaSサービスを展開していますが、それはSpookという軸があってこそ生まれてきたもの。フォルシアにはSpookという軸があり、Spookを用いたサービスを柔軟に自由に考えて作っていけるということを一緒に楽しんでいってほしいと思っています。   インタビュー後記「Spookとは...」 ITコンサル、エンジニアという職種の異なる2人の先輩社員からのSpook解説三部作はいかがでしたでしょうか。私としても、営業の現場ではこういった言い回しで紹介しているのか...!だったり、エンジニア用語をよりかみ砕いて説明いただくことで理解が進んだりと非常に学びの多い時間となりました。 今回、採用時に多く質問いただくSpookについて紹介させていただきましたが、文字で読むのと実際に社員と会話するのではきっと感じ方や得られるものは異なると思います。フォルシアでは通年カジュアル面談を行っていますので、少しでも興味を持っていただけましたら、是非下部のフォーム(水色ボタン)よりご連絡いただければと思います! フォルシアの継続的な挑戦の証であり、さらなるビジネス展開のための源泉であるSpookについて興味を持ち、より良いSpookを一緒につくり、広めていきたいという新しい仲間に出会えることを楽しみにしています。
こんにちは、経営企画室 広報の伊藤です。 先日の 「そもそもSpookとはいったい何なのか?」 に続き、今回は第二弾「Spookの強みとは何なのか?」について社員の生の声で解説していきます! Spookについて教えてくれた先輩社員プロフィール (右)ITコンサル:DXプラットフォーム部 営業部長 諏訪 (左)エンジニア:旅行プラットフォーム部 グループ長 西山 ITコンサルタント職 DXプラットフォーム部 営業部長 諏訪 俊(すわ・しゅん) /写真右 2017年 キャリア入社 旅行会社での海外渡航手配や法人営業を経て、2017年にフォルシアへ入社 前職での経験を活かし、旅行・観光業向け営業活動に5年間携わり、今年からはDXプラットフォーム部で非旅行業界向け営業・コンサル活動に従事 エンジニア職 旅行プラットフォーム部 グループ長 西山 諒平(にしやま・りょうへい) /写真左 2015年 新卒入社 新卒での入社後 、 大手旅行サイト開発を経て、現在は 旅行・観光業界向け商品販売プラットフォームサービス「webコネクト」 のプロダクト開発に従事 ※所属は2022年7月現在のものとなります それではさっそく聞いていきます! Spookの強みとは... パッケージでも自社内製でもない優位性のあるポジション 他社にはないSpookならではの強みとは何でしょうか? 諏訪 /Spookはお客様にとって、パッケージでも自社内製でもないところが強みだと考えています。パッケージだとどうしても機能の拡張性や自由度に限界があり、自社内製とするには多くのエンジニアを抱えたりサービス維持のために必要なコストが大きすぎるというジレンマがあります。 Spookは、これまでフォルシアが開発してきた検索システムの知見をベースとして新しい機能を検討して行くことができるので、ゼロベースでの開発ではない検討ができます。また、エンジニアを多く抱えたりサービス維持のノウハウがなくても、フォルシアのエンジニアがそれを直接サポートするため自社内製に近いかたちで自由な機能改修、機能拡張を実現できます。 先ほど西山さんからも「レシピ本と包丁とフライパンのセット」というお話がありましたが、Spook、パッケージ、自社内製をそれぞれ"カレーライス"に例えたら... パッケージ =レトルトカレー 自社内製  =自宅に料理人を抱えて作ってもらったカレー Spook    =????? 西山 /自社内製もSpookもどちらも料理人が作るカレーなのですが、自社内製は料理人が「鍋はどこにありますか?」「どんなカレーが(ビーフ?チキン?シーフード?)良いですか?」「このスパイスないと作れないですよ?」といった風にたくさんの会話が必要で、多くのやり取り・多くの時間がかかってしまうイメージです。さらに、料理人たちは分業していて、自分の担当している部分以外のことになると都度確認が発生してしまう。 一方、Spookを用いて作られるカレーは、自分のことをすごく理解してくれている料理人が、だいたい必要となる下ごしらえを済ませた状態で、「(常連さんのこれまでのオーダーから察するに)この味付けが好きだと思うのでこんな感じに作ってみましたがどうですか?」「下ごしらえはできているので追加の注文伺いますよ」といった感じで作られるカレーのイメージです。 Spookというお作法・ノウハウ、これまでに作ってきた機能があるからこそ、一番良いものを、気持ちの良いコミュニケーション量で作ることができます。 諏訪 /パッケージで事足りるのであれば、パッケージのほうが安くて容易に導入できるので良いと思います。ただ、パッケージでは事足りず、かと言って自分たちで何でもやる(=自社内製)となると費用も手間もまかなえないという場合はSpookがぴったりだと思います。 営業線上、パッケージと勝負することもあるし、SIer(自社内製)と勝負することもあります。その際にパッケージでも自社内製でもない中間のポジションであるということを強みとして押し出しています。 ポジションに優位性があるということですね。 ほかにもSpookのすごさを語る上で「速さ」は欠かせないと思うのですが、Spookは何で速いのでしょうか? Spookといえば、高速検索。高速検索の秘訣はデータの圧縮技術 西山 /一言でいうと、データベースの圧縮が得意だからです。 例えば旅行会社のもつツアーの料金データは、ツアーごとに1日の料金が設定されていて、平日と祝日でも金額は異なるし、大人と子どもでも異なる。そうなると一つのツアーだけでも数レコードの料金データとなり、大手の旅行会社さんだと取り扱うツアーの数も何千何万とあるので、料金データは非常に膨大なものになります。このデータを検索する際に全部上からなめていくと、あまりのデータの多さに検索ボタンを押した後、画面がかたまって処理中の画面がずっと出続けてしまいます。 ただ、Spookの技術においては裏側でその膨大なデータを圧縮して保持しているので、検索ボタンが押された後すぐに検索結果を表示させることができます。 諏訪 /フォルシアはデータ保持の仕方に関する特許をもっていて、その特許技術を使った圧縮方法でデータサイズを小さくすることによって高速検索が実現できています。 西山 /これがSpookの高速検索の速さの要因です。 他にも他社の検索機能との違いという点では、Spookは絞り込まなくても検索ができるという強みがあります。他社の検索の場合、例えばそのコンピューター上で処理できる情報の数が100だとしたら、100にするために日付を決めて、出発地を決めて、参加人数を決めてという風にデータの全量が100になるように条件で絞り込んで行く必要があります。一方Spookであれば、データを圧縮して保持しているため、日付未指定であっても、出発地が決まっていなくても、料金の上限下限のレンジを設定していなくても検索することが可能です。 諏訪 /これは私のイメージなのですが... 少ししか情報を書けない一筆箋が1万枚ぐらい詰まった箱から1枚の紙を取り出すのと、一筆箋100枚分の情報が書かれているカードが100枚入っている箱から1つの情報を取り出すのとどっちが速く取り出せるかっていう話かなと思っています。 Spookは、一筆箋1枚1枚を1データとして捉えるのではなく、データの圧縮技術によって100個のデータが固まっているものを1データとして捉えているようなイメージなので、1万枚から探すのではなく、100枚の中から探せば良い。要は探す対象となるカードの枚数を減らせるから、たどり着きたい情報を速く探し出すことができるんです。 諏訪 /速さというパフォーマンスに一番直接的に影響するのはレコード数(データの量)なので、いかに対象とするレコード数を抑えるかというのがフォルシアの着眼点であり、技術の真髄です。 なるほど!速さの秘訣が少し解明された気がします。 他社との違いといえば、検索というと真っ先に浮かぶのはキーワード検索かと思うのですが、Spookの検索とキーワード検索との違いについて教えてください。 コンセプトは、「キーワード検索で探せないものを検索する」 諏訪 /そもそもSpookは「キーワード検索では探せないものを検索する」っていうコンセプトで生まれています。キーワード検索だと、そのキーワードを知っていれば検索できるけれど、そうでなかったら検索できません。特に旅行先の宿を探したいとなったとき、知っている宿しか検索できなかったら選択肢の幅がすごく狭いわけです。ユーザーとしては、"その宿"に泊まりたいのではなくて、"その条件を満たしている宿"に泊まりたいわけで、条件にさえ合致するならば絶対にそこでないといけないということはないものです。 だからこそ、複数の条件からそれに合致する施設が全国にどれくらいあるのかを検索できるという点がSpookの良さとして旅行・観光業界に採用されています。 Google 検索でミラコスタに泊まりたい人がミラコスタを検索することができても、ミラコスタに似ている他のホテルを見つけることは難しい。なので、そういうものを検索できるようにするというのがSpookのコンセプトなんです。 諏訪 /さらに言うと、お客様はどのように検索させたいかという意図を持って情報を管理しています。Spookはその工夫された情報管理のもと、条件検索で最適解に導こうとしていますが、キーワードだけでの検索においてはそのキーワードをどこの情報として当てるかも判然としないので 、当てたくない情報を引っ張ってきてしまい希望のものへたどり着けないといったこともあります。 Spookの強みとは何なのか? 独自技術により高速検索に長け、パッケージでも自社内製でもない優位性のあるポジションを確立していることが強みであるSpook。第三弾ではSpookを採用いただいている業界・企業についてや、今後の展望について伺います。お楽しみに!
こんにちは、経営企画室 広報の伊藤です。 7月もあっという間に過ぎ去り、いよいよ8月。2024年4月入社を考える学生の皆さんはそろそろ就職情報サイトへの登録などを行う時期でしょうか。 そんな本日は、フォルシアの採用面接において最もよく聞かれる「Spook(スプーク)って何ですか?」という質問を、採用担当者と広報担当の私とで、ITコンサルとエンジニアの社員に投げかけてみました。 コーポレートサイト でもフォルシアならではのテクノロジーとして紹介しているSpookですが、 そもそもSpookとはいったい何なのか? Spookの強みとは何なのか? どんな企業で導入されていて、今後どんな未来を見据えているのか? といった疑問を、社員の生の声で解説していきます! まずは第一弾、 「そもそもSpookとはいったい何のか?」 についてです。 Spookについて教えてくれた先輩社員プロフィール (右)ITコンサル:DXプラットフォーム部 営業部長 諏訪 (左)エンジニア:旅行プラットフォーム部 グループ長 西山 ITコンサルタント職 DXプラットフォーム部 営業部長 諏訪 俊(すわ・しゅん) /写真右 2017年 キャリア入社 旅行会社での海外渡航手配や法人営業を経て、2017年にフォルシアへ入社 前職での経験を活かし、旅行・観光業向け営業活動に5年間携わり、今年からはDXプラットフォーム部で非旅行業界向け営業・コンサル活動に従事 エンジニア職 旅行プラットフォーム部 グループ長 西山 諒平(にしやま・りょうへい) /写真左 2015年 新卒入社 新卒での入社後 、 大手旅行サイト開発を経て、現在は 旅行・観光業界向け商品販売プラットフォームサービス「webコネクト」 のプロダクト開発に従事 ※所属は2022年7月現在のものとなります それではさっそく聞いていきます! そもそも、Spookとは... フォルシアエンジニアで代々受け継いでいる開発の手法+その手法でつくられた機能群 サイトでは「技術基盤」という記載がありますが、Spookとはいったい何なのでしょうか? 西山 /エンジニア目線で一言で言うと、膨大・複雑なデータを高速に検索させるためのソフトウェアです。Spookを用いることで、顧客のDB(データベース)からデータを取り込み、検索に最適化した形に組み換え、ブラウザ表示を最適化させています。また、バックエンド~フロントエンドまで、高速検索の工夫が詰められています。 諏訪 /営業的には、フォルシア独自の検索エンジンを中心とした機能やノウハウの集合体を指していると捉えています。Spookは膨大・複雑なデータの高速処理に強みがあり、and/orを組み合わせた複雑な検索クエリでも高い検索性を維持できることが特長です。 これまでの様々な企業における検索システムの開発知見が集約されており、ゼロから開発するよりも短い期間で高度なシステムを構築できます。 「ソフトウェア」や「ノウハウの集合体」といった表現が出てきましたが、目に見える実態があるものなのでしょうか? 西山 /エンジニア用語で言うとフレームワークというものがありまして、例えば、「Webアプリケーションを使いたかったらこのお作法に則って作れば簡単にセットアップできるよ」というのをフレームワークと言うのですが、Spookはそれに近いイメージだと思っています。 Spookというお作法(フレームワーク)がフォルシアのエンジニアに提供されていて、それに則ってつくれば、ちゃんと、かつ速く動くシステムを素早く簡単につくれるというようなものです。 レシピ本のようなイメージでしょうか? 西山 /そうですね。レシピ本+包丁もフライパンもだいたい同じものがセットになって提供されているようなイメージです。 先ほどフレームワークと表現しましたが、実際には世の中一般にあるフレームワークよりも少し緩く、手順にまるまる従うか否か、エンジニア側で取捨選択の余地があります。なので、人によってはフレームワークと表現しない場合もあるかと思いますが、わかりやすく説明するとしたらフレームワークと表現して差し支えないように感じています。 「フレームワーク」という言葉はエンジニア用語かと思いますが、営業の現場ではどのように表現されているのでしょうか? 諏訪 /フレームワークというのは枠組みであったり土台だと解釈しているのですが、西山さんの説明にもあった通り、土台というほど固まってはいない、「機能の集合体」のように捉えています。 A社で実装したaaa、B社で実装したbbb...といった機能が大量にライブラリに入っていて、そのライブラリの中から今回自分がつくりたい機能に近しいものを取捨選択して新しいものをつくっているといったイメージです。例えば、今回自分が担当するC社はA社とB社とでいえばB社に近しい仕様でありつつも、一部分はA社に似ているという場合は、B社をベースにA社の要素も取り入れてC社用にカスタムして機能をつくっていく。そうやって生まれたものの集合体でSpookは成り立っていると捉えています。 西山 /Spookを用いての開発は受託開発なので、お客さんごとにアプリケーションがあり、アプリケーションごとの実装が必要なのですが、開発の土台としてSpookを用いることで、だいたいの作り方も、作り方のお作法も同じ。なのでそれをライブラリ化することで横展開しやすくなっているという意味では「機能の集合体」という風にも言えるように思います。 なるほど。Spookという商品(システム)を提供しているのですか?といった声もあったのですが、Spookはフォルシアエンジニアで代々受け継いでいる開発の手法+その手法でつくられた機能群ということですね。 成長し続けるSpook × 自分のアイディア = 新しさの創出 技術職志望の学生にライブラリ化されていることを話すと、それを使う・組み合わせるだけなんですか?自分たちで新しいものを考えてつくりだしていかないんですか?といった声を聞くことも多いのですが、自分たちで新しいものを作っていくわけではないということでしょうか? 諏訪 /「新しいものをつくる」ということには、①ゼロベースでこの世にない新しいものをつくる ②世の中にある便利なものを使って、それに自分のアイディアを加えて新しいものをつくる の二種類があると思うのですが、①ができる人はなかなかいないと思います。なので、先人のノウハウを学び、良いと思った部分を採用し、そこに自分のアイディアを加えていく②がいわゆる「新しいものをつくる」ということだと思っていて、Spookはまさにそのかたまりという風に思っていただければと思います。 西山 /この言語がイケてる、こういうフレームワークがイケてるといったエンジニアのなかでの流行り廃りがあるのですが、そういった部分におけるアップデートはSpookにもどんどん適用させていて、Spook自体はどんどん新しいものへとアップデートされてますし、お客さんへ提供する価値という意味でも、これまでの功績ゆえに新しい相談をいただけて、その相談がSpookのさらなる機能追加、提供価値の向上につながっていったりというケースもあります。 なので、事業会社のように0→1という新しさはないかもしれませんが、1→10であったり、10→100という新しさをつくりだしていく場面はまだまだたくさんありますし、なくなることはないと思っています。 つまり、Spookはいまだ完成されたものではない...ということでしょうか? 西山 /成熟はしていますが、完成はしていません。各エンジニアの工夫が共通ライブラリに還元されていったり、開発効率化のためにライブラリがアップデートされたりしていますが、完成はしないものです。   そもそもSpookとはいったい何なのか? フォルシアエンジニアで代々受け継いでいる開発の手法+その手法でつくられた機能群であり、今後も柔軟に成長していくSpook。今後の成長のさせ方も案件次第、創り手次第だと思うと非常に可能性を秘めているように感じます。 続いて第二弾では「Spookの強みとは何のか?」について伺います。お楽しみに!
タイトル : PostgreSQLで簡単なif文関数を作るなら キーワード: エンジニア、テクノロジー、PostgreSQL こんにちは、エンジニアの羽間です。 フォルシアではDBにPostgreSQLを利用しており、業務でSQLを書く機会がよくあります。 SQLを書く上では ビジネスロジックはできるだけ単体テストを書く SQLの見通しを良くする 同じ処理は共通化する といったことを心がけ、ユーザー定義関数を積極的に作成しています。 商品の検索といった速度を重視する処理においては、処理速度に優れるC言語関数を作成することが多いのですが、 C言語はちょっとした処理を書きたいときに不便で
概要 こんにちは、エンジニアの籏野です。 フォルシアのフロント開発ではReduxを利用して状態管理をしていることが多いです。 その中で、selectorsの書き方について少々気になったことがあったので紹介したいと思います。 背景 現在開発を進めているプロジェクトではRedux周りのディレクトリ構成にre-ducksパターンを採用しています。 ※re-ducksパターンについては詳しい記事がたくさんあるので詳しい説明は省きます。 re-ducksパターンに沿ってコードを書いているとselectorsを定義すると以下のようにしている方が多いのではないでしょうか。 // selecto
初めに こんにちは、エンジニアの籏野です。 フォルシアのアプリ開発ではアプリの状態管理にReduxを用いることが多いです。 Reduxから状態を取得するときに「reselect」という言葉が出てきますが、どのような点が嬉しいのかがいまいちわかっていなかったので調べました。 前準備 さくっとReduxを利用したアプリケーションを用意しましょう。 $ npx create-react-app redux-selector-experience --template redux-typescript $ cd redux-selector-experience $ npm run st
こんにちは。エンジニアの籏野です。 フォルシアでは OpenAPI でAPI定義を書いてから、APIを実装するのが一般的になってきました。 APIの実装についてはTypeScriptとexpressを利用することが増えてきている状況です。 今回はexpressとOpenAPIをより強固に結びつけるためのモジュールとして express-openapi を見つけたので試してみました。 ざっくりまとめ エンドポイントをOpenAPI定義に沿って作成してくれる リクエストパラメータのバリデーションもお手の物 レスポンス項目もチェックしてくれる 成果物 環境 Node.js: 16.13.2 TypeScript: 4.6.2 express: 4.17.3 express-openapi: 10.1.0 準備 npmでプロジェクトを作成し以下のモジュールをインストールします。 linter等はお好みで。 express express-openapi TypeScript @types/express ts-node expressの起動 まずはシンプルにexpressサーバーを起動してみます。 import express, { Request, Response } from "express"; const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.listen(3000, () => { console.log("Start on port 3000"); }); app.get("/user", (req: Request, res: Response) => { res.send({ name: "hatano" }); }); ts-nodeを利用すれば、tsファイルのままで実行できるので便利ですね。 npx ts-node index.ts http://localhost:3000/user にアクセスすることで以下の結果が取得できます。 { name: "hatano" } OpenAPI定義を作成 OpenAPI が何なのかという説明は長くなるので省略します。 定義書の作成については別の記事でも紹介していますので、よければご確認ください。 npmパッケージを組み合わせてSwaggerの定義ファイルをいい感じに書く 今回作成したAPIを定義書に落とすと以下のようになります。 { "openapi" : "3.0.2" , "info" : { "title" : "API定義書" , "description" : "express-openapiを試すための定義書" , "version" : "0.1.0" }, "servers" : [ { "url" : "http://localhost:3000" } ], "paths" : { "/user" : { "get" : { "summary" : "ユーザー取得API" , "description" : "ユーザー情報を取得するAPI" , "operationId" : "getUser" , "parameters" : [], "responses" : { "200" : { "description" : "取得に成功した場合" , "content" : { "application/json" : { "schema" : { "type" : "object" , "required" : [ "name" ], "properties" : { "name" : { "type" : "string" } } } } } } } } } } } express-openapiの利用 ここからが本番です。 express-openapiを利用することでindex.tsは以下のように書き換わります。 import express , { Request , Response , NextFunction } from " express " ; import { initialize } from " express-openapi " ; import path from " path " ; const app = express (); app . use ( express . json ()); app . use ( express . urlencoded ({ extended : true })); app . listen ( 3000 , () => { console . log ( " Start on port 3000 " ); }); initialize ({ app : app , apiDoc : path . resolve ( __dirname , " openapi.json " ), validateApiDoc : true , operations : { getUser : [ function ( req : Request , res : Response , next : NextFunction ) { next (); }, function ( req : Request , res : Response ) { res . send ({ name : " hatano " }); } ] } }); initializeにオプションを与えるだけなので利用はとても簡単ですね。 それぞれのオプションについて解説します。 args.app (required) express-openapiを適用したいexpressアプリケーションを指定します。 args.apiDoc (required) 読み込ませるAPI定義書を指定するためのオプションです。 渡せる値としては以下のいずれかになりますが、基本的には定義書のパスを渡せばよい気がします。 OpenAPI定義のjsonパス fsなどを使ってjsonファイルを読み込んだ文字列 OpenAPI定義に沿ったオブジェクト 読み込んだ定義に何かしらの不備があるとエラーを出してくれるのも便利です。 OpenAPI定義のチェックを行わない場合は args.validateApiDoc をfalseにします。 express-openapi: validation errors [ { "instancePath": "/servers", "schemaPath": "#/properties/servers/uniqueItems", "keyword": "uniqueItems", "params": { "i": 1, "j": 0 }, "message": "must NOT have duplicate items (items ## 0 and 1 are identical)" } ] Error: express-openapi: args.apiDoc was invalid. See the output. args.operations API実装の本体になります。 OpenAPI定義書に記載の operationId をキーとして、APIを実装します。 値の部分には関数もしくは関数の配列を定義できます。 配列にした場合はミドルウェアとして定義され、NextFunctionを実行することで次の処理に伝播していきます。 既にお気づきだと思いますが、operationsを利用するとAPIエンドポイントパスを記載する必要がなく、OpenAPI定義に記載の通りのエンドポイントを勝手に生成してくれます。 リクエストパラメータのバリデーション express-openapiを利用する大きなメリットとして、リクエストパラメータのバリデーションを自動で行ってくれます。 先ほどのAPI定義にパラメータ定義を追加してみましょう。 "parameters": [ { "name": "user", "in": "query", "required": true, "schema": { "type": "string", "enum": ["hatano"] } } ], さらにinitializeに渡すオプションにerrorMiddlewareを追加します。 こちらはAPIアクセス時に何かしらのエラーが発生した時に実行されるミドルウェアになります。 エラーハンドリングが1箇所にまとまるのはいい感じです。 { errorMiddleware : ( err , req : Request , res : Response , _next : NextFunction ) => { res . status ( err . status || 500 ). json ( err ); } } この状態で http://localhost:3000/user にアクセスすると以下のエラーが返ります。 { status: 400, errors: [ { path: "user", errorCode: "required.openapi.requestValidation", message: "must have required property 'user'", location: "query" } ] } 必須のパラメータが付与されていないためのエラーになります。 今回はuserパラメータにenumを指定しているので、user=hogeのような値を指定した場合にもエラーとなります。 これまではリクエストパラメータのバリデーションを社内で独自に実装していましたが、express-openapiに大部分を任せられるようになるのはとてもありがたいです。 レスポンス項目のチェック express-openapiではレスポンス項目のチェックにも対応しています。 以下のように実装することでエラーを取得できます。 リクエスト項目と異なり、エラーがthrowされることはないのでエラー検知時にどのようにハンドリングするかはアプリで検討する必要があります。 // express-openapiがResponseの型を提供していないようなので、エラーを回避するために項目追加 function ( req : Request , res : Response & { validateResponse : any }) { const response = { name : 200 // string型想定のところにnumber型を入れるとエラーになる。 }; const validationError = res . validateResponse ( 200 , response ); if ( validationError ) { throw validationError ; } res . status ( 200 ). send ( response ); } { message: 'The response was not valid.', errors: [ { path: 'name', errorCode: 'type.openapi.responseValidation', message: 'must be string' } ] } OpenAPI定義にpathsを定義しない方法 ここまで試した方法は、すべての定義をOpenAPI定義書に記載するというものでした。 しかしexpress-openapiを利用していると、pathsの定義を記載せずにディレクトリ構成に従ってAPIを自動生成させることも可能です。 以下のようにファイルを作成し、APIを実装します。 paths └── user └── {id}.ts import { Operation } from "express-openapi"; export const GET: Operation = [ (req, res, _next) => { res.status(200).json({ id: req.params.id }); } ]; GET.apiDoc = { summary: "ユーザーID API", description: "ユーザーIDを取得する関数", operationId: "getUserId", parameters: [ { in: "path", name: "id", required: true, schema: { type: "integer" } } ], responses: { 200: { description: "取得に成功した場合", content: { "application/json": { schema: { type: "object", required: ["id"], properties: { id: { type: "integer" } } } } } } } }; また、initializeに渡すオプションに以下を追加します。 { paths : " ./paths " , // 以下はtsファイルでAPIを実装する場合に必要 routesGlob : " **/*.{ts,js} " , routesIndexFileRegExp : / (?: index )? \ .[ tj ] s$ / , } 上記を作成することで、 http://localhost:3000/user/{id} にAPIが作成されます。 ※{id}には任意のintegerが入ります。 operationsでAPIを作成した時と同じくリクエストパラメータのバリデーション、レスポンス項目のチェックを行ってくれます。 pathsもしくはoperationsのどちらかが必須設定項目になっています。 どちらを利用するかはチームの要件に従えばよさそうです。 OpenAPI定義の取得 ここまでpaths/operationsを利用してAPIの実装をしてきました。 最後に作成したOpenAPI定義を Swagger UI で確認してみましょう。 ここまで作成したOpenAPI定義は http://localhost:3000/api-docs にアクセスすることで取得可能です。 関連するオプションについても簡単に記載します。 args.docsPath default: /api-docs API定義を取得するためにアクセスするエンドポイント args.exposeApiDocs default: true APIドキュメントにアクセス可能とするかどうかのフラグ 何かしらの理由でAPI定義を外部公開したくない場合にはfalseにする SwaggerUIについてはDockerで簡単に立ち上げられます。 APIを試しに実行してみるというのがかなり楽にできます。 最後に express-openapiを利用することで、expressとOpenAPI定義がより密接につながることが確認できました。 OpenAPI定義は単なる定義書には収まらないことが今回の記事からもわかると思います。 フォルシアではほかにもAPIの型定義をOpenAPI定義から生成したり、スクリプトの一部を自動作成するようなスクリプトを内製しています。 エンジニアがよりコアなロジック部分の実装に注力できるような環境を今後も整えていきたいと思います。
こんにちは、エンジニアの山門です。 日々仕事をしていると、ちらほら手動で行っている定型作業というものが出てきてしまいます。それを自動化出来れば、これまでその作業に費やしていた時間が他のことに使え、業務効率の向上に繋がります。 今回はその定型作業を解消するためにBotを作ってみた話をしたいと思います。 経緯 あるときPMから1件のコメントがチャンネルに寄せられる 手動で毎回アラートするのが大変 そうだBacklog アラートBotを作成しよう 目的 フォルシアの一部では、プロジェクト管理ツールとして、 Backlog を利用しています。私の参加していたプロジェクトでも利用していたのですが、大規模プロジェクトだったこともあり、大量の課題の起票・更新が行われ、反応が遅れてしまうケースがありました。 これに対して、Backlogを開き、該当チケットを抽出することは出来るのですが、 課題を抽出するために画面を開く 条件を指定して検索する 該当チケットを取得 Slack上に通知 と手間がかかり、これを手動で時間を取って行うのは無駄なコストで且つ、ストレスのかかる作業だと感じました。当時私はチームの1エンジニアではありましたが、今後のプロジェクト進行を考えると、今の段階でBotを作り、こちらから情報を取りに行かずともSlackに通知してくれればHappyになれるのでは?と考え、Bot作成に乗り出しました。 使っている技術 今回技術要素としては以下を利用しました。 API : Baklog API, Slack API HTTP client: axios webApp: TypeScript Notification: GitLab CI API BacklogにはNulab社が提供するAPIがあり、公式の丁寧に書かれた 定義書 があったので、そちらを採用しました。 また、Slackへの通知に関しては、Incoming webhookを使うと手軽なのですが、message形式が比較的柔軟に作れる、スレッドに返信といったことができる、message送るときは今後は chat.postMessage の利用が推奨されている、などの理由で chat.postMessage を選択しています。 (個人的に使ってみたかったから、という理由もあります。) HTTP client 私の参加していたプロジェクトで既に axios を利用しており、勝手がわかっていたため axios を採用しましたが、fetch API とかでもいいと思います。 webApp こちらも同様に、現プロジェクトで採用していたため TypeScript にしました。 Notification 自分のPC上でcronを仕込んで動かすでも良かったのですが、折角なら別のところで動いている方が個人の事情に左右されないので GitLab CI を採用しました。 (フォルシアではGitHubではなく、GitLabを利用しています。) 機能(2022/03/15現在) 予め定義した人たちが担当者になっている、反応が遅れたチケットを通知 期間によって段階を区切って、3パターンにグルーピングしてalert CIのスケジュール実行で1日1回Slackに通知 構成 root ├── README.md ├── node_modules ├── package-lock.json ├── package.json ├── src └── tsconfig.json src以下 ├── definitions │ ├── apiDefinition.ts │ ├── searchConditionDefinition.ts │ └── userDefinition.ts ├── fetchData.ts ├── index.ts ├── sendMessage.ts ├── types │ ├── comments.d.ts │ └── issues.d.ts ├── util │ ├── dateUtil.ts │ └── typeUtil.ts └── view-data ├── comments.ts ├── issues.ts └── sendMessage.ts 構成はざっくり以下の構成になっています。 root: 設定回り src: API リクエスト周りのコード definitions: userIDやAPIのurl定義等 types: 型定義 util: module系 view-data: データ加工処理系 事前準備 Botの利用にあたって、事前にBacklogとSlackで準備が必要です。 Backlog API のAPIキーの発行 プロジェクトの個人設定に進みAPIの項目から新しいAPIキーを登録すれば完了です Slack Botの登録 Slack API のAPIキーを発行するには、Slack Appを作成する必要があります。詳細は割愛しますが、 https://api.slack.com/apps に進みCreate New App Add features and functionality でBotsを選択し、アプリの名前を決める Add features and functionality でPermissionsを選択し、 Scopesを chat:write , users:read にする xoxb- から始まるキー情報が上の方に表示されているはずなので、これを控えておく(これがAPIキーです) Install your app to your workspace でforciaのワークスペースにinstall 通知を入れたいチャンネルに対して、アプリをinviteする 自分はこれを忘れていて少してこずりました ポイント Backlog API Backlog API のメインで使っているAPIは /api/v2/issues です。 updatedSince , updatedUntil を更新日の範囲が絞れるので、これを使って特定の期間内のチケットを取得しています DoS攻撃対策なのか、短時間に数十リクエストを投げるとアクセス制限がかけられてしまうので、リクエスト数は注意して作っています issues の1回のレスポンス上限が100件なので、担当者のチケットが100件以上ある場合はリクエストを分けるなど注意が必要です (イケてないですが、)現状は担当チケットが多い特定の人の場合に個別で条件分岐させています Slackに送るメッセージ Slackが提供しているBlock Kitというものを利用してメッセージを組み立てています 記法に従ってjsonを組み立てて chat.postMessage に投げると、それに応じた形式でslack上に表示してくれます BlockKit Builder を使うとプレビューを見ながら必要な形式を確認できるのでお勧め メッセージは大きく分けて3ブロックで構成しています ヘッダー部分 メンション部分 スレッド部分 基本的には必要なパラメータを用意してAPIを投げればSlackに通知できますが、スレッドに返信するには一工夫必要です。 chat.postMessage でリクエストを投げると、下記のようなレスポンスが返って来るのですが、その中の ts という値がリクエストしたメッセージに紐づいているタイムスタンプです。このタイムスタンプを thread_ts パラメータに指定して再度 chat.postMessage にリクエストすることで、該当のメッセージのスレッドにリクエストすることができます。 ※ID情報等は保護のため仮の値で表現しています。 { ok: true, channel: '<channel名>', ts: '1593849552.024700', message: { bot_id: '<bot_id>', type: 'message', text: 'このコンテンツは表示できません。', user: '<user_id>', ts: '1593849552.024700', team: '<team_id>', bot_profile: { id: '<id>', deleted: false, name: 'backlog-watcher', updated: 1592644323, app_id: '<app_id>', icons: [Object], team_id: '<team_id>' }, blocks: [ [Object] ] } } 実際にスレッドに飛ばすと以下のように期間ごとに分けたメッセージが通知されます。(表示センスはご容赦ください、、 ) slackへのmention mentionを飛ばすためのslackIDの取得には https://api.slack.com/methods/users.list を利用しました users:read の権限があれば、ワークスペース内の人のuser情報を取得できます 純粋に @yamakado などとしてもmentionされないので、IDを持ってくる必要があります 今回はメンバーがある程度固定されているので、実装時にIDを取得してそれを定義で持つようにしています 定期実行 日次で1回実行しているのですが、GitLab CIのschedule機能を使って実行しています API キーなどは基本的にcommitしないほうが良いため、GitLab上のVariablesに登録して、スクリプト実行時に環境変数で渡すようにしています 最後に 日常の不便を見つけて自主的に取り組んだBot作成ですが、個々人に対して通知する仕組みが作れたため、結果的にチーム全体で活用することが出来ています。 実際に導入されてから1年以上が経ちますが、こちらから主体的に情報を取りに行かずとも、コミュニケーションの主軸であるSlackに自動で通知して くれるので、とても快適に感じています。 副次的な効果として、通知で可視化されたことでチームメンバーの目にも留まり、チケットの相談やヘルプがしやすくなった気がします。 これからも日々の定型作業を自動化して、業務効率をどんどんあげていきたいです!
こんにちは、エンジニアの山門です。 日々仕事をしていると、ちらほら手動で行っている定型作業というものが出てきてしまいます。それを自動化出来れば、これまでその作業に費やしていた時間が他のことに使え、業務効率の向上に繋がります。 今回はその定型作業を解消するためにBotを作ってみた話をしたいと思います。 経緯 あるときPMから1件のコメントがチャンネルに寄せられる 手動で毎回アラートするのが大変 そうだBacklog アラートBotを作成しよう 目的 フォルシアの一部では、プロジェクト管理ツールとして、Backlogを利用しています。私の参加していたプロジェクトでも
これは、 FORCIA Advent Calendar 2021 の24日目の記事です。 こんにちは、DXプラットフォーム事業部の小海です。在宅勤務やオンライン会議が一般的になりつつある世の中ですが、皆様いかがお過ごしでしょうか。 フォルシアでもGoogleMeetによるオンライン会議を普段から使用しています。今回はオンライン会議をより楽しいものにするための工夫を紹介します。 はじめに フォルシアでは定期的に社内ハッカソンを実施しており、エンジニアやエンジニア以外の社員も開発したいもの・勉強したいことを持ち寄り定期的に報告をしながら共に学ぶ機会を設けています。 数年前までは参加者で合宿所に行っていましたが、最近の情勢ではオンラインで開催をしています。参加者それぞれ自宅で開発・勉強し、定期的にオンライン集まり報告などをしつつ、最終日に発表会を行っています。 合宿所で行われたハッカソンについては こちらの記事 をご覧ください。 今回は2021年のGWに行われたオンラインハッカソンで私が実施した「GoogleMeetのチャット欄を読み上げてみた」という内容について紹介します。 なお、記事は技術的な内容でソースコードの記載もありますが、設計の考え方や課題解決の仕方に主軸を置いているため、プログラミング苦手な方ももう少しブラウザバックは我慢して頂けますと幸いです。 なぜそのテーマを選んだのか ハッカソンに参加するにあたり「最近不便に感じたことはないか」「最近面白いと思ったことはないか」などを考えていました。 私は技術部の教育チームにも所属しているため、4月からGoogleMeetを使った社内でのオンライン講義の企画・講師・参加をしていました。 その中で参加者がチャット欄に感想や質問を書くことが多々あったのですが、講師が説明に集中しているとき、チャット欄を見ることができなくなったり、講義の内容によってはチャット欄を見ながら講義をすることが難しいということがありました。 また、会議や講義は講師の腕だけじゃなく参加者が能動的に参加することが大事だと思っていたため、オンラインならではの仕組みでより活発で面白い講義にしたいと思っていました。 普段からゲーム配信や実況を楽しんでいた私は、コメント欄の読み上げ機能には馴染みがあり、GoogleMeetのチャット欄を読み上げさせることができたら楽しいのではと思ったのがきっかけです。 使用したツールや言語と選定理由 GoogleMeet 普段からFORCIAでは社内オンライン会議にGoogleMeetを使用しています GoogleChromeの拡張機能 GoogleMeetと読み上げツールを使用するため 棒読みちゃん( Ver0.1.11.0 Beta21 ) 筆者が普段から使用しているツールのため 棒読みちゃん開発者様サイトはこちら 仮想サウンドドライバ 入力や出力をライン指定できる仮想ドライバです 筆者は YAMAHA SYNCROOM に同梱されているドライバを使用しました YAMAHA SYNCROOM 公式サイトはこちら 設計 まず考えるべき壁は3点あります。 GoogleMeetのチャット欄の情報をどのように取得するか 取得したチャット欄の情報をどのように読み上げツールに渡すか 読み上げツールで読み上げた音声をどのようにGoogleMeetで再生させるか GoogleMeetのチャット欄の情報をどのように取得するか これは検討段階から答えを持っており、自作のGoogleChrome拡張を作成することで、特定のWebページを開いているときにWebページの情報をJavaScriptで取得できます。 自作GoogleChromeの拡張のソースコードは後述します。 GoogleMeetのチャット欄の情報をどのように読み上げツールに渡すか ここは非常に悩みました。 基本線で考えていたのは「GoogleChrome拡張から読み上げツールのAPIを呼ぶ」という方式です。ただし、GoogleChrome拡張から読み上げツールのAPIを叩くことの難易度が高く、想定よりも時間がかかってしまいそうと感じていました。 GW期間という限られた時間だったこともあり、あまり時間をかけたくなかったため、もっと楽な方法がないか調べていたところ、読み上げツールには『クリップボード監視』という機能がありました。読み上げツール側で『クリップボード監視』をONにしておくと、クリップボードの変更を検知して内容を自動で読み上げるというものです。 この『クリップボード監視』をONにしておき、GoogleChrome拡張からクリップボードに文字列を保存するようにすれば、読み上げが可能となるので、この方向で進めることとしました。 読み上げツールで読み上げた音声をどのようにGoogleMeetで再生させるか GoogleMeetではGoogleChromeのタブ共有で音声を共有できますが、なるべく画面共有などは使わずに、会議の運用に影響しない形で読み上げさせる方法を模索していました。 最終的には、仮想サウンドドライバを使用し、読み上げツールの出力先を仮想サウンドドライバに設定し、GoogleMeetの入力を仮想サウンドドライバに設定することで連携を可能にしました。 ただし、そのように設定した場合、読み上げツールがマイクを使用してしまうため、同じPCで私がマイクを使用できなくなります。仮想ミキサーなどを使用することで解消できますが、時間短縮のため、私と読み上げツールは別のPCで会議に参加することにしました。 実装 ここまで来れば、後はGoogleChrome拡張を作成して、チャット欄をクリップボードに保存すれば実現ができます。 少々記事が長くなりつつあるので、細かい説明は省きますが興味がある方向けにソースコードを共有します。 ファイル構造 拡張を作成するときのファイル構造は下記のようになります。 今回は gmeetClipborder という名前で作成することにしました。 gmeetClipborder  ├ content  │ ├ content.js  │ └ jquery-3.6.0.min.js  ├ images  │ └ icon.png  ├ js  │ └ background.js  ├ popup  │ ├ jquery-3.6.0.min.js  │ ├ popup.html  │ └ popup.js  └ manifest.json manifest.json GoogleChrome拡張作成の構成ファイルです。 { "name" : "GoogleMeet ClipBoarder" , "version" : "1.0" , "description" : "GoogleMeet ClipBoarder" , "permissions" : [ "declarativeContent" ], "background" : { "scripts" : [ "js/background.js" ], "persistent" : false }, "content_scripts" : [ { "matches" : [ "https://meet.google.com/*" ], "js" : [ "content/jquery-3.6.0.min.js" , "content/content.js" ] } ], "page_action" : { "default_popup" : "popup/popup.html" , "default_icon" : { "32" : "images/icon.png" } }, "icons" : { "48" : "images/icon.png" }, "manifest_version" : 2 } background.js GoogleChrome拡張の本体といえるJavaScriptです。 // 基本的なルール chrome . runtime . onInstalled . addListener (() => { chrome . declarativeContent . onPageChanged . removeRules ( undefined , () => { chrome . declarativeContent . onPageChanged . addRules ([{ conditions : [ new chrome . declarativeContent . PageStateMatcher ({ pageUrl : { hostEquals : ' meet.google.com ' }, }) ], actions : [ new chrome . declarativeContent . ShowPageAction ()] }]); }); }); // 有効化・無効化のStatus管理 const Enable = (() => { let isEnabled = false ; const that = this ; this . get = () => { return isEnabled ; }; this . set = ( bool ) => { isEnabled = Boolean ( bool ); return that ; // for method chaining }; this . toggle = () => { isEnabled = ! isEnabled ; return that ; // for method chaining }; return this ; })(); // popupやcontentの世界との連携 const CommandHandler = { GetEnabled : () => Enable . get (), ToggleEnabled : () => Enable . toggle (). get () }; chrome . runtime . onMessage . addListener ( ( request , _sender , sendResponse ) => sendResponse ( CommandHandler [ request . cmd ]()) ); popup.html 拡張アイコンのクリック時に表示されるHTMLです。 <!DOCTYPE html> <html> <head> <style> button { height : 30px ; width : 30px ; outline : none ; font-weight : bold ; } button .is-active { background-color : red ; color : white ; } </style> </head> <body> <button id= "toggleButton" ></button> <script src= "jquery-3.6.0.min.js" ></script> <script src= "popup.js" ></script> </body> </html> popup.js 上記popup.htmlで読み込まれるJavaScriptです。 (() => { // static const SELECTOR_TOGGLE_BUTTON = " #toggleButton " ; const CLASS_ACTIVE = " is-active " ; // functions const renderStatus = ( isEnabled , $button ) => { if ( isEnabled ){ $button . addClass ( CLASS_ACTIVE ); $button . html ( " on " ); } else { $button . removeClass ( CLASS_ACTIVE ); $button . html ( " off " ); } }; // on loaded $ (() => { const $button = $ ( SELECTOR_TOGGLE_BUTTON ); // initial setting chrome . runtime . sendMessage ( { cmd : " GetEnabled " }, ( isEnabled ) => renderStatus ( isEnabled , $button ) ); // attach event $ ( " #toggleButton " ). on ( " click " , () => { chrome . runtime . sendMessage ( { cmd : " ToggleEnabled " }, ( isEnabled ) => renderStatus ( isEnabled , $button ) ); }); }); })(); content.js 拡張によってページ内で読み込まれるクライアント側のJavaScriptです。 ' use strict ' ; (() => { // static const CLASS_READED = " gmc-readed " ; const SELECTOR_ALL_MESSAGES = " .GDhqjd div div " ; const SELECTOR_NEW_MESSAGES = SELECTOR_ALL_MESSAGES + " :not(. " + CLASS_READED + " ) " ; const MAX_TEXTS_AT_ONCE = 5 ; // on loaded $ (() => { initialize (); setInterval ( MainLoop , 1000 ); }); // initialize const initialize = () => { const $elm = $ ( SELECTOR_ALL_MESSAGES ); $elm . addClass ( CLASS_READED ); }; // get new text from HTML const getMessage = () => { const $elm = $ ( SELECTOR_NEW_MESSAGES ); $elm . addClass ( CLASS_READED ); const messages = []; $elm . each (( _i , e ) => { messages . push ( $ ( e ). data ( " message-text " )); }); return messages ; } // save text to clipboard const saveClipboard = ( str ) => { if ( ! str ) return ; const listener = ( e ) => { e . clipboardData . setData ( " text/plain " , str ); e . preventDefault (); document . removeEventListener ( " copy " , listener ); } document . addEventListener ( " copy " , listener ); document . execCommand ( " copy " ); } // main const MainLoop = (() => { // closure let prevEnabled = false ; const callback = ( isEnabled ) => { if ( isEnabled ) { if ( ! prevEnabled ){ initialize (); } else { const messages = getMessage (); if ( messages && messages . length > 0 && messages . length <= MAX_TEXTS_AT_ONCE ){ saveClipboard ( messages . join ( " \n " )); } } } prevEnabled = isEnabled ; }; return () => { chrome . runtime . sendMessage ({ cmd : " GetEnabled " }, callback ); }; })(); })(); GoogleChromeに自作の拡張機能を読み込ませる 下記の手順で、作成した拡張機能を読み込ませることができます。 chrome://extensions/ にアクセス デベロッパーモードをONにする 『パッケージ化されていない拡張を読み込む』を選択 開発したフォルダ「gmeetClipborder」を選択 これで、GoogleMeetのチャット欄の読み上げができるようになりました(拍手) おわりに 今回はオンライン会議を面白くするアイデアを技術で実現した例をお話しさせていただきました。いかがだったでしょうか。ブラウザバックを我慢して読み進めて頂いたプログラミングが苦手な方、ここまで読んで頂いてありがとうございます。おそらく後半は存分にスクロールして頂いたかと思います。 日常でふと思いついたアイデアを形にすることは、とても楽しいしワクワクします。今回作成したGoogleMeetの読み上げ機能はハッカソンでの発表会でもとても好評で、参加者みんな読み上げツールに好きな言葉を読み上げさせて楽しんでくれていました。チャット欄で大喜利が発生し、読み上げツールの発言に皆さん気を取られ、私の発表はあまり聞いてもらえなかったのが良い思い出です。アイデアって難しいですね。オンライン飲み会専用ツールとなりそうです。 この記事の内容に興味をもった方は是非弊社採用ページもご確認ください!
こんにちは、DXプラットフォーム事業部の小海です。在宅勤務やオンライン会議が一般的になりつつある世の中ですが、皆様いかがお過ごしでしょうか。 フォルシアでもGoogleMeetによるオンライン会議を普段から使用しています。今回はオンライン会議をより楽しいものにするための工夫を紹介します。 はじめに フォルシアでは定期的に社内ハッカソンを実施しており、エンジニアやエンジニア以外の社員も開発したいもの・勉強したいことを持ち寄り定期的に報告をしながら共に学ぶ機会を設けています。 数年前までは参加者で合宿所に行っていましたが、最近の情勢ではオンラインで開催をしています。参加者それぞれ自宅で開
これは、 FORCIA Advent Calendar 2021 の22日目の記事です。 こんにちは。アドベントカレンダー22日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務でJavaScriptでプログラムを書くことが多いですが、その際によく使用するJavaScriptのライブラリにUnderscore.jsがあります。 なぜUnderscore.jsを多用するのかというと、フォルシアで開発している主な機能の1つに検索機能があることが関係しています。 検索機能では、データベースに対してSQLを実行し、その結果をHTMLのテンプレートに当てはめていく、ということをよく行います。 SQLの実行結果は、構造化されていないデータの配列であることが多いため、HTMLテンプレートに適用しやすくするためにデータを組み替えて親子関係を持たせたり、複数のSQL実行結果を組み合わせたりします。 その際のデータ処理にUnderscore.jsを使うことが多いのです。 (ただ、最近のブラウザなどのJavaScript実行環境では、Underscore.jsの様々な関数がネイティブでサポートされているので、Underscore.jsを使用する範囲は徐々に少なくなりそうです) Underscore.jsの特徴として、関数型プログラミングのような書き方ができる、という点があります。 関数型プログラミングでは、ある関数に値を渡して得られた値を別の関数に渡す、ということを繰り返していく書き方をしますが、そうした処理を行うための関数型言語にあるような便利な関数がUnderscore.jsにはたくさん用意されています。 例えば以下のような関数があります。 map: 配列(※)と関数を渡して、配列の各要素に対して関数を適用した配列を返す filter: 配列と真偽値を返す関数を渡して、配列の各要素の内、関数に適用した結果trueになるものの配列を返す reduce(foldl): 配列と関数と初期値を渡して、以下の順に処理を繰り返し、最終的に得られた値を返す(「折り畳み」と言うこともあります) 初期値と配列の最初の要素を関数に渡して得られた値 上記の値と配列の次の要素を関数に渡して得られた値 上記の値と配列の次の要素を関数に渡して得られた値 ... 上記の値と配列の最後の要素を関数に渡して得られた値 chain: 上記のmapやfilter等、Underscore.jsがサポートしている関数を複数組み合わせて適用した結果を返す ※配列ではなく連想配列オブジェクトを渡すこともできます。 Underscore.jsを使うと関数型プログラミングのような書き方ができるとして、Underscore.jsの内部ではどのように関数が定義されているのでしょうか? それが気になって、今回Underscore.jsのソースコードを読んでみることにしました。 それでは早速見ていきましょう! なお、Underscore.jsのバージョンは1.3.1を使用し、ソースコードはUMDのDevelopment版( GitHubリポジトリ上のソースコードはこちら )を使用しています。 map関数 まず、自分がよく使うmap関数を見ていきたいと思います! ソースコード には以下のように定義されています。 // Return the results of applying the iteratee to each element. function map(obj, iteratee, context) { iteratee = cb(iteratee, context); var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; } ここでmap関数が受け取る引数は、それぞれ以下のような意味になっています。 obj: 関数を適用する配列(または連想配列オブジェクト) iteratee: obj の各要素に適用する関数 context: this の値を置き換えたい場合に渡す(自分はあまり使いません) 最初に iteratee = cb(iteratee, context); と書かれていて、 cb というWrapper関数を通していますが、基本的には渡した iteratee がそのまま返ります。 _keys には、引数の obj が連想配列オブジェクトの場合に、キーの配列が入りますが、通常の配列の場合は値が設定されません。 var currentKey = _keys ? _keys[index] : index; のところで、 _keys に値が設定されているかどうかで、 _keys の要素をキーに使うのか、配列のインデックスをキーに使うのかが分かれています。 引数の obj が、配列と連想配列オブジェクトのどちらでも受け取れるようになっているのはこの工夫があるからですね! あとは、 _keys または配列の要素数だけループを回して、一時変数として定義されている配列 results に関数を適用して得られた値を順番に格納していき、最後にその results を返す形になっています。かなりシンプルな定義ですね! reduce関数 次にreduce関数を見てみましょう! ソースコード には以下のように定義されています。 // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. var reduce = createReduce(1); おっと、、実際の中身はcreateReduce関数に定義されているようなので、そちらを見ていきます。 // Internal helper to create a reducing function, iterating left or right. function createReduce(dir) { // Wrap code that reassigns argument variables in a separate function than // the one that accesses `arguments.length` to avoid a perf hit. (#1991) var reducer = function(obj, iteratee, memo, initial) { var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length, index = dir > 0 ? 0 : length - 1; if (!initial) { memo = obj[_keys ? _keys[index] : index]; index += dir; } for (; index >= 0 && index < length; index += dir) { var currentKey = _keys ? _keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; }; return function(obj, iteratee, memo, context) { var initial = arguments.length >= 3; return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); }; } ここでcreateReduce関数は以下の関数を返しているので、reduce関数が受け取る引数は obj 、 iteratee 、 memo 、 context の4つであることが分かります。 return function(obj, iteratee, memo, context) { var initial = arguments.length >= 3; return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); }; 引数の obj 、 iteratee 、 context は先ほどのmap関数と同じですが、新しく出てきた引数 memo で初期値を受け取っています。 memo を渡せば、 initial は true になり、 context を渡さなければ optimizeCb(iteratee, context , 4) は iteratee をそのまま返すので、 この場合、createReduce関数内で定義されているreducer関数は以下と同等になります。(ここで、 createReduce(1) で dir の値には 1 を渡しているので、 index の初期値は 0 と定義できます) var reducer = function(obj, iteratee, memo) { var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; }; 先ほどのmap関数と似ていますね! 違いはと言えば、 results の部分が memo に置き換わっている点です。 map関数の場合は results に関数を適用した結果を格納していきますが、reduce関数の場合は、要素を順番に辿りながら、 memo とその要素を iteratee 関数に渡して得られた結果を memo に再代入し、最後にその memo を返す形になっています。 こちらもシンプルな定義ですね! また、 dir を受け取るcreateReduce関数を定義することで、要素を最後から前に向かって逆向きに折り畳む、reduceRight関数も以下のように簡単に定義できています。 // The right-associative version of reduce, also known as `foldr`. var reduceRight = createReduce(-1); リファクタリングの観点からも、素晴らしい抽象化ですね! chain関数 最後に、Underscore.jsの関数を複数組み合わせて適用できる、chain関数を見てみたいと思います! ソースコード には以下のように定義されています。 // Start chaining a wrapped Underscore object. function chain(obj) { var instance = _$1(obj); instance._chain = true; return instance; } おや、、chain関数は _$1(obj) というインスタンスを取得し、インタンス変数 _chain に true を設定して返しているようです。 _$1 という謎めいたクラスが出てきました。これは何でしょう? ソースコードを見ると以下のように定義されています。 // If Underscore is called as a function, it returns a wrapped object that can // be used OO-style. This wrapper holds altered versions of all functions added // through `_.mixin`. Wrapped objects may be chained. function _$1(obj) { if (obj instanceof _$1) return obj; if (!(this instanceof _$1)) return new _$1(obj); this._wrapped = obj; } 引数の obj が _$1 クラスのインスタンスであることはあまり無いと思うので、以下の2行を見ればよさそうです。 if (!(this instanceof _$1)) return new _$1(obj); this._wrapped = obj; ここで、 this は new キーワードを付けて _$1 関数が呼ばれた場合に _$1 クラスのインスタンスになるので、逆に new を付けずに呼ばれた場合は this instanceof _$1 は false になります。 すると new を付けずに呼ばれた場合は !(this instanceof _$1) は true になり、 new を付けて再度呼ばれます。 そのため、 new を付けても付けなくても _$1 クラスのインスタンスが生成され、メンバ変数 _wrapped に引数の obj が格納されて返されます。 そうするとおそらく、Underscore.jsの関数を複数適用した場合に、それぞれの関数が _wrapped に値が入っているかどうかを見ているのではないか、と推測できますね! _wrapped でソースコードを探していくと、mixin関数の定義で以下のように書かれています。 // Add your own custom functions to the Underscore object. function mixin(obj) { each(functions(obj), function(name) { var func = _$1[name] = obj[name]; _$1.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); return chainResult(this, func.apply(_$1, args)); }; }); return _$1; } そして mixin関数はソースコードの最後の方で以下のように呼ばれています。 var _ = mixin(allExports); まさにここのようです! allExports にはUnderscore.jsで使える全ての関数が入っているので、 これら全ての関数を _$1 オブジェクトのプロパティに設定しつつprototypeプロパティにも設定しています。 そのため、 _$1 クラスのインスタンスとして関数が実行された場合は、そのインスタンスと _wrapped の値を関数に適用した結果を、chainResult関数に渡していることが分かります。 そしてchainResult関数は以下のように定義されています。 // Helper function to continue chaining intermediate results. function chainResult(instance, obj) { return instance._chain ? _$1(obj).chain() : obj; } ここで少し紛らわしいのが、 _$1(obj).chain() のchain関数は _.chain() とは異なる、というところです。 _$1(obj) で _$1 クラスのインスタンスが生成されてメンバ変数 _wrapped に obj が入り、 _$1(obj).chain() は、 chainResult(this, chain.apply(_$1, [obj]) を返す形になります。 ここで、 _$1 クラスのインスタンスが再生成されているので、メンバ変数 _chain の値は何も設定されていない状態( undefined )になっています。 そのため、 chainResult(this, chain.apply(_$1, [obj]) は chain.apply(_$1, [obj]) を返し、それは _$1 クラスの新しいインスタンスで、そのメンバ変数 _wrapped には obj が、 _chain には true が設定された状態になっています。 ...と、少々ややこしくなってきたので、 以上をふまえつつ、以下のコードを実行した場合の動作を見てみましょう! _.chain([1, 2, 3]) .map(function(num) { return (num * 2); }); _.chain([1, 2, 3]) が実行される _$1 クラスのインスタンス _a が生成される ※分かりやすくするため、インスタンス _a 、インスタンス _b ... と順番に名前を付けていきます! _a のメンバ変数 _wrapped に [1, 2, 3] が入る _a のメンバ変数 _chain に true が入る _a が返る _a のメンバ関数のmap関数が実行される chainResult(this, map.apply(_$1, [[1, 2, 3], function(num) { ... }])) が実行される。 ここで、 map.apply(_$1, [[1, 2, 3], function(num) { ... }]) は [2, 4, 6] になるので、 chainResult(this, [2, 4, 6]) が実行される this._chain は上記で true が入っているので、 _$1([2, 4, 6]).chain() が実行され、 まず _$1([2, 4, 6]) が実行される _$1 クラスのインスタンス _b が生成される _b のメンバ変数 _wrapped に [2, 4, 6] が入る _b が返る _b のメンバ関数のchain関数が実行される chainResult(this, chain.apply(_$1, [2, 4, 6])) が実行される。 ここで、 chain.apply(_$1, [2, 4, 6]) は _$1 クラスの新しいインスタンス _c を生成して返し、 _c のメンバ変数 _wrapped には [2, 4, 6] が、メンバ変数 _chain には true が入っている chainResult(this, chain.apply(_$1, [2, 4, 6])) の this._chain は設定されていない( undefined になっている)ので、そのまま _c が返る 最後の _c は _$1 クラスのインスタンスになっており、 メンバ変数 _wrapped には [2, 4, 6] が、 _chain には true が入った状態になっています。 _c は _$1 クラスのインスタンスなので、Underscore.jsで使える全ての関数を続けて実行できるようになっています。 なるほど!! chain関数を使って、Underscore.jsの関数を複数実行した結果は、最後にvalue関数を実行すると取得できますが、 value関数は以下のように定義されていて、メンバ変数 _wrapped の値を返すだけになっています。 // Extracts the result from a wrapped and chained object. _$1.prototype.value = function() { return this._wrapped; }; シンプルですね!! さいごに 普段の業務で、既にある機能を改修する際にまずドキュメントを読みますが、モジュールの役割や依存関係を正確に理解・把握するために、実際に書かれているコードを読むことが多いです。 その中で、「こんな書き方があるのか」と勉強になり、自分でもそれを使うようになることがたくさんあります。 今回、Underscore.jsのソースコードを読んでみて、今まで使ったことがないような書き方を知ることができて、とても面白かったです! (例えば func.length でfunc関数が受け取る引数の数が分かったり、 push.apply(arrayA, arrayB)) で配列 arrayA に配列 arrayB の要素を追加できる、など) 皆さんも有名なライブラリのソースコードを読んでみて、新しい書き方を見つけてみてはいかがでしょうか :)
こんにちは。アドベントカレンダー22日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務でJavaScriptでプログラムを書くことが多いですが、その際によく使用するJavaScriptのライブラリにUnderscore.jsがあります。 なぜUnderscore.jsを多用するのかというと、フォルシアで開発している主な機能の1つに検索機能があることが関係しています。 検索機能では、データベースに対してSQLを実行し、その結果をHTMLのテンプレートに当てはめていく、ということをよく行います。 SQLの実行結果は、構造化されていないデータの配列であることが多いため、HTML
これは、 FORCIA Advent Calendar 2021 の21日目の記事です。 こんにちは。第二旅行プラットフォーム部エンジニアの浦上です。アドベントカレンダーの枠を取ってみたはいいものの特にネタが思いつかずフォルシアの過去のアドベントカレンダーを遡っていたところこのような記事を見つけました。 プログラミング言語ではなく、フォルシアの高速検索の鍵を握るSQLで数独を解く なるほど、アドベントカレンダーというのはCやPythonのような『普通の』プログラミング言語以外で数独を解けばよいのですね。 ということでこの記事ではTypeScriptの『型』だけを用いて数独を解いていこうと思います。 盤面の表し方 まずは、数独の問題を表現する型を作りましょう。 扱いやすいように、以下のルールで扱うことにしました。 埋まっているマスは 1 から 9 の数値リテラル型で表す。 空きマスは数値リテラル型 0 で表す。 盤面全体は、左上から順に長さ81のタプル型で表す。 以上をコードで表すと下記のようになります。 Problem1 も変数ではなく「初項から順に数値リテラル 0 , 2 , 4 , ......である長さ81のタプル型」であることにご注意ください。 // 使用可能な数字は1から9まで type Digit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ; // 空きマスは0で表す type Empty = 0 ; // 長さ9のタプル型 type Tuple9 < T > = [ T , T , T , T , T , T , T , T , T ]; // 長さ81のタプル型 type Tuple81 < T > = [ ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > ]; // 数独の問題を表す型 type Problem = Tuple81 < Digit | Empty > ; // 問題例 type Problem1 = [ 0 , 2 , 4 , 0 , 8 , 7 , 5 , 0 , 9 , 8 , 9 , 1 , 4 , 5 , 6 , 3 , 7 , 2 , 5 , 6 , 7 , 0 , 9 , 3 , 0 , 0 , 1 , 7 , 8 , 6 , 5 , 2 , 9 , 0 , 3 , 4 , 2 , 0 , 0 , 3 , 1 , 0 , 0 , 0 , 0 , 1 , 4 , 0 , 6 , 7 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 6 , 0 , 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 9 , 0 , 0 , 0 , 0 , 5 , 0 , 0 , 0 , ]; 準備 数独を解くために必要となる型をいくつか準備しておきましょう。 // タプルのindexを行番号に変換するための型 type ToRow = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 ]; type PopValue < T extends Digit [], K extends number , V extends Digit | Empty > = { [ key in keyof T ]: key extends ` ${ K } ` ? Exclude < T [ key ], V > : T [ key ]; }; type RowLimit < Board extends Problem , Ret extends Tuple9 < Digit > = Tuple9 < Digit > , I extends unknown [] = [] > = I [ " length " ] extends 81 ? Ret : RowLimit < Board , PopValue < Ret , ToRow [ I [ " length " ]], Board [ I [ " length " ]] > , [... I , unknown ] > ; // 以下は使用例 type Ex1 = PopValue < [ 1 | 2 | 3 , 3 ], 0 , 2 > // -> [1 | 3, 3]; type Ex2 = PopValue < [ 1 | 2 | 3 , 3 ], 1 , 3 > // -> [1 | 2 | 3, never]; type RowLimitProblem1 = RowLimit < Problem1 > ; /* -> [ 1 | 3 | 6, never, 2 | 4 | 8, 1, 9 | 4 | 5 | 6 | 7 | 8, 9 | 2 | 3 | 5, 9 | 1 | 2 | 3 | 4 | 5 | 7 | 8, 9 | 2 | 4 | 5 | 6 | 7 | 8, 1 | 2 | 3 | 4 | 6 | 7 | 8 ] */ PopValue はタプル型の第 K 項から型 V を削除するための型です。使用例は Ex1 、 Ex2 をご覧ください。 RowLimit は「0〜8行目(TypeScriptの配列は0から数えるのでそれに合わせて1〜9行目でなく0〜8行目と表しています)のそれぞれについて、まだ使われていない数字はどれか」というのを表現する型です。上記の例だと、1番上の行には1,3,6がまだ使われていないこと、その下の行はもう完成していて何も使えないこと、などを表しています。 これがどのように実現されているかというと Ret の初期値を Tuple9<Digit> 、つまりすべての行に1から9すべてが使える状態にしておく 左上のマスから順に、 Ret の「そのマスの行番号」から「そのマスに書かれている数字」を削除する 右下のマスまで見終わったら終了 ということをしています。タプル型 I の長さを1ずつ増やしながら再帰することで型でもfor文のようなことを実現できます。 「行」だけでなく「列」「グループ(3×3の正方形)」の条件もつくるために、 ToColumn ・ ToSquare 、 ColumnLimit ・ SquareLimit も同様に定義しておきます。 // タプルのindexを列番号に変換するための型 type ToColumn = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ]; // タプルのindexを3×3のグループの番号に変換するための型 type ToSquare = [ 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 , 4 , 5 , 5 , 5 , 3 , 3 , 3 , 4 , 4 , 4 , 5 , 5 , 5 , 3 , 3 , 3 , 4 , 4 , 4 , 5 , 5 , 5 , 6 , 6 , 6 , 7 , 7 , 7 , 8 , 8 , 8 , 6 , 6 , 6 , 7 , 7 , 7 , 8 , 8 , 8 , 6 , 6 , 6 , 7 , 7 , 7 , 8 , 8 , 8 ]; type ColumnLimit < Board extends Problem , Ret extends Tuple9 < Digit > = Tuple9 < Digit > , I extends unknown [] = [] > = I [ " length " ] extends 81 ? Ret : ColumnLimit < Board , PopValue < Ret , ToColumn [ I [ " length " ]], Board [ I [ " length " ]] > , [... I , unknown ] > ; type SquareLimit < Board extends Problem , Ret extends Tuple9 < Digit > = Tuple9 < Digit > , I extends unknown [] = [] > = I [ " length " ] extends 81 ? Ret : SquareLimit < Board , PopValue < Ret , ToSquare [ I [ " length " ]], Board [ I [ " length " ]] > , [... I , unknown ] > ; なお、このコードはTypeScript4.4以前だと再帰回数の上限を超えてしまいエラーとなってしまいます。TypeScript4.5で型レベルでの末尾再帰最適化が行われるようになったので動くようになりました。この機能が追加されていなければ今回の記事は実現しなかったでしょう。 数独を解く それではいよいよ数独を解いていきましょう。結論から書いてしまうと、以下の型で数独を解くことができます。 type Solve < Board extends Problem , Row extends Tuple9 < Digit > = RowLimit < Board > , Column extends Tuple9 < Digit > = ColumnLimit < Board > , Square extends Tuple9 < Digit > = SquareLimit < Board > , Answer extends Digit [] = [], L extends number = Answer [ " length " ], T = Board [ L ] extends Empty ? Row [ ToRow [ L ]] & Column [ ToColumn [ L ]] & Square [ ToSquare [ L ]] : Board [ L ] > = L extends 81 ? Answer : T extends Digit ? Solve < Board , PopValue < Row , ToRow [ L ], T > , PopValue < Column , ToColumn [ L ], T > , PopValue < Square , ToSquare [ L ], T > , [... Answer , T ] > : never ; 上のコードは「左上のマスから順番に『そのマスに使える数字を1つ選んで埋めて、次のマスに進む』を繰り返す。最後(右下)のマスまで詰まずに埋めることができたらそれが答え」という方針を実現するコードです。 まずは各型変数の説明です。 Board :問題の盤面を表す Row :(現時点で)各行でまだ使われていない数字はどれかを表す長さ9のタプル型。 Column 、 Square : Row の「行」を「列」、「グループ」に置き換えたもの。 Answer :左上から順に埋めた数字を記録していくタプル型。これの長さが81まで達するとそのときの Answer が解。 L : Answer の長さは色々なところで使うので見やすさのために新たに文字で置いたもの。「ここまでいくつ埋めたか」を表すと同時に、「今から埋めようとしているマスのindex」も表している。 T :今から埋めようとしているマス(= L 番目のマス)で使うことのできる数字を表す型。 Board の L マス目に応じて以下のように決まる。 問題で埋まっているマスならその数字しか使えないので Board[L] 空きマスなら行、列、グループの条件をすべて守る必要があるので Row[ToRow[L]] 、 Column[ToColumn[L]] 、 Square[ToSquare[L]] の交差型。ここで、例えば Row[ToRow[L]] は「 L 番目のマス目の属している行で使える数字」を表すことに注意。 T extends Digit ? ... によって T にunion distributionが適用される(union distributionについては こちらの記事 の解説がとても分かりやすいです)ので、例えば T が 1|3 であった場合、 Solve < Board , PopValue < Row , ToRow [ L ], 1 > , PopValue < Column , ToColumn [ L ], 1 > , PopValue < Square , ToSquare [ L ], 1 > , [... Answer , 1 ] > | Solve < Board , PopValue < Row , ToRow [ L ], 3 > , PopValue < Column , ToColumn [ L ], 3 > , PopValue < Square , ToSquare [ L ], 3 > , [... Answer , 3 ] > と展開されます。要は Answer に埋めようとしている数字を追加 L 番目のマスが所属する行、列、グループのそれぞれから L 番目のマスに使った数字を削除 した状態で次の再帰に進みます。 途中で T がnever型になってしまった、つまり使える数字が1つもなくなってしまった場合にはその分岐はneverを返却して終了するので、無事81マスすべて埋めることができたものだけが最終的な答えとなるわけです。 実際に問題を解いてみる 早速何問か解いてみましょう。 type Answer1 = Solve < Problem1 > ; /* -> [ 3, 2, 4, 1, 8, 7, 5, 6, 9, 8, 9, 1, 4, 5, 6, 3, 7, 2, 5, 6, 7, 2, 9, 3, 8, 4, 1, 7, 8, 6, 5, 2, 9, 1, 3, 4, 2, 5, 9, 3, 1, 4, 7, 8, 6, 1, 4, 3, 6, 7, 8, 2, 9, 5, 4, 7, 2, 9, 3, 1, 6, 5, 8, 6, 3, 5, 8, 4, 2, 9, 1, 7, 9, 1, 8, 7, 6, 5, 4, 2, 3 ] */ type Problem2 = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0 , 0 , 6 , 0 , 9 , 1 , 5 , 2 , 0 , 0 , 0 , 0 , 0 , 0 , 2 , 3 , 0 , 4 , 8 , 0 , 0 , 0 , 0 , 4 , 1 , 8 , 0 , 0 , 6 , 5 , 0 , 0 , 7 , 0 , 9 , 3 , 2 , 5 , 0 , 0 , 0 , 8 , 0 , 6 , 0 , 9 , 3 , 4 , 1 , 0 , 9 , 2 , 1 , 5 , 0 , 0 , 3 , 0 , 0 , 5 , 0 , 0 , 0 , 1 , 8 , 2 , 9 , 6 ]; type Answer2 = Solve < Problem2 > ; /* -> [ 6, 9, 4, 8, 5, 7, 1, 2, 3, 3, 8, 7, 1, 4, 2, 6, 5, 9, 1, 5, 2, 6, 3, 9, 7, 8, 4, 2, 3, 5, 4, 8, 1, 9, 6, 7, 4, 1, 8, 9, 7, 6, 5, 3, 2, 7, 6, 9, 3, 2, 5, 8, 4, 1, 8, 7, 6, 2, 9, 3, 4, 1, 5, 9, 2, 1, 5, 6, 4, 3, 7, 8, 5, 4, 3, 7, 1, 8, 2, 9, 6 ] | [ 6, 9, 7, 8, 4, 2, 1, 5, 3, 3, 8, 4, 1, 5, 7, 6, 2, 9, 1, 5, 2, 6, 3, 9, 7, 8, 4, 2, 3, 5, 4, 8, 1, 9, 6, 7, 4, 1, 8, 9, 7, 6, 5, 3, 2, 7, 6, 9, 3, 2, 5, 8, 4, 1, 8, 7, 6, 2, 9, 3, 4, 1, 5, 9, 2, 1, 5, 6, 4, 3, 7, 8, 5, 4, 3, 7, 1, 8, 2, 9, 6 ] */ type Problem3 = [ 0 , 0 , 0 , 3 , 0 , 0 , 0 , 1 , 9 , 6 , 7 , 0 , 0 , 0 , 4 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 0 , 2 , 0 , 3 , 4 , 0 , 0 , 7 , 0 , 2 , 0 , 0 , 0 , 0 , 6 , 0 , 3 , 1 , 5 , 0 , 0 , 0 , 0 , 0 , 0 , 6 , 0 , 0 , 9 , 0 , 1 , 3 , 0 , 6 , 0 , 0 , 0 , 0 , 2 , 0 , 0 , 4 , 7 , 0 , 0 , 6 , 0 , 0 , 2 , 0 , 9 , 0 , 4 , 0 , 0 , 0 , 0 ]; type Answer3 = Solve < Problem3 > ; /* -> [ 4, 5, 8, 3, 2, 6, 7, 1, 9, 6, 7, 2, 9, 1, 4, 3, 8, 5, 9, 1, 3, 8, 5, 7, 4, 2, 6, 3, 4, 1, 5, 7, 9, 2, 6, 8, 8, 9, 6, 2, 3, 1, 5, 7, 4, 7, 2, 5, 4, 6, 8, 1, 9, 3, 1, 3, 7, 6, 8, 5, 9, 4, 2, 5, 8, 4, 7, 9, 2, 6, 3, 1, 2, 6, 9, 1, 4, 3, 8, 5, 7 ] */ 無事数独を解くことができましたね! Problem2 のように答えが複数ある場合は解すべてのunion型が得られます。 また、 Problem3 のようなヒントの少ない難しい問題でも解くことができました。 最後に 今回使用したコードは こちら で試すことができます。
これは、FORCIA Advent Calendar 2021の21日目の記事です こんにちは。第二旅行プラットフォーム部エンジニアの浦上です。アドベントカレンダーの枠を取ってみたはいいものの特にネタが思いつかずフォルシアの過去のアドベントカレンダーを遡っていたところこのような記事を見つけました。 プログラミング言語ではなく、フォルシアの高速検索の鍵を握るSQLで数独を解く なるほど、アドベントカレンダーというのはCやPythonのような『普通の』プログラミング言語以外で数独を解けばよいのですね。 ということでこの記事ではTypeScriptの『型』だけを用いて数独を解いていこうと思