くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

要件定義で、すったもんだ

みんなのマーケットでUI/UXデザイナーをしているミソサクです。

入社5年目です。みんなのマーケットに来るまではアパレルEC業界でデザイナーをしていました。この記事にあるように、私は入社して1年くらいは自分の思い描くような活躍ができていませんでした。今回はそんなトライアンドエラーの日々の「失敗」をご紹介します。

本記事は、6/10に開催した「失敗に学べ!くらしのマーケットの開発「失敗」LT会 vol.1」でのLT内容を再構成したものです。

要件定義で、すったもんだ

はじまり

今から5年ほど前、入社してちょっとたったころ「カレンダー機能」というものを作る機会を頂きました。

メンバーはエンジニア2人とデザイナー1人の、計3人でした。要件定義は全員未経験でした。

そこで、デザイナーの私が人生で初めて要件定義をやる事になりました。 右も左も分からなかったので「まぁ、なんとかなるだろう」という感じでとにかく手を動かし始めました。

そしてすぐ、ものすごい困りました。

ものすごい困りました

何もかも不確定なまま手を動かしはじめてしまい、ものすごい困りました。たくさんある選択肢から「何を基準に決めたらいいのか」全くわかりませんでした。しかも「アプリをメインとした機能としたいのにアプリ開発者がいない」「使いたいデータが求めている形ではない」などいろんな問題が立ちふさがりました。

現状を無視してとにかく理想の形を描きましたが、いろんな問題が複雑に絡み合っていて、全てをうまくまとめるのはとても困難でした。

しかし「早く決めなきゃ」という焦りからその状態のまま手を動かし続け、それっぽいデザインを完成させました。焦りとは裏腹に1ヶ月もかかってしまいました。エンジニア2名にデザインを見せた所・・・

1年以上かかってしまいそう しかもこれじゃあ要件を満たしてないよ

考え直し。。。

というかもう、どうしたらいいか分からない。。。

完全に行き詰まりました。。。

転機

そんな時、一冊の本をすすめられました。

 「ユーザーストーリーマッピング」(Jeff Patton 著、長尾 高弘 訳 O’REILLY)

この本の内容をざっくり言うと「機能を全部ポストイットに書き出して、要求を満たす機能を見極めて、最小限でリリースしましょう」という事でした。例えば乗り物を開発する場合、時間をかけて自動車を作ってユーザーにいきなり提供するのではなく、まず最低限の移動手段であるスケボーを短時間で作って提供し、ユーザーの声を聴きながら機能を追加していき、最終的に自動車にすればいいという考え方でした。

やってみた

藁にもすがる思いで、エンジニア2人にも協力してもらい、全員で本の通りに機能を整理してみました。

「要求を満たす最低限の機能」という線引きをすることで以下の変化がありました。

  • 必須だと思い込んでいた機能は実際には必須ではないことが分かった

  • たくさんある選択肢の中から、本当に必要なものがどれなのか判断できた

  • 問題解決も含めた、開発の優先順位が見えるようになった

  • 初回リリースまでの時間が圧倒的に短くなった

また、ユーザーストーリーマッピングをすることで以下のメリットが生まれました。

  • チーム内で共有の言語、認識ができた

  • 全体像のうち、どこの開発をしているのか簡単に把握できた

  • チーム外のメンバーへの説明がしやすくなった

できる人がいなくてどうにもならなかった「アプリ開発」は、後でできる人が入社したら追加開発するという前提で「まずは基盤となる仕組みを作る」という判断ができました。そして無事、後から入社したアプリエンジニアと機能を作りあげました。

以上、はじめての要件定義の失敗談でした!

最後に

当たり前なんですが、やったことがないことに挑戦するときは、素直に本などで勉強すべきだと痛感しました。そしてユーザーストーリーマッピングは開発だけじゃなく「本当に必要なモノはなんなのかを整理する」という点で、いろんな事に活用できる手法なので、非常におすすめです。

QAのお仕事紹介

はじめまして。QAエンジニアのザキです。

ここ数年、QAってなに?QAエンジニアの仕事って?、と聞かれる機会が増えました。 それだけ興味・関心が高まっていると思いつつ、認知度については業界を通じた課題があるのかなと一喜一憂の日々です。

今日は簡単ながら、みんなのマーケットのQAについて、お話したいと思います。

