新卒がベンチャーでの1年間を振り返って

フロントエンドエンジニアの氏家です。私が新卒としてvivitに入社してから早くも1年が経過しました。

入社してすぐのとき、アウトプットとしてこのエンジニアブログを頻繁に更新していたのが懐かしいですが、今回はそんな私がvivitで1年間働いて、具体的にどんなことをやってきたか四半期ごとに振り返りたいと思います。

4~6月 (チームローテーション期間)

新卒エンジニアとして入社してからまず最初にやったことは、MAU350万を超えるアウトドアメディア「hinata」の開発です。入社前のインターンでもお世話になったmediaチームで、フロントエンド開発に携わりました。

チーム開発による業務に慣れておらず、初歩的なミスや拙いコミュニケーションで迷惑をかけていたと思いますが、いつも優しく対応してくださいました。先輩エンジニアから知識や技術力だけでなく、こういった業務態度も見習っていきたいと思いました。

5月からはキャンプ用品のレンタルサービスを提供している「hinataレンタル」のチームにお邪魔し、念願の React + TypeScript によるフロントエンドの開発に携わりました。

独学での個人開発経験があったとはいえ、業務では分からないことだらけでしたが、都度教えていただきながら業務をこなすことができました。

6月からは主にインフラやプロダクトをまたいだ業務を行う「事業横断チーム」にお邪魔し、各チームの開発環境の整備やサーバ監視ツールの設定などを行っていました。 本格的にフロントエンドエンジニアとして開発業務を行う前に知っておくべきことをたくさん知れて、非常に良い経験ができました。

7~9月 (hinataリユース システム内製化 発足)

アウトドア用品の中古買取・販売を行うサービスを提供している「hinataリユース」の管理システムを内製化することが決まり、7月からリユースチームとして管理画面の作成を行うことが決まりました。

入社して4か月目で新規開発に携わることができ、要件定義から技術選定、開発環境構築を経験させていただきました。

管理画面の第一フェーズ(買い取られた商品を画面上で管理し、外部の中古品販売サイトに出品するところまで)を無事リリースすることができたのですが、現状の開発環境を見直した結果、このプロダクトにはBFFが不要であるという結論になり削除したという経緯があります(詳細は「新規開発においてBFF(Backend for Frontend) を採用すべきか」をご覧ください)。

要件定義の段階でどれだけ考えられていても、実際に走ってみないと分からないということを自ら体感し、完璧な設計は無いということを改めて認識することができました。

10~12月 (リユース開発架橋&ストア兼任)

管理画面の第二フェーズ(中古品の査定からシステムへの登録までを中古品買取の会場で行えるようにするための画面作成)のリリースに向けて、引き続きフロントエンドの開発を行っていました。

徐々に React + TypeScript の開発にも慣れ、コードの可読性やテストコードのカバレッジなどを意識しながら業務をすすめることができました。

また、アウトドア用品の販売を行うオンラインセレクトショップ「hinataストア」の開発業務も担うようになり、リユースチームとストアチームを兼任して開発をしていました。

hinataストアでは Shopify というサブスク制のECプラットフォームを使用しており、ブラウザから商品登録や注文管理、ECサイトのデザイン変更などを行うことができます。

フロントエンドは Liquid というShopify独自のテンプレート言語( eRuby に近い)で記述されており、ノーコードだけでは実現できない変更はエンジニアが対応していました。

社内に Liquid の開発経験があるエンジニアがいなかったため、時間をかけて探り探りで開発を進めていましたが、徐々にできることも増え、ビジネス側の難しい要求にも応えられるようになりました。

ただ、 Liquid での開発体験はあまり良いものではないため、まもなく正式にリリースされる React のフレームワーク Hydrogen でストアのページをイチから作り直したいという願望があります笑

1~3月 (リユース&ストア開発 + 新卒採用お手伝い)

12月末にリユース管理画面の第二フェーズの実装はほぼ完了していたのですが、ビジネス都合によるスケジュールの変更や開発メンバーの入れ替わりなどがあり、リリース前に細かい修正やバックエンドのリファクタリングなどを行っていました。

