やわらかテック

業務を通して得られた知見、知見。個人的に試してみたこと、興味のあることについてアウトプットしています。

【API設計】jsonを返す時はできる限りフィールドのkeyを統一してあげよう

APIの仕様書が送られてきた

外部連携させて頂く企業様から、新規に追加されるAPIの仕様書を頂きました。ユーザーが登録しているカテゴリの総数と、その内訳を取得することが出来るAPIだそうで、新たに追加されたとのことです。しかしながら、蓋を開けてみると、何とも言えない残念なレスポンスとなっていました。まずは、以下をご確認ください。

(情報は原文より書き換えてあります)

以下はユーザーが何かしらのカテゴリーを1件以上登録している場合のレスポンスになります。

{
  "total_category": 2,
  "category": [
    {
      "id": 1,
      "name": "Python"
    },
    {
      "id": 2,
      "name": "JavaScript"
    }
  ]
}

そして、こちらがユーザーが1件もカテゴリーを登録していない場合のレスポンスです。

{
  "total_category": 0
}

お分かりいただけただろうか

カテゴリーの登録があるかどうかでjsonのレスポンスのフィールドのkeyの構造に違いがあります。1件でもカテゴリーの登録がされていれば、categoryというkeyが存在して、valueにはカテゴリー情報を持つオブジェクトの配列が入っています。しかしながら、1件もカテゴリーの登録がされていなければ、categoryというkeyが存在しないのです。

発生する2つの問題

実際には2つの問題があろうとも、動くコードを記述することは出来ます。ただ、それは対応としては複雑さを生み出し、保守性と品質を低下させます。APIのレスポンスだけを修正すれば済む問題を、呼び出し元でカバーしなければならないのです。

keyが存在しない場合の挙動

例えば、フロントエンドでユーザーが登録しているカテゴリーを一覧表示しようとした場合に以下のようなコードを書きます。

const generateCategoryDOM = () => {
  const resp = getCategory(); // 架空の関数
  const category = resp.category;
  return (
    <div>
      {category.map((detail) => {
        return (
          <p>{`ID: ${detail.id}`}</p>
          <p>{`Name: ${detail.name}`}</p>
        )
      })}
    </div>
  )
}

カテゴリーの登録が1件以上ある場合には正常に動作するでしょうが、カテゴリーが1件も登録されていない場合はどうでしょうか。const category = resp.category;の評価の結果、categoryにはundefinedが束縛されてしまいます。undefinedに対してmapを呼び出すことは出来ないため、このコードはエラーを引き起こします。

const obj = { "a": 1 };
const exist = obj.a;
console.log(exist); // 1

const notExist = obj.b;
console.log(notExist); // undefined

この問題を回避するためにコードに手を加えなければなりません。無駄なコードが増えてしまいました。

const category = resp.category || [];

「なぜcategoryが存在しない場合に空配列を代入してるんだろう?」ということをコードを見る度に考える必要がある可能性があるので、このような変更はできる限りしたくありませんが、仕方ないですね😭

型定義の煩雑化

またtypescriptでレスポンスの型を定義する場合にcategory?としなければなりません。毎回、categoryが存在するかどうかを確認するコードを書かないといけなくなる可能性があります。型定義も煩雑になってしまいました。

interface CategoryResp {
    total_category: number;
    category?: CategoryDetail;
}

interface CategoryDetail {
    id: number;
    name: string;
}

あるべき姿

カテゴリーの登録がない場合にでも、categoryというフィールドを返してくれれば、上記2つの問題は発生しません。値としては空配列が適切ですね。

{
  "total_category": 0,
  "category": []
}

カテゴリーの登録がない場合にcategoryというフィールドが返ってこないというのは、感覚としては戻り値をbooleanと定義している関数から、nilが返ってくるようなものです。実際に呼び出し元では今回、紹介した問題が発生します。他にも問題はあるかもしれません。

APIというのはプロトコル経由で呼び出せる、ただの関数だ」と考えると、戻り値の型を揃えるのはごく自然なことかと思います。プリミティブな型であれば関数の戻り値の型を統一するというのが自然だと思います(言語によっては関数の戻り値の型が不一致でエラーになっているはず👀)。
jsonであっても、可能な限りレスポンスの型を統一してあげるべきです。

APIの設計については、この書籍が大変役立っています。今回紹介したレスポンスに関するテーマは第3章で扱っていますのでご参考までに。

エンジニアがM1 MacbookAirを購入して2ヶ月使った感想

M1 Macbookを買うかどうか