まずは自己紹介

f:id:curama-tech:20200725163337p:plain

自己紹介も無事?に済んだところで、早速QAのお仕事紹介スタートです!

QAの仕事って何ですか?

テスト設計(テストケースの作成)とテスト実施が業務のメインです。 テスト作業と端的に言っても、仕様要件や影響箇所の把握、テストスケジュールの計画、リソース調整など様々あります。

新しい機能のリリース、既存の機能の修正も、それらが組み込まれる事によって他の機能へ影響が無いか、開発予定の内容詳細に抜け漏れの観点が無いか、内部的なオペレーション(運用面)は整備されている・混乱しないか、開発プロセスは守られているのか等。

その他、社内メンバーからの仕様や不具合の問い合わせを一次受付しています。 問い合わせをQAで切り分けた後に、不具合となった場合はエンジニアへ対応依頼を行います。 この不具合修正はQAテストを経てリリースされ、リリースまでの期間はQAで不具合管理を行います。

作業自体はテストに括られますが、意識しなければならない事はテスト外にも及びます。 プロダクトが成長し大きくなるにつれ、守備範囲は増えます。大変ですが、それもQAの醍醐味かなと思います。

どれくらいの件数をテストしますか?

月ごとに変動しますが、1人あたり毎月約500件〜1,000件のテストケースを消化します。 不具合やフィードバックが無ければ、この件数で終了しますが、再テストや切り分け調査なども入るので、それで考えると1人あたり毎月700〜1,200件程度になるのではないでしょうか。

f:id:curama-tech:20200725154942j:plain

メンバーは何名ですか?

今現在は2名です。2人とも宮崎オフィスのメンバーです。 (メンバー募集中です、是非ご応募お待ちしております)

テストって地味なイメージですが?

確かに、見た目に派手な仕事ではありませんが、見方を変えるとQAテストがリリース前の最終局面になります。 我々のテストを通ったものが世の中にリリースされる、そう考えると不思議と辛さよりも使命感に燃えます。 地味に見えても大事なポジションに変わりなく、逆にそれを担っているからこその誇りもあります。

プロダクトがより多くの人に安心して利用され、より一層好まれるよう、最後の一瞬まで気を抜かずに粛々とテストを行います。 精神論ではなく、そういう気持ちでテスト出来る人がQAであるべきかな(向いている)と思っています。 とは言え、リリース期限ギリギリの状況で「わぁぁぁ」となる事もありますよ、人間なので ^^;

f:id:curama-tech:20200725155217j:plain

お伝えしたい事やお話したい事はたくさんありますが、今日のところはこの辺で。 今後も定期的に記事を書いていきますので、乞うご期待ください (。・・。)

読みやすいコードを書く必殺技

こんにちは、バックエンドエンジニアのカーキです。 今日は自分が思う読みやすいコードの秘密を紹介したいと思います。 いきなり答えを言っちゃいますが、読みやすいコードを書く必殺技はコードの抽象度を揃えることです。

抽象度とは

抽象度というのはものを見る時の視野の広さや高さのことです。

例えば、「富士山はどこにあるの?」という質問の答えは 地球 --> アジア --> 日本 --> 静岡県 のどちらでも答えられると思います。 この例では 地球 が一番高い抽象度で、静岡県 が一番低い抽象度です。私達は普段質問の文脈によってどの抽象度で答えるかを無意識で選んでると思います。

以下のシナリオを想像してみましょう。 僕個人的にマックが大好きなのでマックで注文する時のシチュエーションにします。起きることはだいたいこんな感じでまとめられると思います。

f:id:curama-tech:20200722115146p:plain
抽象度

👆の画像のように注文するアクションを高い抽象度(左側)から低い抽象度(右側)にわけられます (もちろん記載されてる以外の分け方も全然あると思います)。

一番左に寄せられてるマックで注文するが抽象度マックスのアクションで、財布を出すレベルのものは一番低い抽象度にいることがわかると思います。抽象度が違うものを比較してみると、抽象度が高いものは遠くからの目線で見られて、低いものは比較的に近い目線から見られてるのがイメージできると思います。

抽象度の違いをイメージできたところで、コードを読みやすくすることと抽象度はどう関係あるのかについて深堀したいと思います。

コード例

