Nuxt.js化計画vol.6

f:id:kubotak:20200317182842j:plain
Nuxt.js化計画vol.6

第6弾です。

Nuxt.js化計画の概要についてはvol.1を参照ください。
また、過去のシリーズも通してリンクしているのでぜひ御覧ください。

tech.macloud.jp

今回はリプレースではなく、新規機能に伴う新しいページをNuxt.jsで構築しました。
かんたんM&A診断という機能です。

macloud.jp

7つの設問に答えることで売却したい会社・事業に対してマッチする買い手企業をレコメンドするという機能です。

今回はNuxt.js側では初の試みとしてABテストを行っています。
ABテストについては次の記事で紹介しています。

tech.macloud.jp

かんたんM&A診断のLPでデザインを異なるパターンでABテストを行います。
ちなみにこのLPはジョインして日の浅い塚原さんがコーディング・実装してくれました。即戦力!

f:id:kubotak:20210422110220p:plain
LPパターン

設問は以前売却・調達情報の案件登録ページでも実装したようにSPAとしてクライアント側でスムーズに遷移します。

f:id:kubotak:20210422174615g:plain
設問の遷移

f:id:kubotak:20210422174631g:plain
キーワードサジェスト

設問2で表示されているキーワード登録ではサジェスト機能が働いてるのがわかるかと思います。
データベースに登録されているキーワードから検索してGoogle検索窓のようにサジェストでキーワード候補を表示します。

サジェストの機能はvue-simple-suggestを利用しています。
シンプルの名の通り必要な機能(API)が一通り揃っていて使いやすく、ポリフィルも用意されているのでIE11+でも動作します。
弊社プラットフォームの利用者の性質上、まだまだIEは切り離せないのでありがたいです。

f:id:kubotak:20210422151204p:plain
結果のOGP

設問を完了すると結果ページが表示されます。
このページはSNSシェアされることを想定していますので、それぞれ結果に応じてOGPで利用される画像が異なります。

Nuxt.jsはユニバーサルモードで運用しているのでこのように動的なSSRが必要な機能も作れるので強力です。

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください!
Nuxt.jsで開発したい方は是非どうぞ

www.wantedly.com

www.wantedly.com

Laravelで監査ログを実装する

f:id:kazuhei0108:20210418191338j:plain
ログハウス

こんにちは、かずへいです。

M&Aクラウドサービスで管理画面に監査ログの記録機能を追加しましたので、その実装方法について紹介致します。

監査ログとは

f:id:kazuhei0108:20210418191637j:plain
実際の監査ログ画面

監査ログは監査証跡として、システム監査人が追跡するために、操作内容やそれに伴うデータの移り変わりが時系列に沿って記録されているログのことです。

システム監査人とは、システムが正しく動いているか、問題が合っても追跡できるか等を確認する人で、公民情報システム監査人という資格もあるようです。

cisa.jp.net

Laravel Auditingの活用

Laravelでの監査ログの記録にはLaravel Auditingというライブラリが便利です。Githubのスター数も2,000件を超えており、実質的にデファクトみたいになっているようです。

github.com

ドキュメントもしっかりしています。

www.laravel-auditing.com

Laravel Auditingの導入

以下でLaravel Auditingをインストール出来ます。

composer require owen-it/laravel-auditing

その上で、設定ファイルをライブラリ側からコピーして自分のプロジェクトに持ってきます。

php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="config"
php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="migrations"

必要に応じて設定は変更してください。

あとはOwenIt\Auditing\Auditable.phpというトレイトが用意されているので、監査ログを入れたいEloquentを継承しているモデルに use Auditable すれば終わりです。簡単ですね。

Eloquentを使っていない部分の対応

弊社のシステムではEloquentを使わずにQuery Builderを使っている箇所もあり、そちらはTraitをuseするだけで実装することが出来ませんでした。

Laravel AuditableにはAuditorという監査ログを記録するクラスがあり、それを直接呼び出す事もできるので、クエリビルダーを継承し、insertやupdateが呼び出される度に一緒にAuditorも呼ばれるように実装を変更しました。

<?php
declare(strict_types=1);

use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use OwenIt\Auditing\Contracts\Auditor;

class QueryBuilderWithAudit extends Builder
{
    /**
     * @var Auditor
     */
    private Auditor $auditor;

    public function __construct(Auditor $auditor, ConnectionInterface $connection, Grammar $grammar = null, Processor $processor = null)
    {
        parent::__construct($connection, $grammar, $processor);
        $this->auditor = $auditor;
    }

    public function newQuery(): self
    {
        return new static($this->auditor, $this->connection, $this->grammar, $this->processor);
    }

    public function insert(array $values)
    {
        $result =  parent::insert($values);
        $this->auditor->execute(new CreatedAudible(null, $this->from, $values));

        return $result;
    }

    public function insertGetId(array $values, $sequence = null)
    {
        $result = parent::insertGetId($values, $sequence);
        $this->auditor->execute(new CreatedAudible($result, $this->from, $values));

        return $result;
    }

