PokeAPIでポケモンGET!

自転車にまたがるとマッハ自転車のBGMが頭の中で流れ出します。岡村です。

今回は

  • PokeAPI
  • GoogleAppsScript
  • CloudFunctions
  • スプレッドシート

の4つを使ってポケモンGET機能を作ってみたいと思います。

やりたいこと

GoogleAppsScript上で関数を実行すると
スプレッドシートにポケモンのデータを含む新しい行が挿入されること。

それではやってみましょう!

PokeAPI

PokeAPIとはポケモンに関するデータを取得することができる
オープンソースのAPIです。

https://pokeapi.co/

今回取得したいデータは

  • 図鑑ナンバー
  • 名前
  • 画像URL
  • 解説

です。
アクセスするエンドポイントは

https://pokeapi.co/api/v2/pokemon-species/{id or name}/

です。
https://pokeapi.co/api/v2/pokemon/{id or name}/の方が
いろいろな種類のデータが取れるのですが、
日本語解説がpokemon-speciesの方にしかなかったので。

スプレッドシート

GETしたポケモンを格納するための
pokemonsシートを作っておきます。

列の定義は以下です。

  • A列:図鑑ナンバー
  • B列:名前
  • C列:画像URL
  • D列:解説

CloudFunctions

PokeAPIにポケモンデータ取得のリクエストを行います。
GoogleAppsScriptから行ったところ、
IPアドレスがブロックされたのか、
1回目以降データが取れなくなってしまいました。

People not complying with the fair use policy will have their IP address permanently banned.

公式ドキュメントFair Use Policyより引用

ですので前回同様PokeAPIへのリクエスト部分のみ、
CloudFunctionsを使って実装します。

ソースコードは以下のようにしました。

const fetch = require('node-fetch');
const functions = require('@google-cloud/functions-framework');

const END_POINT_POKEMON = 'https://pokeapi.co/api/v2/pokemon-species/';

/**
 * PokeAPIにポケモン取得をリクエストします。
 * 
 * 取得できた場合は、jsonオブジェクトを返します。ステータスは200です。
 * エラーが発生した場合は、空のjsonオブジェクトを返します。ステータスは500です。
 *
 * @param {Object} request クライアントからのリクエストに関する機能を提供するオブジェクト。
 * @param {Object} response クラアントへのレスポンスに関する機能を提供する提供するオブジェクト。
 */
functions.http('getPokemon', async (request, response) => {

  let returnJson = new Object();

  try {
    const url = END_POINT_POKEMON + request.body['id'];
    console.log('url:' + url);

    let responseObject = new Object(); 

    try {
      responseObject = await fetch(url); 
      returnJson = await responseObject.json();
      response.status(200);

    } catch(error) {
      throw new Error('PokeAPIへのリクエスト中にエラーが発生しました。error.message:' + error.message);
    }

  } catch(error) {
      console.error('エラーが発生しました。error:');
      console.error(error.stack);

      response.status(500);
  }

  console.log('returnJson:');
  console.log(returnJson);

  response.send(returnJson);

  return;
});

GoogleAppsScript

ソースコードは以下のようにしました。(DDD学習中🔥)

main関数からPokemonServiceクラスgetNewPokemon関数を呼び出します。



function main() {

  try {
    new PokemonService().getNewPokemon();
    
  } catch(error) {
    const errorMessage = 'エラーが発生しました。error:' + error.stack;
    console.error(errorMessage);
  }
}

getNewPokemon関数からPokeApiClientServiceクラスのrequestToPokeApi関数を呼び出します。

/** ポケモン機能のサービスクラスです。*/ 
class PokemonService {
  
  /** コンストラクタ。*/ 
  constructor() {
  
    /** 全国図鑑ナンバー最小値。*/ 
    this.minId = 1; 

    /** 全国図鑑ナンバー最大値。*/ 
    this.maxId = 1025; 
  }