早速ですが、また例から始めます。今回は上記のマックで注文した時のキッチンの店員さんの動きをシミュレーションしてみましょう。 注文は ソーセージエッグマフィンセット、飲み物は ホットコーヒー 前提で書いています。 本来ならOOPでちゃんと書ける部分いっぱいあると思いますが、抽象度の説明をシンプルにするためほぼ疑似コード程度で書いていますのでご了承ください

抽象度がバラバラのコードが混ざってる場合

// メインの関数 (一番高い抽象度)
function prepareSausageEggMuffinSet(): SausageEggMuffinSet { 
    let continueToastingMuffin = true;
    let muffin = new EnglishMuffin();
    while (continueToastingMuffin) {
        muffin = toastMuffin();
        if (muffin.color) === "GoldenBrown" {
            continueToastingMuffin = false;
        }
    }
    
    let sausage = new Sausage();
    addSaltAndPepper(sausage);
    sausage = makeRoundAndFlat(sausage);
    while (!sausage.isCooked) {
        cook(sausage);
    }
    let egg = new Egg();
    while (!egg.isCooked) {
        cook(egg);
    }
    const cheeseSlice = new CheeseSlice();
    const sausageEggMuffin = new SausageEggMuffin(
        sausage, 
        egg, 
        muffin, 
        cheese
    );
    
    const potato = new SlicedPotato();
    const eggForHashedPotato = new Egg();
    const seasonings = [new Salt(), new Pepper()];
    let hashedPotato = new HashedPotato(
        potato, 
        eggforHashedPotato, 
        seasonings
    );
    let continueCookingHashedPotato = true;
    while (continueCookingHashedPotato) {
        hashedPotato = cook(hashedPotato);
        if (hashedPotato.color) === "Golden" {
            continueCookingHashedPotato = false;
        }
    }
    const coffee = CoffeeMaker.brew();
    return new SausageEggMuffinSet(muffin, hashedPotato, coffee);
}

上記のコードを一度読んで見てください。抽象度バラバラのコードが同じところに混ざってると思います。一番高い抽象度の prepareSausageEggMuffinSet の実装に中レベルの toastMuffin もいれば、すごく抽象度低レベルのマフィンの色の比較も入っています。コレによって prepareSausageEggMuffinSetのコードが長くなり、読みにくくなってると思います。

抽象度を揃えた場合

まずは prepareSausageEggMuffinSet だけ最初に揃えて見ましょう。単純にソーセージエッグマフィンセット を作るためには ソーセージエッグマフィンハッシュポテトコーヒー が必要なので、 それぞれを作る処理を prepareSausageEggMuffin, prepareHashedPotato, prepareCoffee 関数で表します 。この一階層の抽象化を挟むことで ソーセージエッグマフィンを作るハッシュポテトを作るコーヒーを作る アクションそれぞれが独立化され、疎結合になっていると思います。

実際のコードはこんな感じになります。明らかにこっちのコードの方が読みやすいかと思いますがいかがでしょうか?

// メインの関数 (一番高い抽象度)
function prepareSausageEggMuffinSet(): SausageEggMuffinSet { 
    // 中で呼ばれてる全関数がprepareSausageEggMuffinSetより一段階低いレベルの抽象度
    const sausageEggMuffin = prepareSausageEggMuffin();          
    const hashedPotato = prepareHashedPotato();           
    const coffee = prepareCoffee();                 
    return new SausageEggMuffinSet(
        muffin, 
        hashedPotato, 
        coffee
    )
}

これだけだと ソーセージエッグマフィンセットは作られないので次は prepareSausageEggMuffin, prepareHashedPotato, prepareCoffee それぞれの実装をしてみましょう。 残りのコードはこんな感じになります。

function prepareSausageEggMuffin (): SausageEggMuffin {
    const muffin = toastMuffin();             
    const egg = cookEgg();
    const sausage = cookSausage();
    const cheeseSlice = new CheeseSlice();
    return new SausageEggMuffin(sausage, egg, muffin, cheese);
}


function prepareHashedPotato(): HashedPotato {
    const potato = new SlicedPotato();
    const eggForHashedPotato = new Egg();
    const seasonings = [new Salt(), new Pepper()];
    let hashedPotato = new HashedPotato(
        potato, 
        eggforHashedPotato, 
        seasonings
    );
    while (!hashedPotato.isCooked) {
        cook(hashedPotato);
    }
    return hashedPotato;
}