    …省略…

このQuery Builderを継承したクラスを使いさえすれば、後の実装はいつものとおりにするだけで、監査ログが記録されるようになります。

まとめ

LaravelではLaravel Auditingを活用することで簡単に監査ログを導入することが出来ます。 これによりシステムが正常に動いているか、問題が起きたときに、システム上で誰が何を変更していたのか等を追跡できるようになりました。

Laravel Auditingはドキュメントもしっかりしていて、簡単に導入できるのでぜひ使ってみて下さい。

おわりに

M&Aクラウドではデザイナーやエンジニアを募集中です! 少しでも興味を持っていただけましたら、是非お気軽にご連絡ください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

「ナナメウエをいく」という開発バリューについて

f:id:hamakou108:20210405123141j:plain

こんにちは。エンジニアの濱田( @hamakou108 )です。

今回は開発チームのバリューの一つ「ナナメウエをいく」について紹介したいと思います。

開発チームのバリュー

M&Aクラウドの開発チームでは行動指針として以下の三つのバリューを制定しています。

レバレッジ指向」と「全員インフルエンサー」の二つについては過去の記事で紹介していますので、是非そちらも読んでいただければと思います。

tech.macloud.jp

tech.macloud.jp

今回は最後の一つ、「ナナメウエをいく」について紹介します。

「ナナメウエをいく」とは?

M&Aクラウドは「2nd Priority」というコーポレートバリューを掲げています。

corp.macloud.jp

2nd Priority

顧客第一になろう。
それ以外の都合は2番目に置いておいて、まずはなによりユーザーを大事にすること。ユーザーが求めることに素直に応えること。
長期の視点ではユーザーにもっとも価値を提供できる会社が、必ず流通革命を起こす。自分たちの作ったサービスで、ユーザーが喜ぶ瞬間は最高の時間だ。
とにかく迷ったら自分達のことより、ユーザーのことを考えよう。

営業や M&A アドバイザーのようにお客様と対面で直接関わるメンバーにとっては比較的意識しやすいバリューかもしれません。 一方で開発者の場合、ユーザーと直接関わる機会は少なく、ややもすると画面の向こう側のユーザーよりも目の前の機能開発やバグ修正に目を奪われてしまします。

開発チームとしてどうすれば 2nd Priority を実践できるのか話し合った末、生まれたバリューがこの「ナナメウエをいく」です。

ユーザーにとって使いやすいもの、必要なものを作ることでユーザーのニーズを満たすことはできるかもしれない。 しかし我々は卓越したアイデアを出し、それをプロダクトとして具現化することで、ユーザーの想像のナナメウエをいく価値を届けたい。 そのためには顧客ヒアリングやビジネス・テクノロジーの理解を積極的に進めていこう。

このような意味がバリューに込められています。

バリュー体現に向けた取り組み

ユーザーを理解する

先ほど「顧客ヒアリング」という言葉が出てきましたが、開発チームのメンバーは定期的に商談やヒアリングなどに同行させていただいています。

同行を通して実感したことの一つは、各売り手・買い手企業様の M&A に対するスタンスの幅広さです。 最初から能動的に M&A を進めたいと思って仲介業者を探したりプラットフォームを訪れる方もいれば、そもそも自分の会社を売却するという選択肢に気づいていない方もいらっしゃいます。

ユーザーの属性が画一的でないことを理解するにつれて、日々の開発で意識することも少しずつ変化してきたように思います。 自分が開発している機能はどんなペルソナに向けて提供するのか、どんな価値を感じてもらえれば成功なのか、というようにプロダクトの意義を意識する重要性を感じています。

ナナメウエの UX を提供する

M&Aクラウドで売り手として本登録する際には会社や M&A に関する情報を幾つか入力していただいています。 そのうち会社 URL や従業員規模といった項目はその場ですぐに入力できないケースが多いのか、離脱が発生しやすい箇所となっていました。

この問題への対策として ST&E というツールを導入しました。 このツールの導入により、会社名の最初の数文字を入力すると会社名の候補が一覧表示され、自分の会社を選択すると会社 URL や従業員規模などの情報が自動で補完されるようになりました。

会社名の一部を入力し、候補一覧が表示された際のスクリーンショット
会社名の一部を入力すると、候補一覧が表示される

この効果はデータとしても現れており、会社 URL の離脱率が導入前後で 57% も減少しました! 「ナナメウエ」の UX を提供し、データからも分かるように明確に価値を向上させることができたと考えています。

カスタマーサポートの業務効率化への貢献

先ほどの自動補完機能の話には続きがあります。

自動補完の機能のリリースを全社に向けて発表したところ、カスタマーサポート(以下 CS と書きます)チームから法人番号などのデータも一緒に取得してほしいと要望がありました。 CS チームでは売り手企業様のデータを調査する際に法人番号を検索しており、この作業のコスト削減を狙っての起案でした。 会社の検索履歴のデータは(フォーム中では使用されなかった情報も含め)管理ツールから CSV 形式でエクスポートすることができるため、このデータ取得の方法を CS チームに情報共有しています。

2nd Priority の指す「ユーザー」は社外の関係者のみではなく、社内のメンバーも含んでいます。 結果論ではありますが、社内メンバーに対しても「ナナメウエ」の価値を届けることができた好例だったと思います。

まとめ

「ナナメウエをいく」という開発バリューとそれを体現するための取り組みについて紹介しました。 バリューが制定されてから日が浅く、事例はまだ少ないのですが、今回紹介したような活動を継続してより多くの「ナナメウエ」なユーザー価値を提供していきたいと思います。

最後に

M&Aクラウドでは「ナナメウエをいく」のバリューを体現したいデザイナーやエンジニアを募集中です! 少しでも興味を持っていただけましたら、是非お気軽にご連絡ください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

DDDの設計方針を維持しながらN+1問題を起こさないようにする方法論について

こんにちは。エンジニアの鈴木(@yamotuki)です。
今回はDDDの設計方針を維持しながらSQLのN+1問題を起こさないようにする方法論について書いていきます。

前提

レイヤードアーキテクチャについて

弊社では、DDDの考え方を取り入れたレイヤードアーキテクチャが使われています。

GET処理に関わるコードですと、具体的には以下のようなレイヤに別れています。特に今回のお話に関わる部分は太文字にしておきます。