第二フェーズのリリース完了後は徐々にリユースチームでの開発も減り、hinataストアの事業に注力していきたいという会社の方針もありストアチームでの開発がメインになっていきました。

また、裏では23卒の新卒エンジニア採用を行っており、vivitでは唯一の新卒エンジニアである私が会社説明会や面談などに顔を出し、vivitでの新卒としての働き方について話させていただきました。 自分が1年前と比べてどのように成長したか見つめ直す良い機会でしたし、自分がまだエンジニアとして未熟であることを再認識することができたので、エンジニアとして成長していくためのモチベーション向上にも繋がりました。

終わりに

これまで書いた開発業務以外にも、アウトドア用品の中古品買取イベントに参加したり、hinataレンタルの撮影キャンプにお邪魔させていただいたり、全社的に催されたお花見デイキャンプの実行委員をしたりなど様々な経験をしてきました。

エンジニアリングのスキルを磨きたいということはもちろん、vivitのメンバーとしてアウトドア業界を盛り上げていきたいという思いも強くなったので、積極的にキャンプに行ったりアウトドア関連の情報をキャッチアップしていきたいです。

また、今年の3月に23卒のエンジニア新卒採用を終え、すでに内定者の北條くんがインターンとしてジョインしてくれています。

僕がインターン生だったときと比べてとても優秀なので、追い抜かれないように頑張ろうと思うのと同時に、初心を思い出し、改めてエンジニアリングについて向き合おうと思いました。

1年たってもまだまだ未熟な私ですが、vivitで更なる挑戦をしていきたいです。


vivitでは一緒に働くエンジニアを募集しています。

アウトドア事業に興味がある方、モダンな技術でエンジニアとして成長していきたい方のご応募をお待ちしております!

www.wantedly.com

mediaチームでのasanaの使い方について

こんにちは!技術開発部 media開発チームリーダーの河村です。

今回はvivitで使用しているタスク管理ツールの「asana」について、こんな使い方をしているという事例を交えつつ紹介します。

asanaについて

asanaとはvivitで使っているタスク管理ツールです。

vivitでは入社当時の2020年初頭から無料プランで使っていましたが、当時は技術開発部だけが使用しており、主なプロジェクト管理はスプレッドシートで行われていました。

その後、スプレッドシートより圧倒的にタスク管理がしやすいということで全社的に使うようになりました。

私はいろんな現場でRedmine, Wrike, backlogなど他のツールも使ってきましたが、asanaはUIも直感的だったりタスク間の関係性(親子関係、先行/後続など)がわかりやすい部分がとても気に入っています。

asana.com

asanaのmediaチームでの使い方

mediaチームでasanaを使い始めた時、同じプロジェクトのタスクでも各チーム毎にタスクを持っていてそれぞれのタスクの関係性が見えなかったり、リリース前後でタスクの抜け漏れが発覚するということがありました。

そこでプロジェクトを進める時に以下のようなテンプレート機能を使っています。

このテンプレートでタスクを作ることによって、1つのプロジェクトで必要なタスクを1度に全て揃えることができます。

f:id:Kawam:20220411090152p:plain こちらが親タスクになります。機能追加用テンプレートと書かれた部分にプロジェクト名や実現したい内容が入ります。 説明欄にはこのタスクをなぜやるのか・何がゴールなのかなどの目的を記載し、デザインの関連URLがあれば記入できるようにしています。 vivitではデザインツールはfigmaを使っているので、ワイヤーやfixしたデザインの共有用URLを記入しています。

f:id:Kawam:20220411090218p:plain こちらは親タスクに紐づくサブタスクです。 プロジェクト内で主に対応する必要があるタスクが並べられています。 今回はデザインがいらないなどのケースもあるので、このテンプレートでタスクを作った後に不要なタスクは削除するようにしています。 そうすることでタスクの抜け漏れを防止しています。

f:id:Kawam:20220411090401p:plain こちらは親タスクの実装に紐づくサブタスクです。 実装でもある程度やることが決まっているので必要タスクを並べています。 こちらも不要なタスクがある場合は削除して使っています。