function prepareCoffee(): Coffee {
    const coffee = CoffeeMaker.brew();
    return coffee;
} 


function toastMuffin(): EnglishMuffin {
    let muffin = new EnglishMuffin();
    while (!muffin.isPerfeectlyToasted) {
        toast(muffin)
    }
    return muffin;
}

function cookEgg(): Egg {
    let egg = new Egg();
    while (!egg.isCooked) {
        cook(egg);
    }
    return egg;
}

function cookSausage(): Sausage {
    let sausage = new Sausage();
    addSaltAndPepper(sausage);
    sausage = makeRoundAndFlat(sausage);
    while (!sausage.isCooked) {
        cook(sausage);
    }
    return sausage;
}

結論

抽象度を揃えた場合は明らかに抽象度バラバラで書いた場合よりコードが読みやすくなっていて、各関数の役割もしっかりしてることがわかるかと思います。 上記の例から言いますと、抽象度バラバラの場合一目でオーダー入った時にどんなことをやってるかはわかりづらいと思います。高抽象度の関数 (prepareSausageEggMuffinSet)にいきなり低抽象度のコード (ソーセージや卵の具体的な作り方など) が書かれてるので prepareSausageEggMuffinSet が複数の理由で変わる可能性が高まっています (コーヒーの作り方が変わっても変わりますし、ソーセージの味付けが変わっても変わります)。一方で、抽象度揃えてる例の場合 「ソーセージエッグマフィン作って、ハッシュポテト作って、コーヒー作ってまとめて返す」とすぐにわかるようになってるかと思います。具体的「こうやってコーヒー作る」とか「こういう味付けする」など prepareSausageEggMuffinSet に書かれていないのでコーヒーの作り方やソーセージの味付けが変わったとしても prepareSausageEggMuffinSet へ影響がないようになっています。

当記事は「コードを読みやすくする」視点で書きましたが、上記のように抽象化するメリットは読みやすさだけではなく様々あると思います:

  1. 責任の制限: 抽象度を揃えることで責任の線がハッキリするので、Godクラスが生まれにくくなります。
  2. テスタビリティ: 抽象度を揃えることでコードがモジュール化され、単体テストしやすくなります。
  3. 変更への耐えやすさ・拡張性: 抽象度揃った場合自然とコードがある程度疎結合になっているので既存機能の変更や拡張はしやすくなります。 例えば、記載の例でソーセージの味付けを変えたい時に、抽象度揃えてない例の場合関係ないところ (例えばコーヒーの作り方) にも影響してしまう可能性があります。抽象度を揃えた例だと、メインの関数 prepareSausageEggMuffinSet に変更無し、prepareSausageEggMuffin 自体にも変更無しで、cookSausage の変更だけで済みます。

最後に

よくオブジェクト指向プログラミングでSOLIDという単語が出てくると思いますが、個人的にはSOLIDを意識してコードを書くより、抽象度を意識してコードを書いた方が自然とSOLIDに従うコードになるんじゃないかと思っています。なので、今度設計する時や同僚のコードレビューする時に抽象度にフォーカスしてみてください、新たな発見があるかも知れません!

SQL を一定間隔で実行する

夜ご飯んンンンンンンンンンンンンンンッ!食べれないやつ😐
夜ご飯んンンンンンンンンンンンンンンッ!食べれないやつ😐

こんにちは、バックエンドエンジニアの @akira です。
データを自動で更新するなど、 SQL を一定間隔で実行したいケースがあり、今回はその方法を調べてみました。

前提条件は以下です。

  • PostgreSQL 9.3~
  • PGPASSWORDhost, port, username, dbname は省略
  • 以下の SQL を実行
update "Fruits" set updated_at = now() where id = '123';

bash

watch -n n COMMAND で COMMAND を n 秒ごとに実行できます。

COMMAND 部分はダブルクオートで囲むこともできます。
watch -n 10 "ls -l" # 10秒ごとに ls -l を実行

上述のクエリを実行する場合は次のようになります。

watch -n 60 psql -c "update \"Fruits\" set updated_at = now() where id = '123';"

エスケープがちょっと辛いですね。
watch の COMMAND 部分をダブルクオートで囲むと、さらに大変になりそうです。

psql

PostgreSQL 9.3 から \watch メタコマンドが追加されました。12 系でも使えます。