4年前にずっと愛用していたWindowsからMacbookに乗り換えました。当時、アルバイトしていた開発会社がMacbookをメインに使っていたので、その流れに乗ったという感じです。早いものでMacbookを使い始めてから4年の歳月が流れておりました。元々、自分が使っていたMacbookは2016年のMacbookProでした。詳しいスペックについては省きますが、当時、なけなしのお金をかき集めて買った思い出ある一台です。

しかしながら、ある日、Macbookから唐突に火花が出ました。「ジュッ!」という音と共に、電源がつかなくなり、「あー、これは終わったな...」と思いAppleのカスタムサポートの方とやり取りをしました。

www.okb-shelf.work

どうやら機種が古いため、修理するのに5万ほどかかるとのことで、「新しい機種を買った方がいいですね」とアドバイスを頂きました。すでに5年前の機種となっていたため、思いきってMacbookを買い換えようと考えていました。

ProかAir

元々はMacbookProを使っていましたが、スペックはオーダーできる最低スペックのものでした(お金が無かった💰)。
当時は機械学習をやりたかったのと、「エンジニアならMacbookProでしょ」という先輩のアドバイスからMacbookProを使っていましたが、機械学習も実機ではやらなくなり、WEBアプリの開発がほとんどだったので、次に買うのは別にMacbookProでなくてもいいかもなぁとは思っていました。

M1 Macbookシリーズの発表

そんな時に丁度、M1チップを搭載したMacbookシリーズが発表されました。

www.apple.com

今まではIntelのCPUを搭載してきたMacbookですが、なんとApple社オリジナルのCPUを搭載してしまいました。結果的に金額はかなり安くなり、性能も大きく改善されているとAppleが公言しており、ベンチマークでも輝かしい結果を残しています。

f:id:takamizawa46:20210617074030p:plain

僕が使っていたMacbookProに搭載されていたCPUよりもスコアが高い👀

f:id:takamizawa46:20210617074427p:plain

www.cpubenchmark.net

「お、これはM1 Macありだなぁ」と考えておりましたが、当時、Twitterでは以下のような問題が確認されていました。

  • homebrewが使えない
  • Dockerが正式にサポートされていない(サポートされたとのことだったが、安定するかは不明)
  • プログラミング言語によってはM1対応がまだされていない
  • M1搭載されていない新型Macbookが近々、発表されるのではないか

特にDockerが使えないというのが、痛手でした。開発環境にDockerを用いるのが当たり前となっていたので、購入を思い留まらせる大きな要素です。ところがある日、Dockerの公式よりM1に対応したという内容がアップされました。

docs.docker.com

いいや!限界だッ!買うねッ!

まだ上記の他の問題ありましたが、この勢いにのってしまおうと流されるままにM1 Macbookを買うことにしました。最後にM1 MacbookAirとM1 MacbookProのどちらがいいかなと悩みましたが、パフォーマンス比較を確認したところ、M1 MacbookAirとM1 MacbookProでほとんど違いがないようです。合わせて、個人的にTouchBarがあまり好きではないので、TouchBarが搭載されていないM1 MacbookAirの方が良いと判断しました。

端末名 発売年 プロセッサ シングルコア マルチコア
MacBook Air(M1) 2020 Apple M1 1705 7220
MacBook Pro(13-inch, M1) 2020 Apple M1 1717 7602

corriente.top

とはいえメモリ8GBでは心許ないと思い、メモリだけは16GBにしました。グラフィックをいじったり機械学習をガンガン回すこともないので16GBで十分です。

購入してから二週間ほどしてM1を搭載したMacbookAirが我が家に到着しました🎉(写真を撮り忘れるorz)

2ヶ月使った感想 / 良かった点

使い方によって所感は変わってくると思うので、先に普段、僕がどのようにMacbookを使っているのかを記載しておきます。最も多いのはネットサーフィンです。不明点や疑問点を調べることが多いです。youtubeで動画を見ることもあります。 次に多いのはWEBアプリの開発、markdown形式でのブログ記事の執筆 & メモです。特に重い処理をやっているなと感じることはありません。どれも一般的なレベルの内容でしょう。

上記を踏まえてM1 MacbookAirを購入して良かった点から順に紹介していきます。

動作が圧倒的に早い

過去に使用していた2016年モデルのMacbookProと比べると圧倒的に早いです。 業務で使っている2019年のフルスペックのMacbookProにも負けない快適さを感じています。なんなら、「M1 MacbookAirの方が早いんじゃ...」と思っています。カクつく時間が圧倒的に少ないです。めちゃくちゃありがたい。 特定のソフトが軽いと感じるよりは平均的に全ての動作がスムーズになったという感じです。