以上のようなテンプレートの使い方をすることでタスクの抜け漏れが防止できたり、タスクの前後関係もわかりやすくなってスケジュール管理がとてもしやすくなりました。

最後に

asanaの機能についてはまだまだ使い切れていない部分も多々あるので、今後も他の機能も見ながらより円滑にプロジェクトが進むような使い方を考えていこうと考えています。

vivit では中途エンジニアも積極募集中です。 少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております! www.wantedly.com

GKE に Nginx でレート制限を入れる時の注意点

こんにちは。 vivit で SRE をやっている 宮本 です!

今回はサービスを Dos 攻撃から守るためにレート制限を導入した際に得られた知見をまとめてみたいと思います。

vivit の各サービスは GKE 上で動いており、 GKE Ingress から最初に転送する Pod は基本的に Nginx のリバースプロキシです。 Nginx からユーザーが利用するフロントや API などにアクセスします。

図にすると、以下のようになります。

f:id:tatsurom:20220310155842p:plain

どうやったのか

やり方自体はシンプルで、 Nginx が提供しているレート制限の仕組みをそのまま利用しただけです。 こちらの Nginx 公式ブログ が分かりやすいです。

一部省略や加工を行っていますが、以下のように設定を入れます。

http {
  limit_req_zone $http_x_forwarded_for zone=limit_x_forwarded:50m rate=3r/s;
server {
  listen 80;
  server_name example.com;
  location / {
    limit_req zone=limit_x_forwarded burst=1 nodelay;
    limit_req_status 429;

これだけです。

レート制限の動作確認は必須です。 検証環境への意図的な攻撃には ab を利用しました。

結果の確認は全て Datadog で行っているので簡単でした。 ホストや UA、IP アドレスなどで絞ることで簡単に、どの程度ステータスコード429 で返せたかが分かります。

f:id:tatsurom:20220310162941p:plain

Datadog は決して安くないツールですが、 vivit の開発部では必要で価値あるものにはしっかり投資する文化なのでケチらず使っています。
SRE だけでなく各チームの開発者も日常的に使っており、トラブルシューティングや開発に大いに役立てています!!

この構成の何が問題か

Nginx は Deployment で管理しており Pod のレプリカ数は2以上ですが、レート制限に引っかかるかの閾値はそれぞれの Pod の中だけで判断しています 😇

つまり L4 Service から Pod へのトラフィックは、Pod 間でほぼ等しく分散されるので、意図した数以上のアクセスを許可してしまいかねません。

当然ではありますが、最初は盲点でした。。。

だからといって、例えばレプリカ数が 4 であることを前提として閾値をチューニングすると、それはレプリカ数に依存した設定になってしまい、Pod を気軽に増減できなくなってしまい Kubernetes の良さを一部捨てることになります 🥲

レート制限においては、Nginx は Kubernetes の考え方と相性が悪いとも言えそうです。

結局どうしたのか

かかる工数や、現状受けている攻撃の規模や傾向、Nginx Pod の負荷などを総合的に判断して、今回はこのまま Nginx を使ってレート制限をすることにしました。

適切にチューニングを行った結果、完璧ではないものの、ある程度攻撃に強い構成にできたので目的は達成です!!

改善するなら

Kubernetes 環境でレート制限を入れたいケースは珍しくなく、そのような場合の最も多い構成はサービスメッシュで入れることかなと思います。

Istio を使っている企業さんは多く見受けられます。 公式チュートリアル もありました。

Istio ベースのマネージドな Anthos Service Mesh を検討する方も多そうです。

入れるとなると Kubernetes クラスター全体に影響があることなので今回は見送りましたが、もっと大規模になってきたら検討しても良いのかもしれません。

最後に

vivit では一緒に働くエンジニアを大募集しています 🎉

www.wantedly.com

少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております!

「エンジニア組織論への招待」という本から学んだ言葉の意味について

こんにちは、技術開発部の河村です。 アウトドアメディア、hinataの開発リーダーをしています。

2021年9月-12月にかけてvivit社内で「エンジニア組織論への招待」の輪読会を行いましたが、今回はその中で普段使っている言葉の意味を改めて知ることで目から鱗だったものをまとめてみました。

gihyo.jp

輪読会を始めたきっかけ

vivit社内では2週間に1度のペースで1on1を行っており、より1on1が有意義なものになる方法がないかと「メンタリング」というキーワードからこちらの書物に辿り着きました。 それ以外にもチームをより強くするために有益な情報がありそうだったので、社内でも私のようにこの本を読んでみたいというメンバーや一度読んだけど改めてみんなと見解を議論したいという声もあって輪読会を始めました。

どういう書籍か

「どうしたら効率よく不確実性を減らしていけるか」という考え方をもとに、エンジニアリングの課題を解決していく方法が書かれている本です。 最初は思考の整理から入り、メンタリングの技術、アジャイルについて、さらにマネジメントについてなどの組織を設計・運営するための解説がされています。

特に学んだ言葉の意味

この書籍を読んでいて特に感じたのは、普段使っている言葉の意味があまり理解できていなかったり勘違いしていたことでした。 その中でも特に学んだことを以下にまとめてみました。

エンジニアリング

エンジニアリングとは不確実性の高い状態から、低い状態に効率よく移すその過程で行う全てのことです。 (開発をすることはその手段の一つであって、開発=エンジニアリングではありません。)

ここでいう不確実性というのは、「未来のこと」「他人のこと」の2つが挙げられます。 これらをマネジメントすることで事業を最速かつ生産性高く成長させられるようになります。

その不確実性を減らすために、

  • 自分の考えにどんなバイアスや思い込みがかかっているのかを知る
  • 未来の不確実性を下げるために仮説と検証を行う
  • 同じ目的で働いているはずの人々とのコミュニケーションの不確実性を減らす(通信不確実性を排除する)

などといったことを日夜行っているのです。

アジャイル

アジャイルという言葉は、「チームが環境に適応して、不確実性を最も効率よく削減できている状態(自己組織化)」のことを指します。 (私はウォーターフォールと比較対象になると思っていたので、方法論の違いだと勘違いしていました。)

動詞ではないので、「アジャイルをやる、アジャイルをする」という言葉も「アジャイルになる」という言葉の使い方が正しいです。 また1on1で行うメンタリングをチームを対象にして行うことで振り返りや学習という仕組みを取り入れた考え方がアジャイルな方法論、アジャイル型開発とも言えます。 

プロジェクトマネジメントとプロダクトマネジメント

こちらもよく聞く言葉です。(いきなり違いを説明しろと言われてもなかなか難しいかと思います。) まず大きな目的の違いとして、プロジェクトマネジメントは終わらせること、プロダクトマネジメントは終わらせないことという違いがあります。

プロジェクトマネジメントは資源や資産、リスクを管理して効果を最大化する方法のことです。 プロジェクトマネージャはスケジュールに対する不安を減らし、納期の大幅な超過やプロジェクトを頓挫させないようにしながら効果を上げて終了させることが役割となります。 また、プロジェクトを進める方法がはっきりしていないのでこの方法不確実性に対処していくことが必要となります。

対して、プロダクトマネジメントは「プロダクトが継続的に収益をあげることで終了しないこと」が目的となります。 プロダクトマネージャはプロダクトがマーケットに受け入れられ続けるように物事に取り組むことが役割となります。 また、やってみないと何を作れば受け入れられるかわからないため、目的不確実性に対処していくことが必要です。

さいごに

この書籍では開発面だけに留まらず、ビジネス側とのコミュニケーションなども含めて会社の組織論が書かれていました。 今後は開発部だけでなく、この話を社内に広めてよりvivitを強い組織の会社にしていければと考えています。

今回は「エンジニア組織論への招待」という本から学んだ言葉の意味についてお話ししてみました。

vivit では一緒に働くエンジニアを大募集しています。 これからもメンバー一人一人が自走していけるような環境作りをしていきますので、その中でみなさんと一緒に働けることを楽しみにしています。

いつでもカジュアル面談を受け付けていますので、是非よろしくお願いします。

【デスクツアー】アウトドア企業のプログラマーの作業環境

はじめに

フロントエンドエンジニアの関です。

vivit株式会社では一部の社員がリモートワークでの業務を行っています。
リモートワークの詳細な取り組みについては下記の記事をご覧ください。

vivit.hatenablog.com

私個人としては感染症リスクについてはともかくとして、リモートワークはしたいもののオフィスでの顔を合わせる業務もバランス良く行いたいと思っています。

今回は最近流行りのデスクツアーと題してオフィスと自宅の両方で快適に作業をするための工夫や商品の紹介ができればなと思います。

オフィス

こちらがオフィスでの私のデスクです。(写真は一部加工しています)
恐らく社内でも随一好き勝手やってるデスクですがとても快適に作業できています。

f:id:KeytacK:20220117041429j:plain

PCは基本的に支給されますがその他周辺機器の利用は個人の自由です。純正コネクタ等一部申請すれば会社側で買ってもらえるものもあります。

f:id:KeytacK:20220117041546j:plain

キーボードは HHKB Professional HYBRID Type-SDomikey Semiconductor for HHKB を付けたもので、ORCASリストレストを使っています。

ポインティングデバイスエレコムトラックボール HUGE M-HT1DRBK を愛用しています。

トラックボールといえばロジクールの親指タイプがポピュラーかと思いますが大玉の安定感が自分は好きです。

ノートPCスタンドは YOHANN のウォルナットを一目惚れで取り寄せました。

基本的に木でできた製品が好きで買ってしまうのですが、これは高さが丁度良くて座りがいいので買ってよかったです。

下には MiTo GMK Pixel のデスクマットを敷いています。

生地が思ったより厚くて打鍵感が凄く心地よいです。

デスク下にはリモート中の方と通話する際に使うヘッドセットが引っ掛けてあります。
(ブームマイク付きが使いたくて家に余ってたのを引っ張り出してきたのでいいのがあれば買い変えたいところです。。。)

基本的にPC周辺機器は毎日触るものなので気に入った道具を揃えたいという思いがあり、デスク周りの用品は妥協なく選んでいます。

自宅

こちらは自宅で作業する際のデスクです。
コミュニケーション周辺のツールを AVerMedia 製品で固めているので親和性が高く快適にテレワークができています。
(仕事だけでなく友人とオンラインゲームをする際もここなのでゲーム用コントローラーも見えていますが平日昼間はちゃんと仕事してます。。。)

f:id:KeytacK:20220117041734j:plain

画角の都合上写せなかったのですが、チェアは Herman Miller の アーロン リマスタード ライト を使っています。購入にはかなり勇気がいりましたが、やはり買ってよかったと言えるものの一つです。

卓上両端には 以前 紹介したキーボードの Lily58 + Domikey SA Dolch Orange がありますが、今スイッチが Candy’s Jade Green に変わっています。
手前には PFU公式 のセパレート型ウッドパームレストを置いています。

vivit.hatenablog.com

トラックボールはオフィスと同様 HUGE M-HT1DRBK を使っています。

モニター上にWebカメラの AVerMedia Live Streamer CAM 313 があり、セットで購入したデスクマットを敷いています。

写真左側には Live Streamer MIC 330 がアームでせり出てきていて、ここでの音周りは全てその下の Live Streamer AX310 で一括管理しています。

家で作業しているときは正面モニター下の iPad Air でタスク管理アプリやカレンダーを表示しています。

終わりに

デスクまわりに限らず海外製品があったりして、注文したら翌日には届くというものでもないので中々おすすめしにくい面もありますがどれも買ってよかったと言えるものばかりです。

執筆時現在のデスク周りはこんな感じですが、ウルトラワイドモニターやモニターアームなど欲しいものが結構あるため割とすぐアップデートされるかもしれません。
情勢的にまだまだ油断の許されない昨今です、テレワーク環境構築の旅はまだまだ終わりません。

vivit株式会社ではアウトドア業界の新たな可能性を共に探す旅人を募集しています。

www.wantedly.com

Datadog で「リリース後にだけ」利用するモニターを作成する

こんにちは。
vivit で SRE をやっている宮本(https://github.com/tatsuro-m)です!

今回は Datadog で「リリース後にだけ」利用するモニタリングリソースをどうやって作成、運用していったかを書いてみようと思います。

基本的には常時 OFF(ミュート)で、リリース後に ON(アンミュート)になり、1時間後に自動で再度ミュートします。

背景

一般的に、何らかのリリースが起点となって障害が発生することは多いと思います。

vivit では常時 Datadog モニターを利用してエラー率、 SLO の監視をしていますが、タイムウィンドウが1週間と長くなっています。

リリース後の1時間など、もっと小さいタイムウィンドウで突発的なエラー率の上昇を監視したいという要件があり、今回の仕組みを構築しました。

前提知識

  • モニターにはミュート、アンミュートの機能があり、ある期間のアラートを無視することができる(モニターの管理)

  • ミュート時にはミュートを解除する時間を指定できる

  • アンミュートする際には再度ミュートする時間を指定する機能は存在しない
  • Datadog は多くのリソースに対して REST API を提供していて、モニターもある(https://docs.datadoghq.com/ja/api/latest/monitors

よって、時間経過によって自動で再ミュートする仕組みは自分で構築しなくてはなりません。

アーキテクチャ

f:id:tatsurom:20220111151024p:plain

順番に解説します。 対象となるモニターは予め作成した上でミュート状態にしておき、 monitor_id を控えます。 monitor_id は Datadog 画面から確認できます。

  1. リリースをトリガーにして1つ目の Cloud Functions を gcloud で呼び出す
  2. unmute エンドポイントを叩いてミュート解除する
  3. Cloud Tasks のキューにタスクを積む。この時、起動時刻のオプションで特定時間を設定する
  4. 指定した時間に HTTP 呼び出しで mute エンドポイントを叩く Cloud Functions を呼び出して再度ミュート

ポイント

トリガーは何でも良いのですが、今回は特定マイクロサービスの本番環境へのリリースです。

vivit では GitOps を採用しているので、Kubernetes マニフェストの更新を検知して GitHub Actions のワークフローが起動するところからスタートできます。

vivit での GitOps については 、

vivit.hatenablog.com

こちらの記事をご参照下さい !!


以下、Cloud Functions で実行される Cloud Tasks キューにタスクを作成する部分のコードです。

ts := &timestamppb.Timestamp{Seconds: time.Now().Add(1 * time.Hour).Unix()}
req := &taskspb.CreateTaskRequest{
    Parent: queuePath,
    Task: &taskspb.Task{
        ScheduleTime: ts,
        MessageType: &taskspb.Task_HttpRequest{
            HttpRequest: &taskspb.HttpRequest{
                HttpMethod: taskspb.HttpMethod_POST,
                Url:        os.Getenv("MUTE_FUNCTION_URL"),
                AuthorizationHeader: &taskspb.HttpRequest_OidcToken{
                    OidcToken: &taskspb.OidcToken{
                        ServiceAccountEmail: os.Getenv("UNMUTE_FUNCTION_SA_EMAIL"),
                    },
                },
            },
        },
    },
}

Go で提供されているクライアントライブラリを利用します。
ScheduleTime に起動時刻を設定することができます。 Cloud Functions の HTTP 呼び出しには認証必須にしているので、 OIDC トークンも忘れずにセットします(ここでハマりました 🤧)。


Datadog は様々な言語の API Client を提供しており、Go のライブラリも存在します(GitHub - DataDog/datadog-api-client-go: Golang client for the Datadog API)。

ただし今回は API Client を使うまでもないと判断したので、Go の標準パッケージで REST API を呼び出しています。

   req, err := http.NewRequest(http.MethodPost,
        fmt.Sprintf("https://api.datadoghq.com/api/v1/monitor/%s/unmute", os.Getenv("MONITOR_ID")),
        nil)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("DD-API-KEY", os.Getenv("DD_API_KEY"))
    req.Header.Set("DD-APPLICATION-KEY", os.Getenv("DD_APP_KEY"))

    if err != nil {
        return err
    }

    resp, err := http.DefaultClient.Do(req)

Datadog の API を呼び出すには、2つの秘匿情報が必要です。

  • api key
  • app key

これらの情報を Cloud Functions に伝えるために、Secret Manager を利用して環境変数に登録しました。

Using secrets  |  Cloud Functions Documentation  |  Google Cloud


「特定時間経過後に Cloud Functions を呼び出したい」という要件だけ聞くと、Cloud Scheduler の利用を検討される方もいると思います。

しかし Cloud Scheduler は Cron を使用する以上、単発の実行には不向きです。

cloud.google.com

単発で実行したいという理由で、今回は Cloud Tasks の scheduleTime を利用する構成で作ってみました。

最後に

割と特殊なケースですが、 Datadog APIGCP のサービスを上手く組み合わせることで、このような構成も可能であることが伝われば幸いです。

vivit では一緒に働くエンジニアを大募集しています。
www.wantedly.com

少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております!

Goでコンストラクタ関数(完全コンストラクタ)を定義する

はじめに

vivitで hinata spot というキャンプ場の検索・予約サービスのbackendを担当しています名嘉眞です。 hinata spot のbackendはGoで書かれています。 今回は、Goでコンストラクタ関数(完全コンストラクタ)を定義した際の問題や対応策について書きます。

※この記事の内容はチームの方針や実装者の考え方、サービスの仕様やユースケースにより変わる部分もあると思いますので、ひとつの参考になればと考えています。

Goでコンストラクタ関数(完全コンストラクタ)を実装

hinata spot では全ての箇所ではないのですが、値オブジェクトの生成などで、 コンストラクタ関数(完全コンストラクタ) を定義しています。 完全コンストラクタ はオブジェクトを生成した時点でそのオブジェクトは正しく利用することができる状態になる実装方法です。

そのためオブジェクトを生成するタイミングで、対象のオブジェクトを生成する上で不正な値ではないか、バリデーションを行う必要があります。

また、setterを定義しないことで、コンストラクタ関数で安全な状態で生成したオブジェクトを変化させることがないです。

※オブジェクトの変更を加えたい場合は、setterではなくその変更内容をふるまいとしてメソッドを定義したりします。

下記は注文金額(OrderAmout)を生成するコンストラクタ関数で0円では生成できないルールを実装しています。 フィールドもprivateなフィールドとして宣言することで外部のpackageから変更できないようにします(フィールドの先頭の文字を小文字にする)

type OrderAmount struct {
  amount uint
}

func NewOrderAmount(args uint) (OrderAmount, error) {
    if args == 0 {
        errors.New("OrderAmoutは0円では生成できない")
    }

    return OrderAmount{amount: args}, nil
}

フィールドがprivateなので、外部のパッケージからのOrderAmountオブジェクトの生成をNewOrderAmountに限定することができました。 また、 NewOrderAmount内でバリデーションが実行されることで不正な値のOrderAmountオブジェクトが生成されることも無くなりました。

しかし上記の実装だと以下のようなちょっとした問題があります。

Goでコンストラクタ関数(完全コンストラクタ)を実装した際の問題

不正な値で生成される可能性がほぼ無い場合でもコンストラクタ関数を使いエラーチェックする必要がある

Goの場合、例外(panic)を気軽に使うことが推奨されていないため、エラーを返す関数の場合はエラーチェックを行い呼び出し元の関数が正しく処理すべきです。

今回の例の場合だとDBの情報からオブジェクトを生成する場合や、テストコードでオブジェクトを使用する場合もエラーチェックを行う必要があります。

※DBの情報からオブジェクトを生成する際もバリデーションチェックするべきという考えもあると思います。

これは人によっては問題ではないと考えるかもしれません。

私も基本的には問題とは考えずに、不正な値でオブジェクトが生成される可能性がほぼ無い場合であっても定義した完全コンストラクタ関数を使ってオブジェクトを生成しエラー処理も書いています。

またテストコードでもコンストラクタ関数を利用してオブジェクトを生成することで不正なオブジェクトを使ってテストされることを防ぐメリットもあります。

しかしテーブル駆動テストコード書いた場合だと特にそうなのですが、テストコードが長くなり見通しが悪くなる可能性が高いです。

テストコードでは完全コンストラクタをラップした関数を用意するなど、テスト用のオブジェクト生成の仕組みを定義するだけでも基本的には解決します。 ただ、完全コンストラクタを定義したオブジェクトの数だけテストコードに関数が作られる可能性があります。

対応策

対応策は単純なのですが、バリデーションを行いエラーを返すコンストラクタ関数と、オブジェクトの生成だけを行うコンストラクタ関数を2つ定義します。

type OrderAmount struct {
  amount uint
}

func (o OrderAmount) Valid() error {
  if o.amount == 0 {
        return errors.New("OrderAmoutは0円では生成できない")
  }

  return nil
}

// NewOrderAmountはバリデーションなし、エラーをかえさない。
func NewOrderAmount(args uint) OrderAmount {
    return OrderAmount{amount: args}
}

func CreateOrderAmount(args uint) (OrderAmount, error) {
    o := NewOrderAmount(args)

  if err := o.Valid(); err != nil {
    return OrderAmount{}, err
  }

  return o, nil
}

前の例と比べると以下のように変更しています。

  • バリデーションを別のメソッドに抽出しています
  • NewOrderAmountはバリデーションせず、エラーも返さない関数として定義しています
  • CreateOrderAmountを完全コンストラクタとして定義します

バリデーションを別のメソッドに抽出しています

バリデーションを別のメソッドにせず、CreateOrderAmount内で処理しても良いですが、分けた方がCreateOrderAmount関数がシンプルになります。

今回の例では0円かどうかのチェックのみですが、ルールが増えた場合は分けた方がわかりやすくなると思います。

NewOrderAmountはバリデーションせず、エラーも返さない関数として定義しています

NewOrderAmountは、シンプルなコンストラクタ関数としてDBの情報からオブジェクトを生成する場合やテストコードで使用します。

エラーを返さないコンストラクタ関数を使用する箇所を限定的にするために、関数名をNewOrderAmountFromRepositoryなど明確にすることも良いと思います。

ただ、NewOrderAmountFromRepository だとrepository層からオブジェクトを生成しない場合などに違和感がありますので別の名前の方が使いやすいかもしれません。

CreateOrderAmountを完全コンストラクタとして定義します

CreateOrderAmountはバリデーションを行い、不正なオブジェクトを生成しない完全コンストラクタとして定義します。そのためエラーを返します。

実際のユースケースで外部からの情報をもとにオブジェクトを生成する場合に使用します。

今回は引数の値をそのまま設定するだけですが、例えば金額計算を行う必要が出てきた場合、CreateOrderAmount関数内で計算処理を行うように変更することもできます。 その場合CreateOrderAmountからCalculateOrderAmountという関数名に変更し、よりドメインロジックの内容にあった関数名にして良さそうです。

まとめ

コンストラクタ関数を2つに分けることで、上記にあげた課題の解消はできました。

ただ、コンストラクタ関数が2つになることで問題も生まれます。

それは、実際はバリデーションを行うべきユースケースで、バリデーションを行わないシンプルなコンストラクタ関数が使用されることです。

この問題はレビューで確認したり、コンストラクタ関数の命名をより明確にしていくことである程度は防ぐことができると思いますが、完全に防ぐことはできないと思います。

そのためコンストラクタ関数を2つに分けるのは、オブジェクトが利用される頻度などを考えて分けた方が良さそうと判断した場合のみと考えています。

最後に

Goでコンストラクタ関数(完全コンストラクタ)を定義した際にでた問題とその解決策に関して書きました。 記事の冒頭にも記載しましたが、チームの方針や実装者の考え方、サービスの仕様やユースケースにより変わる部分もあると思います。また人によっては問題ではないと考えるかもしれません。 今回紹介した内容はコンストラクタ関数の実装方針という細い部分かもでしたが、誰かの参考になれたら幸いです。

vivitではGoを書きたい人を募集しています!