  /** 
   * 新しいポケモンをGETし、スプレッドシートに追加します。 
   */ 
  getNewPokemon() {

    const id = Math.floor(Math.random() * (this.maxId + 1 - this.minId)) + this.minId;
    console.log('GETするポケモンの図鑑ナンバー:' + id);

    const response = new PokeApiClientService().requestToPokeApi(id);
    const pokemon = new Pokemon(response);
    new PokemonRepository().insertPokemon(pokemon);
  }
}
/** PokeApiのAPIクライアントサービスクラスです。*/ 
class PokeApiClientService {

  /** 
   * CloudFunctions経由でPokeApiへポケモン取得のリクエストを行います。 
   * @param id 取得するポケモンの図鑑ナンバー。
   * @return レスポンス。
   */ 
  requestToPokeApi(id) {

    // ご自身のCloudFunctionsのトリガーとなるURLを入力してください!
    const endPointGetPokemon = 'example.com';

    try {
      const headers = {
        "Authorization": `Bearer ${ScriptApp.getIdentityToken()}`
      }

      const params = {
        "id": id
      }

      const options = {
        "method": "POST",
        "headers": headers,
        "contentType": "application/json",
        "payload": JSON.stringify(params),
      }
    
      return UrlFetchApp.fetch(endPointGetPokemon, options);

    } catch (error) {
      throw error;
    }
  }
}

endPointGetPokemonにはご自身のCloudFunctionsのトリガーとなるURLを入力してください!
getNewPokemon関数にはCloudFunctionsがPokeApiを叩いたレスポンスが返ってきます。

レスポンスをPokemonクラスのコンストラクタに渡します。

/** ポケモンのEntityクラスです。*/ 
class Pokemon {

  /** 
   * コンストラクタ。
   * @param PokeApiからのレスポンス。
   */ 
  constructor(responseFromPokeApi) {

    const responseObject = JSON.parse(responseFromPokeApi.getContentText());

    const id = responseObject['id'];
    const name = responseObject.names.find(name => name.language.name === "ja-Hrkt").name;

    // 日本語解説がない場合は英語解説
    let text = '';
    try {
      text = responseObject.flavor_text_entries.find(entry => {
        return entry.language.name === "ja-Hrkt";
      }).flavor_text;

    } catch {
      // undefined
      text = responseObject.flavor_text_entries.find(entry => {
        return entry.language.name === "en";
      }).flavor_text;
    }

    /** 全国図鑑ナンバー。 */ 
    this.id = id;

    /** ポケモン名。 */ 
    this.name = name;

    /** 画像URL。 */ 
    this.imgUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/'+ this.id +'.png';

    /** 解説。 */ 
    this.text = text;
  }
}

図鑑ナンバー、名前、画像URL、解説をフィールドに持つエンティティとします。
スカーレット・バイオレットのポケモンたちは
まだ日本語解説が実装されてなかったので
日本語解説がなかった場合は英語解説をレスポンスから取り出します。

PokemonインスタンスをgetNewPokemon関数へ返し、
PokemonRepositoryクラスinsertPokemon関数へ渡します。

/** ポケモンのRepositoryクラスです。*/ 
class PokemonRepository {

  /** コンストラクタ。*/ 
  constructor() {

    this.scriptProperties = PropertiesService.getScriptProperties();
    const activeSpreadSheet = SpreadsheetApp.getActiveSpreadsheet();

    // pokemonsシートを取得する
    this.pokemonsSheet = activeSpreadSheet.getSheetByName('pokemons');
  }

  /** 
   * ポケモンを挿入します。
   * @param pokemon ポケモン。
   */
  insertPokemon(pokemon) {
    const pokemonValues = Object.values(pokemon);
    this.pokemonsSheet.appendRow(pokemonValues);
  }
}

ポケモンデータをスプレッドシートの最終行へ挿入します。

では、ソースが書けましたら
実行!

実行結果

スプレッドシートにポケモンが追加されてますね!

あとがき

ポケモンはGETできましたでしょうか?
私はこの機能をdiscordのbotに組み込んで
みんなでポケモン図鑑を完成させるゲームにしました。
友人が今日の積み上げこと「きょうつみ」を投稿したら1ポイント。
5ポイントが溜まったらポケモンGET!
図鑑完成度を100%にしよう!★