今のところ、Macのローダー(虹色のぐるぐるするやつ)を一度も見ていません。

バッテリーの持ち時間が長い / めちゃくちゃ静か

購入したばかりなのでバイアスがあるのは承知ですが、バッテリーの持ちがかなり良いです。フル充電してから電源アダプターに接続せずに、8時間程度の作業をしてもバッテリーの充電残量が半分いくか、いかないかぐらいで残っています。Appleの公式を確認する限りは最大15時間程度は稼働できるとのことですので、間違っていないようです。

バッテリーと電源1 
- 最大15時間のワイヤレスインターネット
- 最大18時間のApple TVアプリのムービー再生
- 49.9Whリチウムポリマーバッテリー内蔵
- 30W USB-C電源アダプタ

support.apple.com

電源アダプターを持っていかなくても1日は問題なく作業が出来るでしょう。

また、M1 MacbookAirはファンレスでめちゃくちゃ静かです。雑音が一切ありません。MacbookProを使っていた時、動作がカクついた時などにファンが一気に音を立てるのが気になっていました。しかも一度、音が鳴り出すと数分間はずっとうるさいんですよね。ファンレスになって、雑音が完全になくなり、ストレスから開放されました。

「おいおい、ファンが無いって熱は大丈夫なのか」と思われるかもしれませんが、キータイプをしていて本体が熱いなと感じたことは今のところありません。今後、気温が高くなるので、気になることがあれば、こちらに追加しようと考えています。

(というか業務で使っている2019年モデルのMacbookProの方がファンあるのに本体が熱くなりやすいと感じています🤔)

Dockerが問題なく使える

当初はDockerが使えないという声をTwitterでよく見かけましたが、公式が発表した通り、Dockerを問題なく使えるようになりました。 以下の内容に従ってセットアップをするだけです。インストールも従来方法と変わりませんし、インストール作業中にトラブルは何も起きませんでした。

docs.docker.com

注意しなければいけない点としては、従来のM1対応されていないDockerでもM1 Macbookにインストールをすることが出来てしまいますので、インストールするべきバージョンを確認した方が良いです。一度、誤って従来バージョンをインストールしてしまいました。インストール後にM1だと使えないよ?というエラーが出てきた時は驚きました👀

またdocker-composeも問題なく使えます。

 $ docker-compose up
Docker Compose is now in the Docker CLI, try `docker compose up`

Starting next_demo_client_1 ... done
Attaching to next_demo_client_1
client_1  |
client_1  | > with-firebase-hosting@5.0.0 dev
client_1  | > next
client_1  |
client_1  | ready - started server on 0.0.0.0:3000, url: http://localhost:3000
client_1  | info  - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5
client_1  | event - compiled successfully

悪かった点

Node.js関連がフルサポートされていない

個人開発でフロントに、React.js, Next.jsをよく使っています。また、バックエンドをサーバー用意してAPIやらを構築するとお金がかかってしまうことが多いのでサーバーレスに使えるFirebaseを合わせてよく使っています。なのでNode.jsには大変お世話になっています。いつものノリでcreate-react-appを使って、新規のプロジェクトを作成してフロントを立ち上げようとしたら、以下のエラーが発生してしまいました。