  • Controlller
  • Scenario Service
  • Query Service
    • 画面やコマンド出力に必要な Data Transfer Object (以下DTOと呼ぶ)を返すのが責務
    • Domain Entity (以下Entityと呼ぶ)そのまま返すので事足りるのであれば強いて新しい DTO を作らず Entity をそのまま返すこともある
    • Repository や Domain Service を呼び出す
  • Domain Service
  • Domain Repository
    • InterfaceとしてDomain層に配置される
    • Entity もしくは Entity のリストだけを返しても良い制約がある。DTO はアプリケーション層のものなので DTO は返さない
    • Repository には参照と更新の責務があるが、この記事では参照だけに焦点を当てる
  • Infra Repository
    • 実装としてInfra層に配置される

例とする Entity の切り方

Entity の切り方として、例えば以下のようにしたとします(弊社事例を使って話そうかと思ったのですが、前提知識が多くなって分かりづらくなりそうだったので汎用的な例を用います)

  • User は Entity として存在する
  • User が持っている Watch(腕時計)も User とは独立した Entity として存在する
    • Watch の中に所有者情報として User Id を持っている

例とする DTO の構造

<?php
class UserWithWatch
{
    private User $user;
    private ?Watch $watch;

    public function __construct(User $user, ?Watch $watch)
    {
        $this->user = $user;
        $this->watch = $watch;
    }

    // 略: 必要な getter など
}

これまでの実装方針

Repository が Entity か Entity List しか返せない制約により、DTO として複数の Entity にまたがる情報をまとめる場合には、QueryService において複数の Repository を呼ばなければなりません。

仮に、管理ツールのユーザ一覧で「つけている腕時計の情報も一緒にみたいよ」というニーズがあったとします。愚直にやると以下のような実装になります。この実装手法は、過去に(現在も一部?)弊社で使われていました。

  • User Repository の getUserList() から User List を返す
    • SQL発行1回
  • Query Service において、N件のUserを含む User List をループで回し、その中で Watch Repository の get() を叩く
    • SQL発行N回

この実装の問題点は、get() の内部で1回SQL実行されるため、User 件数が多くなると、Watch Repository の get() 回数もそのまま増えることです。つまり"N+1 問題"を起こしています。サービスリリース当初やlocal開発環境では数十件しかなくてSQL発行数もたかだか100件程度だったものが、仮にユーザが10000人になりそのリストを表示させたいとなると、10000回以上のSQLクエリが発行されることになります。つまりページ表示がとても重くなります。

解決方法案は何があるか?

一般的に用いられる解決策は以下のようなものがあるかと思います(※私の観測範囲のみ)。

  • キャッシュをする
  • 一覧にページネーションを追加する
  • CQRS を使う

弊社ではどれも使っています。ただし、メリットデメリットあるため、必ずしも全てのケースでぴったりの解決策とはなりませんでした。

  • キャッシュをする
    • Watch Repository の get() でキャッシュを差し込み、N件取得を早くする方法
    • プロダクト立ち上げ時点ではこの手法が採用されていました
    • メリット
      • コードを大きく変更しなくて良い。ServiceProvider により差し込む Infra Repository を Cache を使う Repository に差し替えれば良いので導入が簡単
    • デメリット
      • SQLより早いかもしれないが、キャッシュにN回問い合わせされるのは変わらないので、件数が増えるとやっぱり重い
  • 一覧にページネーションを追加する
    • メリット
      • 一回で取得するユーザ数が少なければ N+1問題も大した問題となりません
      • 件数が多いとブラウザ側でDOM表示に時間がかかるのでページネーションは入れることで、ページは軽くなる
    • デメリット
      • 用途によりますが、管理ツールで一度にたくさん見たいとなると1ページあたりの件数を強く制限するのは社内UXが下がる。弊社の管理ツールのケースだと、件数はそこまで小さくしていないところがあります
  • CQRS を使う
    • Repository を通さず、アプリケーション層で DTO を返してくれる Interface を定義し、Infra 層で実装する
    • メリット
      • 部分的に導入できる
      • Infra層実装次第だが、join を使用してSQLを1回にすることができるので高速
    • デメリット
      • 過去作ってきた Repository の実装を使用できず、新たに Infra 層を記述しなければいけず、join してフィールド全て列挙するなどの手間が少なくない。また、infra から Domain や DTO にコンバートするロジックが散らばり、保守性が悪い
      • 今まで、ドメインロジックは Domain Service や Domain オブジェクトに書いてきたが、それらを経由しないことによりロジックの置き場所がない
    • 詳細は以下の記事が詳しいです

第4の選択肢としてのHashMapAttachment法

HashMapAttachment法は広く知られている単語ではなくて、私が社内で啓蒙のために名前が欲しかったので適当につけているものです。ググっても他の記事は出ません(実は正式名称ある、などあれば連絡ください)。
Repository から取得したものを Query Service で DTO に変換する時にN+1問題とO(N2)に陥らないようにするテクニック。

この方法の詳細は以下の通りです

  1. Repository から Entity List(①とする)を取得する
  2. DTOの中に含めたい Entity List(②とする) を別にリストとして取得する(ループを回してそれぞれ取得するとN+1)。取得したリストを①に含まれるIDをKeyとするHashMapに変換する。
  3. ①をループで回して、HashMapのキーにより②から該当のものを探してDTOを作る
    • HashMap の探索のオーダはO(1)
    • 補足: HashMapを用いず、①をループで回して②を全件走査するのだと、N+1問題にはならないが、O(N2)のコードになり、将来性能の問題が起こり得るので避けた方が良い

実装イメージを掴んでもらうため、実装の一部を紹介します。

<?php
class UserQueryService
{
    // プロパティなど省略