ポケモンといえば最近?
推し声優がパモの声をやってるらしい!!
という不純な?理由で?
パモが好きかもしれない。かなり。

また何か作ったら共有します!

入社のご挨拶

はじめまして。中途入社の小松﨑と申します。

前職では学校法人や保険会社のバッグオフィスなど業務システムの改修を主に行っておりました。

かねてよりEC業界に興味を持っておりまして、グリニッジの提供するサービスであれば、
より多くのお客様に喜びを提供できると考え、この度入社する運びとなりました。

趣味はインドア中心で映画観賞、漫画鑑賞、たまに旅行に行きます。

映画は洋画を中心にさまざまなジャンルの作品を見ています。入社後、他のスタッフの方に勧められ
「シックスセンス」や「SE7EN」を見ました。
MCUをドラマを含めて網羅し続けていることが自慢です。

漫画は王道アクション系も好きですが、最近は「あかね囃」という落語の漫画を読んでいます。
いつか寄席にも行ってみたいです。

これまでに培ってきた経験を生かして、グリニッジのサービスをよりよいものにできるよう、
精進してまいります。
どうぞよろしくお願いいたします。

Thonnyのすすめ

こんにちは、窪田です。

プログラミングを始めてみようと思っている方にとって、障壁となるのはどのようなものでしょうか。

やってみたいけど難しそう、何から始めれば良いのかわからないなど、様々な要因があると思います。

なんとなくですが、心理的なハードルの高さとプログラムを書くまでの準備が難しいため、躊躇してしまっている人が多いような気がしています。

今回はプログラミングを始めてみようと思っている方に、おすすめの開発環境を紹介したいと思います。

紹介するのは Thonny というソフトです!

Thonnyとは何か

Thonnyとは、Pythonというプログラミング言語の初心者向けのIDE(開発環境)です。

Pythonは機械学習やデータ分析に強みを持つ汎用性の高いプログラミング言語で、コードの記述が短くシンプルなため初心者にも優しい言語だと思います。

ThonnyをPCにインストールするだけで、Pythonのプログラムを書くことと書いたコードを実行することができます。

公式サイト:https://thonny.org

Thonnyのインストール方法

Thonnyをインストールする方法について解説していきます。

公式サイト にアクセスし、画像赤枠のダウンロードリンクからPCのOSに合わせたインストーラーをダウンロードします。

ダウンロードしたインストーラーを起動すると、インストールモード選択画面が出てくるので「Install for me only (recommended)」をクリック

次の表示では「Next」をクリック

ライセンス同意画面に表示される文章を確認し、「I accept the agreement」にチェックを入れた後に「Next」をクリック

インストールするフォルダーを選択する画面で、インストールする場所を確認し、問題なければ「Next」をクリック

スタートメニューに表示される名前を確認し、問題なければ「Next」をクリック

デスクトップにショートカットアイコンを追加したい方は「Create desktop icon」にチェックを入れ、「Next」をクリック

今までの項目の確認画面が表示されるので、「Install」をクリック

以下の画像と同じように完了画面が表示されれば、Thonnyのインストールが完了です。

Thonnyの使い方

次はThonnyを起動し、実際にプログラムを書いてみましょう。

起動すると、以下の画像のような画面が表示されると思います。

画像赤枠の部分にPythonのコードを記述し、実行した結果が画像青枠の部分に表示されます。

今回はPythonを使って「Hello world!」と出力させたいので、print('Hello world!')と記述し、test.pyというファイル名で保存しました。

画像赤枠のボタンを押すと記述したPythonのコードが実行されます。

実行した結果として「Hello world!」が表示できました。

文字を出力するだけだと味気ないので、もう少し実用的な例として税込みの金額を求める処理を書いてみました。

金額として2024を入力すると、税込み金額の2226円が正しく出力できました。

まとめ

今回はプログラミング初心者でも簡単にPythonの開発ができるThonnyというソフトを紹介してみました。