<--- JS stacktrace --->
FATAL ERROR: wasm code commit Allocation failed - process out of memory
 1: 0x1030059d8 node::Abort() [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 2: 0x103005b58 node::errors::TryCatchScope::~TryCatchScope() [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 3: 0x10311f330 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 4: 0x10311f2c4 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 5: 0x1036b4cbc v8::internal::wasm::WasmCodeAllocator::AllocateForCodeInRegion(v8::internal::wasm::NativeModule*, unsigned long, v8::base::AddressRegion, v8::internal::wasm::WasmCodeAllocator::OptionalLock const&) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 6: 0x1036b5950 v8::internal::wasm::NativeModule::CreateEmptyJumpTableInRegion(int, v8::base::AddressRegion, v8::internal::wasm::WasmCodeAllocator::OptionalLock const&) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 7: 0x1036b4618 v8::internal::wasm::NativeModule::AddCodeSpace(v8::base::AddressRegion, v8::internal::wasm::WasmCodeAllocator::OptionalLock const&) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 8: 0x1036b5760 v8::internal::wasm::NativeModule::NativeModule(v8::internal::wasm::WasmEngine*, v8::internal::wasm::WasmFeatures const&, v8::internal::VirtualMemory, std::__1::shared_ptr<v8::internal::wasm::WasmModule const>, std::__1::shared_ptr<v8::internal::Counters>, std::__1::shared_ptr<v8::internal::wasm::NativeModule>*) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
 9: 0x1036b7cd4 v8::internal::wasm::WasmCodeManager::NewNativeModule(v8::internal::wasm::WasmEngine*, v8::internal::Isolate*, v8::internal::wasm::WasmFeatures const&, unsigned long, std::__1::shared_ptr<v8::internal::wasm::WasmModule const>) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
10: 0x1036c37e8 v8::internal::wasm::WasmEngine::NewNativeModule(v8::internal::Isolate*, v8::internal::wasm::WasmFeatures const&, std::__1::shared_ptr<v8::internal::wasm::WasmModule const>, unsigned long) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
11: 0x103692b34 v8::internal::wasm::AsyncCompileJob::CreateNativeModule(std::__1::shared_ptr<v8::internal::wasm::WasmModule const>, unsigned long) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
12: 0x103692cfc v8::internal::wasm::AsyncCompileJob::GetOrCreateNativeModule(std::__1::shared_ptr<v8::internal::wasm::WasmModule const>, unsigned long) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
13: 0x10369b360 v8::internal::wasm::AsyncCompileJob::PrepareAndStartCompile::RunInForeground(v8::internal::wasm::AsyncCompileJob*) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
14: 0x10369b12c v8::internal::wasm::AsyncCompileJob::CompileTask::RunInternal() [/Users/okb/.nodenv/versions/14.16.0/bin/node]
15: 0x103062c08 node::PerIsolatePlatformData::RunForegroundTask(std::__1::unique_ptr<v8::Task, std::__1::default_delete<v8::Task> >) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
16: 0x1030618a0 node::PerIsolatePlatformData::FlushForegroundTasksInternal() [/Users/okb/.nodenv/versions/14.16.0/bin/node]
17: 0x103818c00 uv__async_io [/Users/okb/.nodenv/versions/14.16.0/bin/node]
18: 0x10382a7c0 uv__io_poll [/Users/okb/.nodenv/versions/14.16.0/bin/node]
19: 0x103819090 uv_run [/Users/okb/.nodenv/versions/14.16.0/bin/node]
20: 0x10308de7c node::worker::Worker::Run() [/Users/okb/.nodenv/versions/14.16.0/bin/node]
21: 0x103090b90 node::worker::Worker::StartThread(v8::FunctionCallbackInfo<v8::Value> const&)::$_3::__invoke(void*) [/Users/okb/.nodenv/versions/14.16.0/bin/node]
22: 0x19534206c _pthread_start [/usr/lib/system/libsystem_pthread.dylib]
23: 0x19533cda0 thread_start [/usr/lib/system/libsystem_pthread.dylib]
Abort trap: 6

エラーを見てみると、メモリ関連でWASM(WebAssembly)を使っている箇所でエラーが出ているのでしょうか。

FATAL ERROR: wasm code commit Allocation failed - process out of memory
 1: 0x1030059d8 node::Abort() [/Users/okb/.nodenv/versions/14.16.0/bin/node]

詳しくない領域なので、何が原因で発生しているエラーなのかまでは特定できませんでしたが、M1 MacbookAirに買い替える前からNode.jsは14系を使っており、version管理にはnodenvを使っていました。なので、M1 Macbookに買い替えたことで発生したエラーということになります。

(Node.jsを14系にしているのはcloud functionの最新対応versionが14系だからです)

この問題は15系で公式に解消されたようです。また、合わせて14.17でも解消されたようですがパッチの配布元が公式ではなく個人とのことで、使用するかどうかは自己責任になります。

stackoverflow.com

zenn.dev

僕はDockerでcontainer内で実行させることで、この問題を回避しています。Docker最高。使えて本当に良かったです。

ドキュメントが更新されていない場合がある

よくインストールページにWindowsの方はこちら、MacOSの方はこちら...という記載があることがあります。しかし、サイトによってはM1 Macbookへのインストールを想定していないことがあり、従来のまま更新されていないことがほとんどです。そのため、MacOS環境へ提供されているインストーラーを使っても、上手くインストールが出来ないことがあります。

Dockerでこの問題に遭遇しました。今では、分かりやすく専用ページが作られて何をインストールすれば良いかが明確になりました。また、Elixirhaskellをインストールしようとした時にも同じ問題に遭遇した記憶があります。結局はどちらもbrew経由でインストールしました。 それでもダメな時はDockerでイメージをbuildしてあげれば何とかなるので、特定の言語が使えない!という状況は現在ではほとんど発生しておりません。

その分、Dockerfileだったりを用意したり、brewしか選択肢がないという状況が発生するので、嫌な人にはつらいかもしれません😅

キーボードの打感が軽すぎる

これは好みの問題なので、あまり言及するつもりはないのですが、元々使っていた2016年モデルのMacBookProがバタフライキーボードでした。M1 MacbookAirに搭載されているMagic keyboardの打感が軽すぎて、中々、慣れることが出来ません。とはいえ、外部キーボードを使えば何の問題もなかったりするので、それほど大きな問題ではないかなと思っています。

自分は付属のキーボードを使う派なので、少し気にしています。机が狭くなるのが嫌なのです🙇‍♂️

総評

M1 MacbookAirは間違いなく買って良かったと言えます。性能面でも快適さを感じますし、現状ではこれといって、大きな問題になっていることはありません。悪かった点でNode.jsがサポートされていないと書きましたが、現在の最新versionではサポートされているようです。M1への対応も各所でどんどん進められています。 とはいえ、情報量がまだ少ないというのも事実なので、購入される方は「問題が起きても何とかするぞ」という気持ちでいた方がいいかなと思います。

ありがとうM1 Macbook!買って良かった🎉🎉🎉

開発者が複雑だと思う機能はユーザーには使いこなせない

プロジェクトの肥大化

自分が参加しているプロジェクトが早いもので、スタートから2年が経ちました。
ありがたいことにお客様の数は増え続けており、今でも多くのお客様に使って頂いております。初期の頃と比べると、かなり機能がリッチになりました。 元々は、「シンプルさを保っていこう!」というKISSの原則に従い、誰が見ても初見で使えるアプリを目指していました。

意味するところは、設計の単純性(簡潔性)は成功への鍵だということと、不必要な複雑性は避けるべきだ

ja.wikipedia.org

こちらの書籍でもKISSの原則が紹介されています。

しかしながら、お客様が増えると様々な声が上がってきます。

  • 〇〇という機能が欲しい!
  • 既存のこの機能をもっとカスタマイズしてほしい!
  • 社内システムと連携したい!

など...
気がつけば、シンプルさを売りにしていたことを多くの人が忘れて、この提案された機能、仕様の実装に取り掛かってしまいます。自分はスクラムマスターなので、「いやいや、この機能はコンセプトからずれている」だとか「仕様が複雑すぎて使いこなせないよ」という話を提言したりします。

しかしながら、声の大きなお客様(大きな企業の...)に対しては自分の発言は無力な時があるのです。

欲しいと言われた複雑な機能を作った結果

どうしてもほしい!と言われたので対応をしました。
結果的には要望元のお客様が使いこなせない、設定を代行し、どの設定の時に、どういう動作をするのかを把握できる人物がほとんどいないという状態になりました。また、他のお客様には不要な機能であるため、使って頂けず、UIを複雑化させてしまいました。

使われていない AND 複雑なため取り除きたい機能ではありますが、それでも要望元のお客様が使っている以上、気軽に取り除くことも出来ないという最悪の状態になってしまいました。この複雑な機能を保守していかなければならないのです。

今回の教訓

実装時に開発者(エンジニア)から、「こんな複雑なもの使いこなせるんですかね」という声が上がっていました。自分もそう思います。しかしながら、上からの圧に負けて「GO!」の判断をしてしまいました。

「開発者が複雑だと思う機能は当然、ユーザーにも使いこなせないのだ」ということを改めて学びました。あるべき姿、ストーリーを把握している我々が、本当に必要かどうかを判断しないといけません。言わば、最終防衛ラインです。プロジェクトを間違った方向に向かわないようにコントロールしてあげなければいけません。

スクラムでは実際に作業する人たちが見積りをする。作業の量を見積もることが、自分たちの作業について考える良い機会にもなるんだ。

対策法

そのためにもプロジェクトがどういう方向に向かっているのか、強みは何か、ターゲットはどの層なのかをチームで再定義すると良いというアドバイスをメンターの方から頂きました。いわゆるインセプションデッキというものだそうです。

qiita.com

インセプションデッキを用意することで、「いや、その機能はコンセプトからずれてる」など発言がしやすくなりますし、インセプションデッキに記載されている以上、妥協することも難しくなるとのことです。

スクラムマスターとしてプロジェクトが正しい方向に向かうように導くのは簡単な事ではありませんが、今回の教訓を元に認識を改めようと思いました。