    public function getDtoList(): UserWithWatchList
    {
        // ①のリスト取得
        $userList = $this->repository->getList();

        return $this->attachWatch($userList);
    }

    private function attachWatch(UserList $userList): UserWithWatchList
    {
        // ②のリスト取得
        $watchList = $this->watchRepository->getListByUserIds($userList->toUserIdList());

        // ②のリストをHashMapに変換
        $watchHashMap = $watchList->toUserIdHashMap();

        $resultArray = array_map(function (User $user) use ($watchHashMap) {
            // HashMap からの取得で軽量
            $watch = $watchHashMap->get($user->getId());

            return new UserWithWatch(
                $user,
                is_null($watch) ? null : $watch
            );
        }, $userList->toArray());

        return new UserWithWatchList($resultArray);
    }
}

この方法を使用すると、Entity List などにその List に対してのロジックを持たせて使用することができます。
また、今までの DDD に沿った実装方針の資産をフル活用することができます。

終わりに

サービス成長に伴い、今までの設計が限界を迎えることがよくあります。弊社では、設計をアップデートし、ユーザに価値を届けられるコードを一緒に作ってくれるエンジニアを募集しています。

www.wantedly.com

Laravel Meetup Tokyo Vol.13 Onlineで津崎がLT登壇しました

こんにちは、エンジニアの津崎( https://twitter.com/820zacky )です。

最近は燻製作りにハマっていて、よくベーコンを作っているのですが、 ベーコンの食べ過ぎでいよいよ自分が🐷ちゃんになりそうなフェーズに突入しております。

f:id:zacky2:20210308211814p:plain
ぶたちゃん

Laravel Meetup Tokyo Vol.13 Online

先日「Laravel Meetup Tokyo Vol.13 Online」というイベントに参加させていただきました。

「Laravel Meetup Tokyo」は、PHPフレームワークLaravelで開発を行うエンジニアを対象としたmeetupです。 今回およそ1年半ぶりの開催で、初のオンライン開催とのことです。

こちらのイベント詳細はこちら。 laravel-meetup-tokyo.connpass.com

今回は、以下のタイムテーブルで行われました。

f:id:zacky2:20210308212618p:plain

ytake ( https://twitter.com/ex_takezawa )さんのDDDのお話がとても白熱していたのと、 Hiroki Matsuo (https://twitter.com/handm871 )さんのLaravelあるあるネタがオーディエンスを沸かしていたが印象的でした。

Laravel Meetup Tokyoのおもひで

2019年の5月に行われた前回の「Laravel Meetup Tokyo」にも参加させていただきました。 当時は弊社に入社したばかりで、LaravelもPHPもど素人で、勉強会に参加するのもほとんど初めてという感じだったのを懐かしく思います。

スピーカーは、「吉田あひる」さん、「くまモンエンジニア」さん、「suthio」さん、「おかしょい」さん、「カンボ」さんで、 今でもLaravel界隈のイベントやTwitterでよく目にする有名な方達が登壇されていました。 今回はLT参加でしたが、同じように登壇できたことを嬉しく思います。

当時はLaravelに苦戦していた時期だったので、同じくLaravelを使っている方々とピザをつまみながら直接お話しできたのがとても楽しかったです。 コロナになってしまい、対面でお酒を飲みながら話すことができないのが、やはりちょっと寂しいですね。

発表内容

今回は「アクセス制御ライブラリ Casbinを使ってみた」というテーマでLTさせていただきました。 ご覧いただいた皆様ありがとうございました。

speakerdeck.com

最後に

エンジニア募集中です! 一緒に燻製やりましょう!

www.wantedly.com

www.wantedly.com

【AkitoTsukahara】中途入社しました。

f:id:tsukahara1991:20210226173143j:plain
 

みなさん、こんにちは!

今月よりM&Aクラウドにジョインしました塚原です。 ネット上では以下のアカウントで活動しております。よろしくお願いします!

AkitoTsukahara (akito) · GitHub
Akito.Tsukahara (@AkitoTsukahara) | Twitter

自分が入社するまでの経緯と、入社して感じたM&Aクラウドの魅力をご紹介させていただきます。

入社までの経緯

前職ではWebの受託開発の会社でWebエンジニアをしていました。
担当していたプロジェクトでは、設計・開発・保守、顧客折衝にまで及び、プロジェクトに関することは満遍なく携わってきました。
お仕事する上で、エンジニアとしてシステムを開発するだけではなく、クライアントの期待値を意識したコミュニケーションを心がけていました。

今回はCTOの荒井さんからお誘いいただき入社する運びとなりました。
お声がけいただいたきっかけは昨年のPHPerKaigi2020での登壇経験を評価いただいてのことでした。(外部に向けて発信するって大切ですね!)

入社して感じたM&Aクラウドの魅力

日々アップデートされるアーキテクチャとそれを支える環境

M&Aクラウドの魅力の1つは、入社を決めた理由にもなったM&Aクラウドプラットフォームのアーキテクチャです。前職の受託開発会社でもシステム設計・開発をしていましたが、スケジュールや予算の兼ね合いで、アーキテクチャを意識した開発や技術的負債を解消する機会作ることが難しく、歯痒い思いをしておりました。。。

一方で、M&AクラウドではDDD や CQRS などの考え方をシステムに取り入れて開発されており、ビジネスの複雑性と技術的な複雑性に応えられる設計になっています。また、現在進行中のプロジェクトでも実装方針の意見交換が活発に行われており、メンバー全員がシステムの設計・開発に高い関心を持って、開発を進めていると感じました。 システムの完成度の高さはもちろんのこと、アーキテクチャに高い関心を持つエンジニアメンバーがいることも素敵だと感じました。

エンジニアが主導する「リリース⇆計測&ヒアリング」のサイクルがある

開発チームでは毎日サービスのKPIを確認するMTGがあります。直近のリリースした機能がユーザに使われてるのか、さらに改善できるところは無いかと議論しています。
また、営業の方に同行してクライアントのアポイントメントに参加する機会があります。数値データだけでなくサービスを利用するクライアントの意見に触れる中で、サービス改善の気付きや学びを得る為の取り組みが行われています。
より良いサービスをリリースし続けていく仕組みが根付いていることもM&Aクラウドの魅力だと感じました。

これからは

M&Aは専門知識が多く、この会社の事業ドメインを理解するまでにやらなければいけないことが盛り沢山で大変ですが、新しい見識が得られる機会を楽しんでいこうと思っています。

開発チームの行動指針「全員インフルエンサー」を自分も体現できるように情報発信を増やしていきます! (自分も含めて、開発チームは全員PHPerKaigi2021に登壇しますので、ぜひご覧ください。)

引き続き、技術力をさらに磨き、事業ドメインの知識を身につけて、1日でも早くみなさまにM&Aクラウドのさらなる活躍、素敵なサービスをお届けできるように頑張って参ります!

最後に

M&Aクラウドでは、エンジニアを募集中です!!興味がありましたら、是非以下からご応募ください!

www.wantedly.com

www.wantedly.com

開発のプロジェクト管理をGitHub ProjectsからJiraに移行しました

f:id:kazuhei0108:20210219113421p:plain

こんにちは。M&Aクラウドのかずへいです。

今年に入って開発のプロジェクト管理をGitHub ProjectからJiraに移行しました。

どのような意図で今回Jiraに移行したのかを紹介させてもらえばと思います。

GitHub Projectsを利用していたときの課題

GitHub Projectsを使い始めた時はホワイトボードの物理的なカンバンの代わりとしてだったと思います。その時のエンジニア数はまだ3名で、 GitHub Projectsの機能でも特に問題は感じていませんでした。

元の運用は、

  1. issueのマイルストーンバックログを作る。マイルストーン内ではissueが優先度順に並べられる。ストーリーポイントは数字のラベルをissueにつける。
  2. マイルストーンから次のスプリントでやる分だけをスプリントマイルストーンに移動、GitHub Projectsに紐づけてカンバンに表示させる。
  3. スプリント開始!プルリクエストにissueを紐付けておくとプルリクエストがmergeされたときにissueもdoneになって便利。

というシンプルな形でした。

しかし、メンバーが増え、リポジトリが増え、管理するタスクが増えるとこの運用がつらくなってきました。

つらかった点

GitHub Projectsでの運用で辛かった点には以下の様なものがありました。

  1. マイルストーンGitHub Repositoryに紐付いているissueをまとめる機能なので、複数のRepositoryをまたいで一つのマイルストーンにissueを集めることが出来ない。
  2. issueが入れ子関係を持てないのでlabelで管理することになり、labelが増殖する
  3. 消化したストーリーポイントを数えるのがめんどくさい
  4. asigneeをつけ忘れると誰がやってるか分からない
  5. バックログの整理が大変(GitHubマイルストーンは複数のissueをドラッグアンドドロップで動かせない😢)
  6. 何がいつリリース予定なのか整理するのが大変

たまたま、エンジニア以外からもタスク管理ツールを選定して導入して欲しいという話があったので、このタイミングでJiraを全社導入し、開発チームもJiraに移行することに決めました。

Jiraの導入

今回Jiraに乗り換えたことで上記の問題が全て解決されました!

  1. Jiraのバックログリポジトリに関係無いので、複数リポジトリにまたがるストーリーも管理できる
  2. epicを親のissue、storyを子のissueとすることによって、issueを入れ子で管理
  3. スプリントのベロシティを自動で計測してくれるし、epicに属するissueのポイントの合計もすぐ表示される
  4. カンバン上の移動にルールを追加することが出来るので入力漏れがない
  5. バックログのissueは複数選択出来、簡単に動かせる。
  6. いつ何がリリースされるのか一目瞭然

いくつかピックアップして説明していきます!

スプリントのベロシティを自動で計測してくれるし、epicに属するissueのポイントの合計もすぐ表示される

f:id:kazuhei0108:20210217192440p:plain
Jiraのベロシティーチャートの画面

スプリントの機能を使って、スプリントの開始、終了を実行すると毎回の消化ポイントを自動で集計してレポートに出してくれます。 以前はissueについてるlabelのストーリーポイントを数えていましたが、そんなことをする必要はありません!

また、epicに属するissueの合計の見積もり、完了、残りのストーリーもすぐ表示されるので、大きな機能がどれくらいでリリースされそうかわかります。

f:id:kazuhei0108:20210217193332j:plain

カンバン上の移動にルールを追加することが出来るので入力漏れがない

f:id:kazuhei0108:20210217194559j:plain
左下に足りない入力項目が出ています

カンバン上でTO DOからWIPにカードを移動させようとすると、移動するためのルールを満たしているかどうかのチェックが走ります。 今は、担当者とリリース予定の日付を入力しないとWIPに移動出来ないようにしています。

バックログのissueは複数選択出来、簡単に動かせる。

f:id:kazuhei0108:20210217194921j:plain
ドラッグアンドドロップで簡単にissueを動かせます

Jiraのバックログではスプリントとバックログが同じページに表示されており、簡単に移動させることが出来ます。

いつ何がリリースされるのか一目瞭然

f:id:kazuhei0108:20210218194616j:plain
リリース機能

Jiraのリリース機能によって、リリース予定日ごとに何をリリースするかをまとめるようになりました。 GitHubのPRと連動しているので、もうマージされたのか、まだレビュー中なのかというところまで細かく分かるようになっています。

移行手段

すでにGitHubに大量のissueが積まれており(500個以上!)、それらを整理してJiraに移行する作業はどう考えても苦行に思えました。GitHubAPIでissueをダウンロードしてcsvに整形してJiraにアップロードするというのが正攻法のようでしたが、そもそも全部移行する必要は無いのではと考えたため移行したいものだけ移行できるZapをZapierで作りました。

GitHub issueのコメントにjira移行と書くとJiraのストーリーを作ってくれた上で元のGitHub issueのURLを説明文に入れてくれるというすぐれものです。

f:id:kazuhei0108:20210218194141p:plain
zapierのzap作成画面

移行してみて

Jiraに移行したことにより、これから更にチームメンバーが増えたり、Repositoryの数が増えたりしても大丈夫な体制を構築できたと自負しています。 また、想定していたわけではないのですが、開発のビジネス要件が全てJiraにまとまるようになり、PMとのコミュニケーションが円滑になりました。 元々はGitHub Issue上でやっていたことですが、GitHub上で管理すると、どうしてもコードについてのアレコレを書いてしまいがちになったり、どこのRepositoryに起票したらいいんだ、となったりしていました。 これがJiraとGitHubで使い分けられたことにより、PMはJiraのみ見れば良い状態になりました。

まだ導入して1ヶ月ほどですが、開発チームの状態に合わせてこれからカスタマイズを続けていきます!

Nuxt.js化計画vol.5

f:id:kubotak:20200317182842j:plain
Nuxt.js化計画vol.5

第5弾です。

Nuxt.js化計画の概要についてはvol.1を参照ください。 また、過去のシリーズも通してリンクしているのでぜひ御覧ください。

tech.macloud.jp

徐々に弊社アプリケーションのフロントエンドもLaravelからNuxt.jsに移行しています。
今回は成約事例一覧M&Aお役立ち資料ページがめでたくNuxt.js化されました。

実は今までNuxt.jsによる動的なルーティングは使っていませんでした。
今回のリニューアルのタイミングで個別のページはNuxt.jsによるルーティングを利用しています。

成約事例一覧

https://macloud.jp/interviews

弊社のプラットフォームであるM&Aクラウド上での成約事例をインタビュー記事として紹介しているページです。
リニューアル前は絞り込み機能はありませんでしたが、今回Nuxt.js化する際に追加されました。
記事についているタグで絞り込むことができます。

M&Aお役立ち資料

https://macloud.jp/documents

M&A時に必須である資料のフォーマットの配布や、弊社調べによる有益な資料をダウンロードすることができるページです。

NotFoundの扱いについて

ここからは少し技術的な話をしたいと思います。
今回のリニューアルから新たにNuxt.jsの動的なルーティングを利用しています。
ここで注意しなくてはならないのは存在しないページの処理についてです。
ページのコンテンツはHTTP APIによってそのリソースが存在するか確認します。
厳密に言うとリソースが存在することを確認するというよりも、リソースを取得できない場合にそのリソースが存在しなかったという結果をHTTPのステータスコードで表現することが一般的だと思います。

Nuxt.jsではaxiosというHTTPクライアントライブラリを利用してHTTP APIからデータを取得しています。
axiosではレスポンスからHTTPのステータスコードステータスコードが取得できますので、404だった場合はNuxt.jsのerror関数を実行することで404画面を表示することが出来ます。

error({ statusCode: status })

※axiosは200系以外のステータスコードをすべてaxiosErrorという例外で扱われます。適宜設定を替える必要があります。 https://github.com/axios/axios/blob/master/lib/defaults.js#L79-L81

しかし、弊社の場合は取得した値を一旦store(Vuexによるグローバルにアクセスできるデータストア)に格納して、そのstoreからデータを取得するロジックになっています。
そのため、axiosで404の場合はエラーにはせずに、storeからの取得の際にデータがない場合に独自の例外を発生させて404を表現しています。

try {
  useInterview(store).getInterview()
} catch (e) {
  if (e instanceof NotFoundError) {
    error({
      statusCode: e.statusCode
    })
  }
}

SSRCSRに跨ったシャッフル処理

Nuxt.jsはSSR(サーバーサイドレンダリング)とCSR(クライアントサイドレンダリング)によるユニバーサルなアプリケーションを作ることが出来ます。
この挙動に関してハマリポイントがあったので共有します。
それは、シャッフルを利用したDOMの操作です。

対象の箇所は以下のようなカルーセルです。 f:id:kubotak:20210212151612p:plain ここでは表示する度に要素がシャッフルされる仕様です。
配列をシャッフルしてその配列をeachで表示する至ってシンプルなロジックです。

問題になったのは、SSR時にシャッフルされてレンダリングされた結果と、表示時にCSRでシャッフルしてレンダリングが異なるため一部のレンダリングがずれた事にありました。
imgタグのsrcがCSR時に変わっていなくてタイトルと画像が食い違うという事象がありました。

そのため、シャッフル要素に関しては都度表示が変わるためSEOとしても対応不要としてCSR時のみ実行されるように変更しました。
Nuxt.jsではこの様にSSRCSR時の挙動も把握する必要があると痛感しました。

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください!
Nuxt.jsの挙動を熟知している方は大歓迎です!

www.wantedly.com

www.wantedly.com

Vue Component をサイトの各所で別々の条件によって表示する機能を Nuxt.js + TypeScript で実装する

Nuxt.js

Nuxt-2.14 TypeScript-3.7

こんにちは。エンジニアの濱田( @hamakou108 )です。

先日、あるモーダルをサイトの各所で別々の条件によって表示する機能を Nuxt.js + TypeScript で実装する機会がありました。 このときの設計が他の場面でも応用できそうな知見としてまとめられそうだったので、この記事で紹介したいと思います。

話さないこと

  • Vue.js (Composition API), Nuxt.js, TypeScript の基本的な使い方
  • 時間計算などの細かいアルゴリズムの実装

背景

M&Aクラウドでは M&A に役立つ資料の一部のダウンロードや買い手企業様とのマッチング機能を会員登録された方向けに提供しています。 サイトのコンテンツが気になった方に少しでもリーチするため、会員登録を促すモーダルを特定のタイミングで表示しています。 勿論モーダルが何度も表示されてしまうようでは UX を損ねるため、どのページでどのような行動を行ったときにモーダルを表示するか様々な条件によって制御されています。

設計

様々な種類の条件を手続き的に実装してしまうと、保守性に乏しいコードが出来上がってしまうのは目に見えています。 そこでまず幾つかの具体的な要件を取り上げ、それらを抽象化して設計を考えてみることにしました。

今回の要件としては例えば以下のようなものがありました。

  • ページ A の訪問時、最初に訪問してから24時間経過していたらモーダルを表示する
  • パーツ B のクリック時、累計3回目のクリックであればモーダルを表示する

これらの要件を分解してみると「ページ表示やクリックなどのイベント」と「経過時間や回数などの表示条件」の2つの要素の組み合わせという形に抽象化できそうです。 このように分離しておくと、後々「パーツ B のクリック時、最初に訪問してから24時間経過していたらモーダルを表示する」のように組み合わせが異なる要件が追加されても柔軟に対応できそうです。

よって今回はイベント表示条件という2つのモジュール、そしてそれらを利用して Vue Component にモーダルの表示可否の判定や表示イベントの保存を行う手段を提供する Manager というモジュールの主に3つに分けて設計しました。

実装

Event

イベントに関するモジュール event.ts は以下のようになりました。

// ~/compositions/event.ts

export type Event = VisitEvent | ClickEvent

export type VisitEvent = {
  key: 'visit'
  page: string
}

export type ClickEvent = {
  key: 'click'
  page: string
  element: string
}

型を判定するための key やページやパーツの名前といった型定義のみを持っています。 Vue Component でイベントを定義するときや Web Storage に保存している過去のイベント情報を取得するときにこれらの型情報を参照します。

Condition

表示条件に関するモジュール condition.ts は以下のようになりました。

// ~/compositions/condition.ts

import { CustomDate, CustomDuration } from '~/foo/bar/date.ts' // 日付関連の型定義を持つ適当なファイル

export type Condition = TimeCondition | CountCondition

export type TimeCondition = {
  key: 'time'
  durationList: Array<CustomDuration>
}

export type CountCondition = {
  key: 'count'
  countList: Array<number>
}

type CurrentState = CurrentTimeState | CurrentCountState

type CurrentTimeState = {
  key: 'time'
  firstEventOccurred: CustomDate | null
  lastModalShowed: CustomDate | null
}

type CurrentCountState = {
  key: 'count'
  count: number
}

export function useCondition(condition: Condition) {
  const canShowUnderTimeCondition = (
    currentState: CurrentTimeState
  ): boolean => {
    // イベント発生からの経過時間からモーダル表示可能か判定するロジック
  }

  const canShowUnderCountCondition = (
    currentState: CurrentCountState
  ): boolean => {
    // イベントの発生回数からモーダル表示可能か判定するロジック
  }

  const canShow = (
    currentState: CurrentState
  ): boolean => {
    if (currentState.key === 'time') {
      return canShowUnderTimeCondition(currentState)
    } else if (currentState.key === 'count') {
      return canShowUnderCountCondition(currentState)
    }
  }

  return {
    canShow
  }
}

モーダルの表示条件に関する幾つかの型定義に加え、モーダルの表示可否を判定するロジック useCondition を持っています。 表示条件を定めて useCondition の引数として与えておき、 canShow の引数として現在の状態を与えると、これらの情報に基づいて表示可否を判定します。

モーダル表示可否を判定する具体的なアルゴリズムの実装もこれはこれで面白いのですが、冗長になってしまうので割愛します。

Manager

イベントと表示条件を利用して Vue Component にモーダルの表示可否の判定や表示イベントの保存を行う手段を提供するモジュール manager.ts は以下のようになりました。

// ~/compositions/manager.ts

import { useStorage } from '~/foo/bar/storage.ts' // Web Storage 関連の型定義やロジックを持つ適当なファイル
import { Event } from '~/compositions/event'
import { Condition, useCondition } from '~/compositions/condition'

export function useManager(event: Event, condition: Condition) {
  const storage = useStorage(event, condition)

  const canShow = (): boolean => {
    const currentState = storage.getCurrentState()
    return useCondition(condition).canShow(currentState)
  }

  const saveEvent = () => {
    storage.saveEvent()
  }

  return {
    canShow,
    saveEvent
  }
}

特筆すべき点はないですが、補足として useStorage はイベントの発生回数やモーダルの表示時刻などの情報を Web Storage から取得または保存する composition です。 メソッドの引数として渡された condition に応じて必要な情報を取得・保存する役割を持ちます。 詳細な説明は本筋から逸れるので割愛します。

Vue Component

モーダルを表示するページの Vue Component は以下のようになりました。

// pages/some-page.vue

<template>
  <div>
    <some-modal :can-show="canShow"></some-modal>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, reactive, ref } from '@vue/composition-api'
import { customDuration } from '~/foo/bar/date.ts'
import { VisitEvent } from '~/compositions/event'
import { TimeCondition } from '~/compositions/condition'
import { useManager } from '~/compositions/manager'
import SomeModal from '~/components/some-modal.vue'

export default defineComponent({
  components: {
    SomeModal
  },
  setup() {
    const manager = ref<ReturnType<typeof useManager> | null>(null)

    onMounted(() => {
      // イベント: とあるページの訪問
      const event: VisitEvent = {
        page: 'some',
      }

      // 表示条件: 最初のイベント発生から24時間または72時間経過後
      const condition = reactive<TimeCondition>({
        durationList: [
          customDuration(1, 'days'),
          customDuration(3, 'days')
        ]
      })

      manager.value = useManager(event, condition)
      manager.value.saveEvent() // ページ表示イベントの発生情報を保存
    })

    const canShow = computed(() => {
      return manager.value !== null ? manager.value.canShow() : false
    })

    return {
      canShow
    }
  }
})
</script>

「とあるページの訪問時、最初に訪問してから24時間または72時間経過していたらモーダルを表示する」という条件を表現するため、 useManager にはそれぞれ対応するイベント、発生条件のオブジェクトを設定しています。 そして computed を使って manager オブジェクトが生成されたタイミングでモーダルの表示可否を判定し、子コンポーネントのモーダルに props としてその情報を伝えます。

今回のイベントはページの表示なので、ライフサイクルフック onMounted を使って useManager を呼び出しました。 クリックイベントの場合は対象のコンポーネントの emit イベントに対してコールバック関数を定義し、その中で useManager を呼び出すように実装します。

このように useManager を利用して Vue Component 各所でモーダルの表示可否を制御できるようになりました!

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください! Nuxt.js や TypeScript に知見のある方は特に大歓迎です!!

www.wantedly.com

www.wantedly.com

「レバレッジ指向」という開発チームバリューについて

f:id:zacky2:20210126110429j:plain こんにちは、エンジニアの津崎です。

前回の記事で、「全員インフルエンサー」という開発チームのバリューについて紹介しました。 今回もその流れを汲んで開発チームのバリューの紹介をしていこうと思います。

開発チームのバリューについて

M&Aクラウドの開発チームには行動指針として以下の三つがあります。

このバリューは、今月(2021年1月)に開発チームによる合宿*1により策定されました。

今回は、「レバレッジ指向」というバリューについて紹介します。

レバレッジ指向」というバリューについて

レバレッジ指向」とは今回のバリュー策定で生まれた造語です。

レバレッジ」という言葉は、よくFXの話で、「レバレッジの倍率が〜」という使われ方すると思いますが、 このバリューでは、「小さい労力で大きな成果を出す」という意味として使っています。

スタートアップにおけるソフトウェア開発では、「価値を素早く届けること」と「スケールできること」を求められますが、このバランスはとても難しいです。 「価値を素早く届けること」を追いすぎると、技術的負債が増えて価値を素早く届けられなくなったり、「運用でカバー」的作業が増えて開発する時間を失ってしまいます。 一方で「スケールできること」を求めすぎると、機能開発するのに時間がかかり過ぎて商機を逃してしまいます。 このバランスはプロダクトの成長具合によって変化することが求められます。

弊社のプロダクトは、順調にユーザー数を伸ばしており、「スケールできること」を重視するフェーズに差し掛かってきました。 ユーザ増加だけでなく、エンジニアの増加や、プロダクトの複雑性の増大にも対応していく必要があります。 そんな背景があり、より長期的な目線を持って「レバレッジ」の効く選択をしようという方針が固まりました。

「指向」という言葉も議論がありました。 「しこう」には「指向」の他に、「思考」と「志向」がありますが、 「指向」は「方針の選択の局面でどちらの方向性でいくか」という判断の際に使いやすいワードであり、 「オブジェクト指向」といったエンジニアに馴染みのあるワードであるため選ばれました。

つまるところ、「レバレッジ指向」という言葉は、『長期的な目線を持って「レバレッジ」の効く方針を「指向」していこう。』という意味を表しています。

レバレッジ指向」の体現

レバレッジ指向」がバリューに設定されてからまだ日が浅いですが、早速レバレッジ指向を体現する開発を行っています。

コードによるコード生成

データに対応して単純なコードをたくさん作成しなくてはいけないケースにおいて、コードを生成するコードを実装することで対応しました。 コードによるコード生成を行うことで、今後、データに追加、削除があっても簡単に対応することができます。 少ない労力で大きな成果を出す、レバレッジ指向な実装ができたと思います。

この話はPHPer会議での登壇を予定していますので、そちらの方でお話しさせていただければと思います。

fortee.jp

NoCodeツールによる人力の排除

現在、開発チームではIssueの管理をGitHubからJiraへ移行している最中です。 必要なIssueだけJiraに移行するため、 GitHubのIssueに「Jira移行」とコメントすると自動でJiraに同じIssueが作成される仕組みをZapierを使って作りました。 こちらも人力作業を排除するレバレッジの効いた仕事といえます。

リリーススクリプトの改修

弊社のリリーススクリプトは最近になって特定条件でうまく動かない問題が発生していました。 運用でカバーできるものはなかなか根本修正されず、運用でカバーが常態化してしまうことはよくあるかと思います。 今回、「レバレッジ指向」がバリューに設定されたため、運用でカバーを排除する修正がすぐに行われました。

最後に

レバレッジ指向」を体現してプロダクトを爆伸びさせたいエンジニアを積極採用中です。 カジュアル面談もやっておりますので、お気軽にご連絡ください💪

www.wantedly.com

www.wantedly.com

*1:合宿とは名ばかりの泊まり込みのない長時間ミーティング