Thonnyは簡単に使うことができるので、プログラミングを始めてみようと思っているけど躊躇している方はぜひ試してみてください。


ちなみにThonnyの発音がトニーなのか、ソニーなのか、わからないです。

個人的にはPython(パイソン)の開発環境なので、ソニーだと思っていますがどうなんでしょう。

慶應義塾大学大学院経営管理研究科(KBS)Executive MBA(EMBA)プログラム

今年からKBSのEMBAプログラムで学んでいるので活動を紹介します。
仕事をしながら大変な面もありますが、いくつになっても学校に行くというのは楽しいですね。

クラスのゴルフ部でラウンド

アントレプレナーシップ授業は活気があります。

LinkedInオフィスでLinkedIn活用について教えていただきました。

著名な海外教授によるグローバルな授業

スリランカでのビジネス可能性を現地で調査、1週間かけてスリランカ現地を周りました。

スリランカ現地メディアでも活動が取り上げられました。

学生所属企業一覧に当社グリニッジ株式会社も掲載されています。
https://www.kbs.keio.ac.jp/graduate/emba/dbook/2025/?pNo=1

EMBAプログラム詳細はこちら
https://www.kbs.keio.ac.jp/graduate/emba/index.html

Node.jsでサーバー動かしていたら時々止まる!?

Node.jsはイベントループで動作していて、それはシングルスレッドで動作する、async/awaitやpromise使って並列で動いているように見える・・・というのは、「node シングルスレッド」でググったらいろいろな先人達がブログを残してくれています。

そこで今回は、そういう動きをするということはわかるのだけど、実際そこを意識しないプログラムを作ってしまうと実運用システムにおいてどんな問題が起こるのか?について書きたいと思います。

今回は、24時間運用していて普通に動いていたNode.jsを使って作られたWebサーバー(ここではexpressサーバー)が突然応答なしになる、しかも時々・・・でもしばらく(数分~30分くらい)したら何事もなかったように正常に動作するようになる、みたいな事象が起きていた時のお話をします。

そもそも通常動作は?

基本的な通常動作は以下のようになります。(かなり簡略化しています)

左の図は1リクエストが1レスポンス返すまでに他のリクエストがない場合の図で、右の図は1リクエストが完了する前に2リクエストが要求されて、2リクエストが動けるまで(イベントループ回ってくるまで)waitしてから、スタートする図です。

基本的にはこれが3リクエストだろうがXリクエストだろうが、awaitなどでイベントループが回ってきたときに動作し、処理の途中で各リクエストがレスポンスを返すまで実行できるタイミングで実行します。

リクエストが1000リクエスト/分あったとしてもこれが動き、大体10ms~長くても500ms程度でイベントループで実行する処理が切り替わりながら動作するので、同時処理しているようにユーザーには見えるわけです。(そういえば、Windows95はマルチタスクでしたね)

問題動作って?

そして、今回問題になった時は以下の挙動をしました。

この30分レスポンスがなくなるリクエストがすべてのリクエストであれば、DBの単純負荷(スペック不足)などが考えられるのですが、レスポンスが30分かかるリクエストは特定のデータが格納されたデータだけでした。

さらに、DB接続はスレッドプールを利用して接続していたため、レスポンスが30分待ちのものがあってもスレッドプールに接続プールが残っていたらそちらを使うので他のリクエストは問題なく処理できていました。

ただ、この30分かかる処理がスレッドプールの上限(当時は10)を超えた場合・・・

すべてのDBリクエストがrequestの時点で「スレッドプール解放待ち」になります。(awaitから返ってこなくなります)

そうなると基本的にほぼすべてのリクエストはDBにデータを参照したり書き込んだりするわけで・・・

最終的には30分かかる処理が終わってスレッドプールが他のリクエストでも使えるようになると、突然サーバーは復帰し、何事もなかったかのように溜まった処理をすべて処理して、通常状態に戻ります。

結局は実装の問題