参考:\watch [ seconds ]

QUERY; \watch n で QUERY を n 秒ごとに実行できます。
先ほどのクエリを実行する場合は次のようになります。

update "Fruits" set updated_at = now() where id = '123'; \watch 60

エスケープ不要で、可読性も高いですね。

おまけ:redis-cli

redis-cli -r n -i s COMMAND で COMMAND を s 秒間隔で n 回実行できるようです。
-i を指定しない場合は即時実行のようですね。

参考:Continuously run the same command

さいごに

MySQL も少し調べてみたのですが、Event Scheduler で実行する方法しかヒットしませんでした(他にやり方をご存知でしたら是非教えてください🙏)。

「何かを実現したい」という意思がないと、上記のやり方を知る機会はなかったのかなと思っています(「30 分に一度、手動でコマンドを実行すればいいや」では決して調べることはなかったでしょう)。

業務でこのような機会を逃さずに、一つ一つ積み上げていきたいですね。

私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています。
ぜひ コーポレートサイト までお気軽にご連絡ください!

pip install Flask-Injector ができない! を解消した

f:id:curama-tech:20200713191723p:plain

こんにちは。バックエンドエンジニア / SRE のまのめです。
先日、弊社のテスト環境「 tako 環境 」が焼けなくなり(*)、それについて調査・解決したことをまとめました。

* tako 環境を焼く = 社内用語で、テスト環境を作成/更新すること

状況

python3.5 で動くアプリケーションの tako 環境 を焼く際、 ansible のタスクが失敗するようになりました。

ansible のログを追うと、以下のようなエラーが出力されていました。

ERROR: You need Python 2.7 or 3.4 to install the typing package.

----------------------------------------

:stderr: Command \"python setup.py egg_info\" failed with error code 1 in /tmp/pip-buikd-XXXXXX/typing
You are using pip version 7.1.2, however version 20.1.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

複数回、複数環境で試したのですが、どうやら Flask-Injector の pip install ができなくなっており、 typing というモジュールの install でコケているようでした。

原因

先程も述べたように、アプリケーションは python3.5 系で動いています。
このアプリケーションで Flask-Injector を利用していますが、ある程度歴史もあるため若干バージョンが古めでした。

また、この現象が起きたのが 2020 年 7 月 10 日 で、 typing モジュールはその日にアップデートがありました。
ここで、typing モジュールの このときの最新版(3.7.4.2)の Project description に新しくこんな注意が記載されていました。

For package maintainers, it is preferred to use typing;python_version<"3.5" if your package requires it to support earlier Python versions. This will avoid shadowing the stdlib typing module when your package is installed via pip install -t . on Python 3.5 or later.

typing - pypl

さらに、 Flask-Injector の該当バージョンのモジュールの requirements を見たら、以下のようになっていました。

install_requires=['Flask', 'injector>=0.10.0', 'typing'],

flask_injector/setup.py at v0.10.1

はい、 Flask-Injector のこのバージョンでは、 typing のバージョンが固定されていませんね。

つまり、

Flask-Injectortyping のバージョンが固定されていないために最新版が入る
=> typing の最新版は python3.5 で入れられなくなった
=> pip install がコケる

ということだったのです。

そして解決へ

typing のバージョンをこちらが定義する requirements で無理やり固定してもよいのですが、ほかのモジュールでまたこういう事があると面倒だな…と思っていました。 そこで試しに、 pip install --upgrade pip setuptools をしたら、直りました。

このとき急いでいたこともあり、なぜそれで解決するのかを詳しく調べませんでしたが、おそらく setuptools がいい感じに働いてくれるのだと予想されます。 なので、 pip install する直前に setuptools をアップグレードするように ansible のタスクを修正し、 tako 環境 は無事直りました。

感想

こういったモジュールが古くて起こる不具合には、今回のようにその場しのぎな解決ではなく、根本的な解決であるモジュールのアップグレードで対応したいです。
しかし、緊急性が高い場合はひとまず今回のように処理し、時間をかけてモジュールのアップグレードの検証を進めて、根本解決できるといいなと思いました。

サービスが大きくなっていくとなかなかモジュールのバージョンにまで目を向けられなくなっていきますが、こういうことが本番で起きる前に、定期的に振り返る時間を作っておきたいですね。