ちなみに今回の問題ではDBに1回のリクエストでAWSの制限クォータを超える大量のレコード数のWrite命令をしていて、それでもAWSががんばってくれた結果、30分で処理を終わらせてくれてた、というのが事の真相です。(AWSすごい!)

対処方法はシンプルで1回のリクエストでWriteするレコード数を減らして複数回のWrite命令でリクエストをするでした。

そのため「Node.jsのシングルスレッド関係なくてDBの使い方の問題じゃん!」と思うかもしれません。

しかし、今回はDBでロックしてしまっただけで、コードの書き方や他のリソースへのアクセスでイベントループに戻らないような長い処理(for文ループやBlock I/OのファイルRead/Writeなど)でも同じことが起こります。むしろDBのようにスレッドプールがなければ、即止まります。(というかそういう事象も起きました・・・)

回避方法はいくつか(WorkerThreadを使う、ロックするような処理は別サーバーや別プロセスで実行するなど)ありますが、それは結構大変だったりするので最終手段として、以下を気をつけてコードを書くようにするのがよいと思います。

  • 可能ならawait/asyncやpromiseにして細かくイベントループが動けるようにする
  • for文などのループ内処理が同期処理のみでループが抜けるのに時間がかかりすぎないかを確認して、かかりすぎるようであれば、await/asyncやpromiseで処理を分割する(必要であればログを仕込んで実行時間を確認する)
  • どこでイベントループに戻るかを意識する

マルチプロセスやマルチスレッドの場合には、同時処理(ディスパッチやクリティカルセクション、ミューテックスやセマフォなど)に関するリソース管理や実行順序などの注意が必要ですが、Node.jsでは他は動かないということに注意する必要があります。

システムが止まるというのは胃がキリキリする話ですが、そういったことが少しでも減って安定したシステムを提供できるようにしたいですね!

以上

ファビコンについて調べてみた

先日、弊社のコーポレートロゴ、サービスロゴが新しくなり、
ロゴの他にファビコンも差し替えました。

その際に、ファビコンについてブラウザのタブに表示されるぐらいの認識しかなかったので、
詳しく調べてみようと思いました。

ファビコン(favicon)とは?

ファビコンは「Favorite Icon」の略で、Webブラウザのタブやブックマークや検索結果に表示される小さな画像です。
Webサイトをブックマークする際や、複数のタブを開いているときに、ファビコンは瞬時にサイトを認識するための視覚的な目印になります

Webブラウザのタブ

ブックマーク

検索結果

ファビコンの重要性

ブランド認知の向上

ファビコンは、ウェブサイトのブランドを視覚的に強調するための強力なツールです。
シンプルで覚えやすいアイコンは、ユーザーがタブやブックマークからサイトを素早く認識できるようにします。

信頼性の向上

ファビコンが設定されていないサイトは、未完成や信頼性の低いサイトと感じられてしまうことがあります。
特に商業サイトやブログでは、ファビコンがあることでプロフェッショナルな印象を与えることができます。

ファビコンの作り方

ファビコンはツールを使って簡単に作成できます。
以下は主な作成ツールです。

  • オンラインジェネレーター
    Favicon.ioReal Favicon Generatorといったツールを使うと、画像をアップロードするだけでファビコンを生成できます。
  • デザインソフト
    Adobe PhotoshopやIllustratorなどのグラフィックデザインソフトを使って、自作のアイコンを作ることも可能です。
    ファイルを作成したら、ICOフォーマットに変換します。

ファビコンの最適なサイズとフォーマット

ファビコンのサイズやフォーマットは、ウェブブラウザやデバイスによって異なるため、
適切なサイズで複数のフォーマットを用意することが大事です。

サイズ用途フォーマット
16x16ブラウザのタブやアドレスバーICO
32x32デスクトップのショートカットPNG
180x180AppleのホームスクリーンアイコンPNG

.ico形式だけでも多くのブラウザで対応可能ですが、
より高品質で柔軟な対応を求める場合は、.ico形式とPNGSVGなどの形式を併用して、
各デバイスやブラウザに最適化するのがベストです。

ファビコンがSEOに与える影響

ファビコン自体が直接的に検索エンジンに影響を与えることはありませんが、
ユーザーエクスペリエンスに関しては間接的にSEOに貢献する可能性があります。

クリック率の向上

ファビコンが適切に設定されていると、ユーザーがサイトをより覚えやすくなり、
再訪問やブックマークからのクリック率が上がることがあります。

信頼性の向上

特にスマートフォンのホーム画面に追加された場合、
ファビコンがしっかり表示されていることで信頼性が高まります。

まとめ

ファビコンは、小さくても大きな影響力を持つウェブサイトの重要な要素になります。
ブランドを視覚的に認識させ、プロフェッショナルな印象を与え、
ユーザーのエクスペリエンスを向上させることができます。

設定方法も簡単なので、まだ導入していない方はぜひ導入してみてください。

入社のご挨拶

はじめまして。
この度、開発職として中途で入社しました佐藤と申します。

前職では受託開発というかたちで、経費精算やヘルスケア領域などのWEBアプリケーション開発に従事していました。

主に以下のような点に惹かれ、かつご縁がありこの度はグリニッジに入社することと相成りました。

  • 事業会社ということもあり、エンジニアもユーザーファーストの意識を持っていること。
  • フルスタックエンジニアとしての経験ができそうであること。
  • 職場の雰囲気が良いこと。(一番の決め手です)

私は「何をやるか」より「誰とやるか」に重きを置いています。
好きな人とであれば、なんでもできるような気がします。なんでも挑戦できるような気がします。
勇気、モチベーション、パフォーマンス、忍耐力―――心の奥底からパワーが沸き上がる感覚ですね。
もちろん、私だけでなく、私が皆さんにもパワーをお裾分けできるような存在になりたいと思います。
グリニッジの皆さん、どうぞよろしくお願いいたします!

また、エンジニアという枠に囚われずに、ビジネスサイドも意識したエンジニアリングに力を入れていきたいです。
グリニッジの企業価値向上へ向けて、目の前のキーボードを叩いていきたいと思います。

習慣化したい!タスクマネジメント

仕事もプライベートも
やりたいことがたくさん。
けど時間が足りない…!

そんな時、時間を有効に使うには
スケジュール管理が不可欠!
ということで、タスク管理をしようとしたものの
こんな失敗をしたことは、ありませんか?

タスクの規模が大きい
タスクがざっくり曖昧すぎてしまうと…

工数や作業時間が読めない
何から着手したらいいかわからない
結果、思っていたより時間がかかってしまい
タスクを予定通りに完了できない。


一度はこんな失敗したこと
あるんじゃないでしょうか。

こういった結果にならないためにも
タスクブレイクダウンをおすすめします!

タスクブレイクダウン(TB)とは?
タスクを細分化して
効率を上げる手法です。


具体的には、以下の3ステップを行います。

STEP 1  完了までの流れを決める
STEP 2  流れで必要な作業を洗い出す
STEP 3 作業に必要な時間を見積もる

このステップを考える時は、7W1Hを使います。

いつまでに(When)
どこで(Where)
誰が(Who)
誰に(Whom)
何を(What)
なぜ(Why)

どれから(which)
どのように(How)

細分化することで、完了までの流れと
必要な時間が明確になり
より現実的な時間管理が行えます

あとは細分化されたタスクに、集中するだけです!

この手法のよい点は、大きいタスク完了までには
時間がかかりますが、細分化されたタスクであれば
タスク完了数も増え、短いスパンで達成感が得られ
モチベーション維持がしやすいところです。

今回は、タスクブレイクダウンについて
紹介させていただきましたが、タスク管理方法について
グリニッジのスタッフblogの投稿でも紹介されていたり
チームやプロジェクトでも、日々タスク管理が行われています。

タスク管理方法は、他にもいろいろあるので
他の手法も取り入れ、個人のタスク管理能力を高めることで
時間を有効に使っていきたいです!

Material Designを学びたい - UIの考えかた -

Material Design とは

https://m3.material.io/

Googleのデザイン原則

Googleが2014年に提唱したデザインシステムです。一貫性のあるデザインで、直感的に使えるユーザーインターフェースを提供することを目的としています。
色や部品のデザインから、余白など配置の仕方まで、UIのあらゆる観点について具体的な数値が定義されています。

引用元:https://m3.material.io/styles/color/system/how-the-system-works

色であれば役割を定義しており、有彩色系(Primary / Secondary / Tertiary)と 無彩色系(Neutral / Neutral variant) の5つの役割と、それぞれの明るさを調整したカラーパレットがあります。
マテリアルデザインでは任意の一つの色から自動的にカラーパレットを生成することもできます。

Material Design のメリット

汎用性が高い

ユニバーサルなデザイン
Googleが提唱しているということもあり、世界中の言語に対応していること、視覚障害などを持つ方でも見やすくする工夫がされていることなど、あらゆる人が使いやすいデザインとなっています。

さまざまなデバイス・アプリで使えるデザイン
デバイスの汎用性も高く、スマホやタブレットなどさまざまなデバイスを利用しても、見た目や使い勝手が同じように使えるデザインになっているため、ほとんどのアプリで適用できます。

設計・開発ツールが充実

Figma

Figmaでは「Material 3 Design Kit」というデザインキットがあり、デザインファイルを開くとこのようにシートの中に既に各部品が用意されています。これらをデザインシート内に取り込めば、Figma上で簡単にマテリアルデザインを使用することができます。

開発ツール (Flutter)
Googleが開発したFlutterではマテリアルデザインが標準でついているため、部品などを配置した際にマテリアルデザインの仕様で作られるので、マテリアルデザインを取り入れながらアプリ開発をすることができます。

デザイントークン
UIの管理や更新をしやすくするために色やフォントなどのスタイルプロパティを階層で関連付けており、開発とデザイナーの共通言語となって連携がとりやすくなります。

Material Design の注意点

膨大で前提知識が必要
優れたUIデザインを実装できるマテリアルデザインですが、事細かなUI資料の量は膨大で、日本語版もありません。また、これまでのマテリアルデザインのバージョンでの前提知識が必要になる場合もあり、UIの基本知識をある程度理解している必要があります。

どれも似たデザインになってしまう
マテリアルデザインに頼りすぎると、似たようなUIデザインばかりになってしまいます。
デザインの一貫性を保つという点ではいいかもしれませんが、そのアプリとしてのオリジナリティは損なわれてしまう可能性があり、参考にする際はどこまで取り入れるか注意が必要です。

Material Design のUIの考えかた

画面の中は3次元空間

引用元:https://m2.material.io/design/environment/surfaces.html#material-environment

画面のユーザーインターフェースは、表面(X軸 / Y軸) と高さ(Z軸) があるという概念です。手前に高さがあり、高さ方向へUIが積み重なっていくような考え方です。
スマホの画面自体は平面ですが、手前側にモーダルなどの部品を重ねることによって空間を作っていき、Z方向の空間を再現していきます。マテリアルデザインの中では z-axis と書かれています。

マテリアルデザインの紙

現実で実在する「」という物体が、重なったりすることで画面の中で空間を再現するという概念です。ここでいう紙とはボタンやヘッダー、カードなどの各要素を指します。すべての紙は1dpの厚みであり、厚み自体は変わらないという考え方です。
また、重なるときは影があり、手前に部品が上がったときには、影が大きくなっていくなど、影の適用についても書かれています。

左図は正しい例ですが、右図は影の入れ方としてはNG例になります。高さの位置が変わっていない場合は影をつけてはいけない、という例です。


こちらはユーザーがマウスのホバーなどをしたときは、操作した部分だけにホバー状態が反映されることが適切であり、右図のように紙の枠から突き抜けてはいけないという例です。
また、下にある紙と上にある紙で重なった紙どうしが突き抜けることはない、といったことも書かれています。

このように、現実世界にある物理法則を取り入れることで、ユーザーの直感的な操作を可能にしています。

8dp を基準としたレイアウト

dpは、画面サイズや解像度が異なるデバイスでも同じ大きさに見えるマテリアルデザイン独自の単位です。同じdpを指定すれば、左図の解像度が低いデバイスと右図の解像度が高いデバイスで同じ大きさで要素が表示されます。
ピクセルなどで指定すると画面の解像度が違えば異なる大きさで表示されてしまいますが、dpの場合は解像度に基づいて描画サイズを算出するため、見た目のサイズを統一することができます。

マテリアルデザインでは、8dpのグリッドが基本の間隔とされています。(アイコンやタイポグラフィなどの小さい部品は4dpグリッドになります)
デザインキットのサンプルを見ると、レイアウトグリッドに合わせて要素が配置されており、左右の余白のバランスも取れていることが分かります。

まとめ

今回はマテリアルデザインのベースとなる基本的なUIの考えかたについて一部ご紹介しました。
マテリアルデザインの存在は知っていながら、色や部品ばかりが先行し、その概要や根本的な考えが書かれているドキュメントをしっかりと読み込んだことはなかったため、今では当然の内容も含め改めてマテリアルデザインのUIを構成する概念から学びなおしたいと思っています。
ガイドライン自体はボリュームがあるため時間はかかりますが、サイト内はアニメーションなども豊富にあり回遊しているだけで楽しいです。

優れたデザインについ頼ってしまいたくなりますが、マテリアルデザインはあくまでもガイドラインというひとつの指針として、いいところを柔軟に取り入れて、自分でカスタマイズしながらコンテンツの工夫をし、デザインに独自性を出していくことが重要なのではないかと思います。

自動テストを流すときはSlack通知をしたくなかった。

背景

弊社ではプロジェクトによってアラートをSlackに通知する運用をしているのですが、自動テストを流すと異常系のテストでSlackに通知が飛んでしまっていたので、これを通知しないようにしたかった。

getMockBuilderで...

PHPで開発を行っているプロジェクトなのでPHPUnitを使用しており、getMockBuilderとかcreateMockとかすればできるだろうと考えていました。

Slackの通知は以下のように行っていました。

<?php

class CheckShimasu
{
	public function noticeShimasu()
	{
		$kakukaku = 'かくかく';
		$shikazika = 'しかじか';
		$kakukakushikazika = $kakukaku . $shikazika;

		$slack = new SlackNoClass($kakukakushikazika);
		$slack->anochannelnitsuchi();
	}
}

なのでgetMockBuilderでこのSlackNoClassをモックにして、テスト対象であるcheckShimasunoticeShimasuメソッドをテストしようと思い、以下のように書いてみました。

// slackが1回通知されることのテスト
public function test()
{
	$mock = getMockBuilder(SlackNoClass::class)
			->setMethods(['anochannelnitsuchi'])
			->getMock();
		
	$mock->expects($this->exactly(1))
		->method('anochannelnitsuchi');

	$sut = new CheckShimasu();
	$sut->noticeShimasu();

	$this->assertTrue(true);
}

しかしこれではSlackNoClassはモックにされず、Slackに通知が飛んでしまっていました。
どうやらgetMockBuilderやcreateMockでは、テスト対象のクラスでインスタンス化されるクラスまではモックにできないようでした。

そうですか、できないんですね...

いろいろ調べていたところ、getMockBuilderやcreateMockでもできるよというような内容の記事もいくつかを見つけることができましたが、どうにもうまいことモックすることができず...
最終的にはPHPのモッキングフレームワークであるMockeryを入れて、下記のようにSlackNoClassを「'overload'」とすることでSlackの通知を止めることができました。

// slackが1回通知されることのテスト
public function test()
{
	Mockery::mock('overload:SlackNoClass')->shouldReceive('anochannelnitsuchi')
                                        ->times(1);

	$sut = new CheckShimasu();
	$sut->noticeShimasu();

	$this->assertTrue(true);

    Mockery::close();
}

staticの関数のテストでも簡単にできたりもするようですし、他の外部APIを使用しているところなどもあるので、いろいろ調べながら使っていきたいですね。

以上