副業を始めた

今月から副業として、知り合いの方の会社で開発のお手伝いをするようになりました。

私が副業を始める前、「副業をしている人が、どうやって生活の中に副業を取り入れているのだろう」とか、「どうやって副業を見つけたんだろう」と気になっていたので、私はこうやっています、という話を書いてみます。

副業をしたくなったきっかけ

会社の人の副業話を聞いて、興味を持ったのがきっかけです。本業以外の会社も開発の仕事をすることでスキルの幅も広がりそうだなと思いました。

それと、単に収入を増やしたかったのも理由です。何かで「会社で月5万昇給するのは時間がかかるけど、副業で月5万稼ぐ事はすぐできる」というような話を何かで読んで、「副業最高じゃん」と思った記憶があります。

副業の探し方

フリーランス向けの案件サイトでWeb開発の副業案件を探していましたが、なかなか見つからず。

即戦力で働ける技術となると私の経験的にPythonに限られる、という理由もありそうですが、そうでなくても週1程度の案件の募集は数が少なかったです。

そんな中、以前仕事でお世話になった方からたまたま連絡をもらった折に、「副業を探してるんですが……」とダメ元で聞いてみたところOKをもらい、その次の月から参画させていただく形となりました。ありがたい……!

案件サイトにも副業案件が全く無いわけではないので、タイミングによっては仕事が見つかるんだろうなとは思っています。私は2ヶ月程度しか見ていなかったので、条件に合う案件は見つけられませんでした。

勤務時間

勤務は水〜金の夕方と土曜の朝の時間に少しずつ、週の合計で8時間程度稼働しています。

副業がある日もない日も、本業&副業合わせてだいたい8:00~18:00の時間で働くようにしています。

これは、「◯曜日は長く働くから憂鬱」みたいな曜日を作りたくなかったからです。

月・火は本業の方で残業気味に働いて、その分水〜金で早めに退勤させてもらう感じにしています。

どちらの会社もイレギュラーな勤務時間となることを許していただき、感謝しています……!

仕事内容

仕事内容としては、UI改善や開発ツール改善など、優先度の高くないタスクを消化していく小人さん的に使ってもらっています。

作業時間が限られるため、新規機能追加等にガンガン参加していくことは副業では難しそうです。

とはいえ、本業とは業界も仕事の進め方も技術スタックも異なるので、いい刺激をもらいながら楽しく作業させてもらってます。

契約

業務委託契約として、稼働した時間単位で委託料をもらう契約をしています。(いわゆる準委任契約というやつだと思う)

契約書には「月◯時間程度」とありますが、明確な稼働時間の上限・下限はありません。稼働した時間分だけ委託料を請求する*1形の契約となっています。

準委任契約だと「◯時間〜◯時間」といった時間幅での契約をするイメージでしたが、そうではないです。これは委託元の会社によって違いそうですね。

まとめ

私の副業の仕方を書いてみました。副業をしている人の1例として参考になると嬉しいです。

副業の探し方についてはあまり参考になる話は書けませんでしたが、知人の会社で働かせてもらう、という選択肢が取れそうなら、ダメ元でも頼んでみるといいのかなとは思います。

仕事を頼む側の気持ちに立ってみても、特に稼働時間の短い契約では、見ず知らずの人を呼ぶよりも素性の知れている人のほうが依頼しやすそうですね。

*1:契約期間ごとに請求書を作成して先方に送付しています。確定申告のために登録したfreeeを使って請求書を作っています

easy-thumbnailsで発生するInvalidImageFormatErrorの調査メモ

easy-thumbnailsというPythonライブラリのエラー調査をしたので備忘録

InvalidImageFormatErrorが発生したら

easy-thumbnailsでは、画像読み込み時のエラーを丸めてInvalidImageErrorというエラーとして送出します。

このエラーが発生した場合は、easy-thumbnailsの画像を読み込む関数source_generators.pil_image()に直接画像を渡してみると、実際に発生しているエラーのスタックトレースを確認できます。

>>> from easy_thumbnails.source_generators import pil_image
>>> with open("<対象画像ファイルのパス>", "rb") as f:
>>>     pil_image(f)

実際に私が遭遇した例は以下。

  • 空の(0バイトの)画像ファイル
    • 画像生成処理に不具合があり、空の画像を登録していた
  • tEXtチャンクに大量のテキストを含むファイル

※なお、SVGファイルの場合は、SVGファイルを読み込む際に使用されるsource_generators.vil_image()に渡してみると良さそうです。(動作未確認)

easy-thumbnailsについて

easy-thumbnailsは任意のサイズのサムネイルを動的に生成してくれるライブラリです。

DjangoImageFieldを継承したThumbnailerImageFieldというフィールドを実装しており、このフィールドに画像を入れておくことで、欲しいサイズのサムネイル画像を簡単に生成することができます。

github.com

以下、ドキュメントのサンプルコードから拝借。

settings.pyにて、avatarというエイリアスで設定を定義:

THUMBNAIL_ALIASES = {
    '': {
        'avatar': {'size': (50, 50), 'crop': True},
    },
}

models.py:

from easy_thumbnails.fields import ThumbnailerImageField

class Profile(models.Model):
    user = models.OneToOneField('auth.User')
    photo = ThumbnailerImageField(upload_to='photos', blank=True)

テンプレート内でエイリアスavatarを使用して画像を取得:

{% load thumbnail %}
<img src="{{ profile.photo.avatar.url }}" alt="" />

Pythonコード内でエイリアスavatarにアクセスして画像を取得:

thumb_url = profile.photo['avatar'].url

https://github.com/SmileyChris/easy-thumbnails#example-usage

なお、エイリアスavatarにアクセスしたタイミングで動的にサムネイル画像が生成される。画像はDjangoに使用しているファイルストレージ(S3など)にキャッシュされ、2度目以降のアクセスはキャッシュから画像が取得されます。

InvalidImageFormatErrorの発生箇所を追ってみる

InvalidImageFormatErrorをraiseしているのは、Thumbnailerというクラスのgenerate_thumbnail()という、その名の通りサムネイルを生成する関数。Thumbnailerは、ThumbnailerImageFieldでも継承されているサムネイル生成機能を提供するクラス。

この処理の中で、engine.generate_source_image()という、生成するサムネイルの元画像を生成する関数を読んでおり、この戻り値がNoneの場合にInvalidImageFormatErrorをraiseしている。

        image = engine.generate_source_image(
            self, thumbnail_options, self.source_generators,
            fail_silently=silent_template_exception)
        if image is None:
            msg = "The source file does not appear to be an image: '{name}'"
            raise exceptions.InvalidImageFormatError(msg.format(name=self.name))

https://github.com/SmileyChris/easy-thumbnails/blob/2.8.5/easy_thumbnails/files.py#L387

このgenerate_source_image() の中では、generator関数を実行している。デフォルトでは、このgenerator()としてpil_image(), vil_image()の2つの関数が順に実行され、どちらかが正常な画像を返せばその画像が返却される。

これらの処理の中で例外が発生した場合、fail_silentlyフラグがFalseの場合は明示的にreturnしない(Noneを返す)挙動となっている。なお、このfail_silentlyというフラグは、profile.photo['avatar'].urlのようなエイリアスへのアクセス時のサムネイル生成ではFalseになっている。

            try:
                image = generator(source, **processor_options)
            except Exception as e:
                if not fail_silently:
                    if len(generators) == 1:
                        raise
                    exceptions.append(e)
                image = None

https://github.com/SmileyChris/easy-thumbnails/blob/2.8.5/easy_thumbnails/engine.py#L114-L121

このgenerator()として実行されるのが、冒頭に記載したpil_image(), vil_image()の関数です。これらの中で画像を開いた際に発生した例外が、easy-thumbnails側でInvalidImageFormatErrorとして丸められているということらしい。

pil_image(), vil_image()の実装: https://github.com/SmileyChris/easy-thumbnails/blob/2.8.5/easy_thumbnails/source_generators.py

まとめ

というわけで、冒頭に書いた通り、InvalidImageFormatErrorが発生した場合は直接pil_image(), vil_image()に画像を渡してみると良さそうです。

分割キーボード BAROCCO MD600v3 RGB を買った話

この記事は、キーボード #2 Advent Calendar 2023の21日目の記事です。

前回は五月雨さんの2023の設計まとめと名付けの話でした。
ご自身で設計されたキーボードの紹介記事ですが、それぞれに可愛い名前を付けられていて愛着を感じました。

はじめに

先月BAROCCO MD600v3 RGB(以下、MD600v3)という分割キーボードを買ったので、製品の紹介や、1ヶ月使ってみての感想を書いていきます。

archisite.co.jp

アドベントカレンダーに参加している他の方々が皆さん自作キーボードの話をしていて、若干の場違い感を感じていますが、強い心で既製品の話をしていきます。)

分割キーボードのメリット

分割キーボードは一体型のキーボードと比べてタイピング時に無理のない体勢でタイピングすることができます。

MD600v3の製品ページには以下のような説明がありました。

キーボード自体が左右に分離できるので、肩や腕を広げた自然なフォームでのタイピングを可能にします。通常のキーボードだと両手が近づくため、どうしても肩や腕が内側に狭くなってしまいます。キーボードを離すことで、それぞれの肩の広さに合わせた位置にキーボードを設置し、自然な状態でのタイピングが可能です。
https://archisite.co.jp/products/mistel/md600v3-rgb/#block-2

私も以前から分割キーボードには興味を持っておりましたが、どの製品も価格が安くなく、なかなか手を出せずにいました。

安いキーボードを2つ買って並べて使っていたこともあるのですが、場所を取り邪魔なのでいつしかやめてしまっていました。

miyashiiii.hatenablog.jp

購入したキーボード

BAROCCO MD600v3 RGB
https://archisite.co.jp/products/mistel/md600v3-rgb/

こちら、販売元のサイトから拝借した商品写真です。

一体型のキーボードを真ん中で割ったような形になっており、左右をくっつければ一体型と同じように使うこともできます。

バックライトを七色に光らせることができてかっちょいいです。

購入の決め手

こちらの製品を選んだ決め手は、サイズのコンパクトさです。

世の中に出回っている分割キーボードの中にはサイズが大きいものも多いのですが、BAROCCOシリーズは比較的コンパクトなものが多く、特にMD600系は元々使っていたMagic Keyboardキーボードとサイズがほぼ変わりませんでした。

MD600系はいわゆる60%キーボード*1となっており、独立したファンクションキー(F1~F12)やカーソルキーがありません。
キーを組み合わせてそれらの操作を行う分、ホームポジションからの離脱が減り、生産性も上がるのでは、と考えました。

キースイッチについて

MD600v3ではCHERRY MXスイッチ*2を採用しており、茶軸・青軸・赤軸・静音赤軸の中から選んで購入することができます。

キースイッチの知識はなかったのですが、店頭で押し比べて赤軸を選びました。
キー荷重の軽い赤軸と静音赤軸で迷ったのですが、静音赤軸は打鍵感がソフトなせいか赤軸に比べて若干の重さを感じました。

パームレストについて

キーボードと一緒に、こちらの分割パームレストを購入しました。
FILCO Majestouch Wrist Rest "Macaron" 薄型12mm・Sサイズ・分離型(2分割)・Rainy

木製やプラスチック製はひんやりするかなと思い、ソフトタイプのものを選びました。

12mmの高さのものを買ったのですが、キーボードの高さを考えると17mmでも良かったかなと思っています。

他に迷った製品

購入に当たり、FILCO Majestouch Xacro M10SPという製品も候補として検討していました。

FILCO Majestouch Xacro M10SP

MD600v3を含む通常の分割キーボードのデメリットとして、一体型キーボードであればどちらの手でも押せる中央付近のキー(6キー, Yキー, Bキーなど)が、左右どちらかの手でしか押せなくなる点があります。

その点、M10SPであれば、中央にあるマクロキーにそれらを割り当てることで今までと同じ運指でタイピングできる点で魅力を感じ、候補に入れていました。

しかし最終的には、キー配置は慣れるだろうということで、価格の安さとサイズの小ささで、MD600v3を選びました。(M10SPのほうがマクロキーの分若干横幅が大きい)

実際、MD600v3を使い始めたらキー配置にはすぐに慣れました。

キー配置の調整

MD600v3を使用するに当たり、私は以下の2つを使ってキー配置の調整をしています。

MD600v3のマクロ機能

MD600v3のマクロ機能を使って、各キーに任意のキー操作を割り当てることができます。
例えば右スペースキーをFnキーに割り当てるなどの変更を行っています。

なお、私は使っていませんが、1つのキーに複数のキー操作を割り当てることも可能なようです。

Karabiner-Elements

Karabiner-Elementsはキーボードの各キーの挙動を変更できるツールです。
単純なキーの置換ではない変更には、こちらを使用しています。

Karabinerにはいくつかプリセットの設定があり、例えば私は「シフトキーを単体で押したときに、英数・かなキーを送信(左が英数、右がかな)」という設定を入れています。

1ヶ月使ってみてよかった点

購入して1ヶ月程度経つので、気づいたことを書いていきます。

タイピング体験の向上

マクロ機能でカーソルキーやBackspaceキーなどをホームポジションで入力できるように調整したのですが、これがかなり作業効率が上がったと感じています。
特にブラウザ上などVimコマンドの使えない環境でのテキスト編集が快適になりました。

なお、「ファンクションキーがないと不便かなあ」と思っていたのですが、これは個人的にはむしろ逆で、Fnキー+数字キーで入力するためホームポジションから離れることなくスムーズにファンクションキーを入力できています。

Fnキーとの同時押し操作を結構頻繁にするのですが、前述のとおり右スペースキーの位置にFnキーに割り当てているので、押しやすく便利です。 これはスペースキーが2つに分かれているからこそできることかもしれません。

姿勢の改善

キーボードの配置ですが、左右のキーボードを肩幅程度にひろげて置いて、その真ん中にトラックパッドを置いています*3

自然な姿勢でタイピングできるようになったおかげで、若干の肩のラクさを感じている……気がします。(正直、劇的な変化は感じてないです。笑)

Magic Keyboardからの乗り換えで苦労している点

現状、MD600v3自体への不満は現状特にないのですが、Magic Keyboardからの乗り換えという点でいくつか慣れない点があります。

MacBookでタイピングする時の違和感

Magic KeyboardはMacBookのキーボードとキーの間隔がほぼ同じですが、MD600v3はMacBookよりもキー同士の間隔が広いです。
最近MD600v3に慣れてきたせいで、たまにMacBookのキーボードを使うと違和感を感じるようになりました。

ソファに座って膝の上でMacBookを置いて作業することがたまにあるんですが、USBケーブルの位置のせいでいわゆる尊師スタイルも難しいので、いつもミスタッチに苦しみながらタイピングしています。

Touch IDがない

Magic KeyboardにはTouch IDがあり、これが地味に便利だったんですが、この恩恵を受けられなくなってしまいました。
失って初めて分かるありがたみ……。

有線接続である

Magic KeyboardはBluetooth接続だったのに対し、MD600v3は有線接続のため、多少デスク上の配線は増えました。

この点は購入前に少し気になってたんですが、キーボードからまっすぐデスクの奥方向にケーブルを伸ばし、デスクの裏を通ってPCまでケーブルを回すような配線にしたところ、見た目のごちゃつきはそこまでないです。

終わりに

当初は姿勢改善目的で興味を持った分割キーボードでしたが、どちらかというとキー配置が変わったことによるタイピングの効率化のほうにメリットを感じる結果になりました。
こういう効率を追い求めていった先に自作キーボードの沼が待っているんだろうか……?

何はともあれ、よいキーボードに出会うことができて満足しています。

購入に当たって相談に乗っていただいた、社内slackのキーボード部屋の皆さんにも感謝しています。


この記事はBAROCCO MD600v3 RGBを使用して執筆しました。
明日の記事はyymmさんです。

*1:キーボードについて調べる中で、キー数によって、○%キーボードという言い方をすることを知りました。https://ggjpn.com/mechanical-keyboard-size/

*2:カニカルキーボードで広く使われているキースイッチのようです。 https://archisite.co.jp/pick-up/keyboard-switch/

*3:購入前に見たレビュー記事で、左右のキーボードを繋ぐUSBケーブルが短い、という意見を見て心配していましたが、肩幅程度に開くのであれば全く困っていません。

PyCon APAC 2023に参加しました

10/26(金) - 27(土)に都内で開催されたPyCon APAC 2023カンファレンスに参加してきました。
Twitterで見かけた「ブログを書くまでがカンファレンス」というワードに後押しされて、参加した感想を書き残そうと思い書いています。

PyCon APACとは

日本では年に1回PyCon JPが開催されているようですが、今年はPyCon APACとしての開催。PyCon APACは毎年各国持ち回りでやっているようで、日本での開催は10年ぶりらしい。
私はPyCon JP含め初めての参加でした。
カンファレンス自体は2日開催ですが、私は2日目に予定があったので1日のみの参加でした。

「Where did you come from?」のアンケートのボード。いわゆるアジア太平洋地域に限らず、幅広い地域からの参加者がいたようです

参加のきっかけ

会社の人が多く参加する&登壇する人もいることを知り、参加してみることにしました。
前職ではPython以外にもJSやFlutterを書いたりしていましたが、今年から働いている今の会社ではほぼPythonだけをゴリゴリ書くようになったため、Pythonの知識を深めたいという思いもありました。

聞いたトーク

Keynote: Why University Teachers Wrote a Python Textbook?

京大でのPythonの授業を持ち、教科書を作っているKita先生のトーク
トークは英語でしたが、別の部屋で日本語の同時通訳付きでの配信を見ることができました。
プログラミング経験のない学生にプログラミングを教える苦労を聞きながら、自分も「変数とは」「ループとは」「関数とは」と基礎的な概念を理解するところから始めたなあ、と昔を思い出しました。

Introduction to Structural Pattern Matching

Python3.10で追加されたCやJavaでいうswitch文の拡張のような機能、Structural Pattern Matchingの紹介でした。
数値や文字列等の単純な比較だけでなく、辞書やオブジェクトのマッチングや型に応じたマッチング(isinstanceを使わなくていい)など、柔軟な条件で処理を分岐することができるようです。

スライド: slides.takanory.net

型チェックを強化するPython 3.11の新機能Data Class Transforms(PEP 681)

dataclassをベースにしたライブラリの型チェックを強化する機能の紹介でした。
一般の開発者目線では、これを直接使うというよりも、各ライブラリや型チェッカーがこの仕様に対応することで、より型チェックできる範囲が広がってロバストなコードになる、という類の機能のようです。発表時点では、対応している型チェッカーはPyrightだけらしい。
こういった自分で直接使わなさそうな機能は、新機能のまとめ記事に載っていても読み飛ばしてしまいがちなので、勉強になりました。

スライド: pyconapac2023-pep681-slide.ryu22e.dev

Pythonでのパッケージング:エコシステムの理解と現場での活用

Pythonのパッケージ管理が複雑になる理由と、対応策の紹介がありました。機械学習系のライブラリのバージョン管理はハマりがちですね……。
発表の中でRyeというRust製のパッケージ管理ツールが紹介されていましたが、この日のLTでもこのRyeの紹介のトークがありました。PyenvのようなPython自体のバージョン管理と、venvのような仮想環境の管理の両方ができるツールらしいですが、流行り始めているのでしょうか。

speakerdeck.com

Django ORM道場:クエリの基本を押さえ,より良い型を身に付けよう

DjangoのORMがどんなクエリを発行するか理解して使おう、という内容のトークでした。予期せぬクエリを発行しないよう、ログでSQLを見るクセをつけたいと思いました。

www.slideshare.net

ModuleNotFoundErrorの傾向と対策:仕組みから学ぶImport

PythonがImportを解決する仕組みを紐解いた上で、ModuleNotFoundErrorの対策を紹介するトークでした。
ModuleNotFoundErrorに限らず、typoなどのしょうもない実装ミスに気づかずエラー解析でハマることはよくあるので、よく遭遇するエラーの原因を改めてまとめておくのはよさそう。

speakerdeck.com

発表テーマの傾向について

先月行われたDjango Congress 2023にも参加してきたのですが、それと比較すると、DjangoCongressは(当然ですが)Web開発に関する内容がほとんどであるのに対し、PyConは以下のようなテーマがありました。

特に後半2つのようなテーマの発表が多いのは、他の言語でのカンファレンスにはない特徴かもしれません。

LINEヤフーさんのスポンサーブースにて「Pythonを何に使っているか」を問うアンケート(ボードにシールを貼って回答するもの)を見かけたんですが、私が見た時点で「Web開発」「機械学習・データ分析」と並んで「業務効率化」も同数程度の回答があったのが印象的でした。 Pythonを使った開発をしているわけではないが業務効率化のためにPython使っている、という人も多く、かつPyConに参加するほどPythonに興味を持っている人が多くいることを知りました。
私もこの手のカンファレンスはほとんど参加したことがなかったですが、メインで使っている言語以外の言語のカンファレンスに行くのも楽しそうかもな、と思いました。

会場での過ごし方

トークの時間以外はスポンサーブースを回ったり、ポスターセッションを見て回ったりしました。 お昼の時間には、私と同じく今年入社した3人で集まってランチをしました。個人でアプリの開発をしている話や、勉強会を主催している話を身近な人から聞けてよい刺激になりました。(受けた刺激を何かに昇華できるかはまた別のお話……)

感想

オンラインの勉強会だと聞き流してしまうような、少し興味の幅から外れたような発表も聞く気になるのがオフラインのカンファレンスのいいところだなと思います。
興味があったけど時間が被って聞けなかったセッションもあるので、アーカイブで聞いてみたいと思います。

心残りなのは、2日目のパーティーに参加できなかったことです(冒頭に書いた通り、今回は1日目のみの参加でした。)
イベント中は、会社の人やスポンサーブースの担当者の方と話すことはできましたが、なかなか社外の人と話す機会を持てず。
せっかく多くのPythonユーザーと交流できる機会なので、参加するならパーティーまで出ればよかったなあ、とパーティー参加者のTwitterの様子を見ながら思いました。
あと、チュートリアルやスプリントといったカンファレンス以外のイベントも面白そう。

おわりに

トークを聞きながら書いていた雑メモを見返したり、公開されている資料を見たりしながら内容を改めて咀嚼し直すことで、聞いた内容の理解を深めることができました。「ブログを書くまでがカンファレンス」と言いたくなる気持ちがわかりました。
記事にするならもっと写真を撮っておけば良かった。

PRODUCTION READY GRAPHQLを読む: 2章. GraphQLスキーマ設計 ①

PRODUCTION READY GRAPHQLを読む: 1章. GraphQL入門の続き。

今回読んだ章

この本の章立て

2章のタイトルは GraphQL Schema Design です。

この章の冒頭に、Googleの開発者Joshua blochの言葉が引用されています。

APIs should be easy to use and hard to misuse

優れたAPIは、簡単に使えて、誤った使い方をしにくいものでなければならない。 クライアントが使い方や振る舞いを容易に理解できる設計とするための考え方をこの章で学んでいきます。 なお、2章は長いので、3回程度に分けていく予定です。

設計ファースト

  • 実装を始める前に、スキーマの設計を行う。
    • そうでないと、システム内部の実装と結びついた設計となってしまう。
  • ユースケースに精通している人々と協力して設計する。
  • GraphQLには設計の懸念事項があるため、GraphQLのエキスパートとドメインのエキスパートが一緒に設計を行う。
  • APIは一度公開すると変更のコストがかかる。はじめに設計を行うことで、後々の変更を行うリスクを下げる。

クライアントファースト

  • GraphQLはクライアント中心のAPI
    • バックエンドのリソースやエンティティベースでなく、ユースケースベースで設計
    • これを怠るとAPIが汎用的になり、クライアントは自分のやりたいことを理解しにくくなったり、理解のために大量のドキュメントを読むことになったり
  • できるだけ早い段階で設計をクライアントと共有する
    • 6章で語られるらしい
  • YAGNI
    • クライアントの必要なものだけを提供する
      • 無駄なAPIは後々パフォーマンスやセキュリティ観点で廃止になることが多々ある
  • 実装の詳細に影響されないスキーマにする
    • 使っているDB、サーバーサイドの言語、ライブラリに依存しない
  • 多くのライブラリはDBをベースにスキーマを生成するが、これはクライアントファーストではない
    • スキーマが実装と結合することになる
    • テーブル、エンティティが汎用的になる
    • 顧客のニーズを意識していない
    • 必要以上のデータを提供することもある(YAGNIに反する)
  • RESTの定義からスキーマファイルを生成するツールもある
    • RESTとGraphQLでは設計上の注意点が異なるため、1からスキーマを作ったほうがいい
    • 開発スピードとのトレードオフ

以降、具体的なGood Practiceを見ていきます。

命名

よい命名は、ドキュメントを読んだり推測したりせずとも、APIが何をするものなのかという情報を与えてくれる。よい命名をするだけで正しい設計に導いてくれることが多い。

一貫性

# bad
type Query {
  products(ids: [ID!]): [Product!]!
  findPosts(ids: [ID!]): [Post!]!
}
type Mutation {
  addProduct(input: AddProductInput): AddProductPayload
  createPost(input: CreatePostInput): CreatePostPayload
}

対称性

具体性

一般的な名前を使ってしまうと、のちにより一般的な名前をつけたい時に変更が必要になる

type Query {
  viewer: User!
}
type User {
  name: String!
  hasTwoFactorAuthentication: Boolean
  billing: Billing!
}

後に、ユーザーのリストが取りたくなって、こう実装するとする。

type Query {
  viewer: User!
  team(id: ID!): Team
}
type Team {
  members: [User!]!
}
type User {
  name: String!
  hasTwoFactorAuthentication: Boolean
  billing: Billing!
}

これでは、各ユーザーの個人設定まで取れてしまう。 そこで、ユーザーをリストで取得する際に返すオブジェクトと、ユーザーが自分の設定を見る際に返すオブジェクトにTypeを分ける

type Query {
  user(id: ID!): User
  viewer: Viewer!
  team(id: ID!): Team
}
type Team {
  members: [User!]!
}
interface User {
  name: String!
}
type TeamMember implements User {
  name: String!
  isAdmin: Boolean!
}
type Viewer implements User {
  name: String!
  hasTwoFactorAuthentication: Boolean
  billing: Billing!
}

ユーザ情報を扱うinterfaceをUserとしたため、もともとUserとしていた個人設定を取るオブジェクトは、具体的なViewerに名前を変更することになった。 こういった変更が発生しないよう、初めからユースケースを検討して具体的な命名をするべき。

Descriptions

  • Descriptionsを書くことで、外部ドキュメントに頼らずスキーマを理解できる
  • 型が何を表すのか、Mutationで何が起こるのか明確に記述する
  • スキーマを見るだけで理解できることが理想
    • Descriptionエッジケースの記述、条件や応じて変わる挙動に関する記述をする
  • Descriptionは大事だが、ユースケースを理解させるためにDescriptionに依存しない

スキーマの機能を活用する

  • Enumを使う
  • Json文字列ではなく構造化する
  • カスタムscalarで形式を伝える
type Product {
# Instead of a string description, we use a
# custom scalar to indicate to clients
# that they can treat the result of this field # as valid markdown.
description: Markdown
}
scalar Markdown
  • 不可能な状態にすることを不可能にする (Make Impossible States Impossible)
  • 以下のスキーマでは支払い情報としてギフトコードorカード情報のどちらかを受け取る。
  • しかし、カード番号、カード有効期限に何を渡していいかわからない。また、ロジック側で形式チェックが必要。
type Payment { 
  creditCardNumber:String 
  creditCardExp: String 
  giftCardCode: String
}
  • 以下とすることで、カード番号と有効期限の形式を強制できた。
  • しかし、まだカード情報に関して番号と期限のどちらかだけを渡すことができ、ロジックでチェックする必要がある
type Payment {
  creditCardNumber: CreditCardNumber
  creditCardExpiration: CreditCardExpiration
  giftCardCode: String
}
# Represents a 16 digits credit card number
scalar CreditCardNumber
type CreditCardExpiration {
  isExpired: Boolean!
  month: Int!
  year: Int!
}

以下に改善することで、カード情報が入力される場合に番号と期限を強制できる

type Payment {
  creditCard: CreditCard
  giftCardCode: String
}
type CreditCard {
  number: CreditCardNumber!
  expiration: CreditCardExpiration!
}
# Represents a 16 digits credit card number
scalar CreditCardNumber
type CreditCardExpiration {
  isExpired: Boolean!
  month: Int!
  year: Int!
}
  • デフォルト動作を示す

      type Query {
        products(sort: SortOrder = DESC): [Product!]!
      }
    

汎用と特化

1つのフィールドに複数の責務を持たせている場合、別のフィールドに分割することでより良い設計になることがある

type Query {
  posts(first: Int!, includeArchived: Boolean): [Post!]!
}
type Query {
  posts(first: Int!): [Post!]!
  archivedPosts(first: Int!): [Post!]!
}

キャッシュが働きやすく、クライアントにとってわかりやすいスキーマとなる。

以下の例では、SQLに近い汎用的な操作を提供しているが、クライアントにとっても使用方法の理解が難しい。

query {
  posts(where: [
    { attribute: DATE, gt: "2019-08-30" },
    { attribute: TITLE, includes: "GraphQL" }
  ]) {
  id
  title
  date
  } 
}

汎用的な操作が求められるならこのような実装も考えられる。そうでないなら、以下の例のように必要なユースケースに絞った実装にすることを検討するべき

type Query {
  filterPostsByTitle(
    includingTitle: String!,
    afterDate: DateTime
  ): [Post!]!
}

Anemic GraphQL

Anemic GraphQLという章タイトルは、Anemic Domain Model(貧弱なドメインモデル)という用語をもじっている。

The anemic domain model is described as a programming anti-pattern where the domain objects contain little or no business logic like validations, calculations, rules, and so forth.

Anemic GraphQLとは、スキーマを単なるデータの入れ物と見ており、アクション、ユースケース、機能を考慮せずに設計されているスキーマのこと。

Queryの例

ユースケースを考えず、単にデータを返している例。

type Discount {
  amount: Money!
}
type Product {
  price: Money!
  discounts: [Discount!]!
}

これは単に定価と割引額を返しているとする。このとき、顧客側で以下のような計算をする必要がある。

const discountAmount = accounts.reduce((amount, discount) => {
  amount + discount.amount
}, 0);
const totalPrice = product.price - discountAmount

ところが、数ヶ月後に商品に税金が課されることになったとする。クライアント側に税金の額を渡すよう変更する必要がある

type Product {
  price: Money!
  discounts: [Discount!]!
  taxes: Money!
}

これに伴い、クライアント側のロジックを修正する必要がある。これを防ぐには、最初からクライアントが必要としているもの、つまり合計金額を渡せば良い

type Product {
  price: Money!
  discounts: [Discount!]!
  taxes: Money!
  totalPrice: Money!
}

こうすることで、今後もtotalPriceの計算方法が変わった場合も、クライアント側は正確な値を取得できる。単にデータを公開するのではなく、クライアントが必要としている形に加工して提供する。

Mutationの例

以下は、ECサイトで使用する、購入に関する情報を変更するミューテーションの例。

type Mutation {
  updateCheckout(
    input: UpdateCheckoutInput
  ): UpdateCheckoutPayload
}
input UpdateCheckoutInput {
  email: Email
  address: Address
  items: [ItemInput!]
  creditCard: CreditCard
  billingAddress: Address
}

あらゆる属性を変更でき、「商品をカートに追加する」「配送先を変更」「支払い方法を変更」など様々な場面で使える便利なミューテーションに見えるが、以下の問題がある。

  • 単にデータの更新に焦点を当てており、ユースケースを意識していない。
    • 一部の値だけ更新したい場合に、更新しないパラメータを渡す必要があるかどうかがわからない。
    • 最悪の場合、クライアントの意図しない形でデータが上書きされる可能性がある。
  • 特定のアクション、例えば「カートに追加」したいだけの時でも、その他の各フィールドの値を渡す必要がある
  • カートに商品を追加したい場合、既に追加済みの商品リストを取得して、新たに追加する商品を追加したリストを生成して渡す必要がある
  • 単にフィールドを見て何ができるかをクライアントが推測する必要がある。
  • 各フィールドがnull許容になっており、スキーマの表現力が低い
type Mutation {
  addItemToCheckout(
    input: AddItemToCheckoutInput
  ): AddItemToCheckoutPayload
}
input AddItemToCheckoutInput {
  checkoutID: ID!
  item: ItemInput!
}
  • 各フィールドが必須になっており、クライアントが何を渡したらいいか迷わない。
  • 追加したい項目を渡すだけでいい
  • このミューテーションで発生するエラーのパターンが減り、より細かいエラーを返せる
  • クライアントが意図しない更新をすることがない

副次的な効果として、入力とペイロードが予測可能になり、読むのも使うのも簡単になる。

まとめ

  • スキーマユースケースベース、クライアントのニーズベースで設計する
    • ドメインのエキスパートとGraphQLのエキスパートが協力するのがベスト
  • 命名では、ユーザーが迷わないように一貫性・対称性・具体性を意識する
  • スキーマの機能を活用して、ユーザーができることを明確にし、誤った使い方ができないようにする
    • スキーマ側の遊びを減らすことで、ロジック側の負担も減る
  • バックエンドのモデルをそのまま返すのではなく、ユースケースに応じた値を返す

感想

  • ユースケースベースで設計すべき、という話については、「理想はそうだけど、実際はモデルベースでスキーマを生成しちゃう方がラクなのでは」という気がしてしまう。
    • 単一のバックエンドの場合はそれでいいけど、複数のバックエンドを持つBFFみたいな構成の場合は、ユースケースベース・スキーマファーストで実装する方がスムーズなのかな、と思った
  • 命名スキーマ設計の話は、GraphQLに限らず一般的なプログラミングの考え方としても勉強になった
    • 「インターフェース側で制約をかけられる部分はビジネスロジックに任せずインターフェース側で担う。そうすることでクライアントにとってもわかりやすいし実装側の負担も減る。」という話は確かにそう。今まで意識できてなかったかもしれない。

今回はここまで。次回はRelay仕様に沿ったスキーマ設計などに触れるようです。

PRODUCTION READY GRAPHQLを読む: 1章. GraphQL入門

仕事でGraphQLを使うようになり、会社の人が紹介していたProduction Ready GraphQLという書籍を購入しました。

book.productionreadygraphql.com

普段こういう読書メモはNotionに雑に書くんですが、洋書ということもありしっかり読みつつ記事にしていくことで理解を深めていきたいと思っています。


目次

今回読んだ章

この本の章立て

Preface, Ackowledgmentsは飛ばして、An Introduction to GraphQLの章から読んでいきます。

この章ではGraphQLの基本的な仕様について説明しています。

GraphQLの公式ガイドQueries and Mutations, Schemas and Typesのページと内容が重複するところも多く、両者は交互に見ながら読み進めました。

読み進める

Hello World

まずは基本的なクエリの投げ方から。

クエリを投げるとよく似た構造でレスポンスが取得できる。dataというキーの値に取得した値が入るfriendsフィールドのfirstのように、引数を取ることができる。

query {
  me {
    name
    friends(first: 2) {
      name
      age
    }
  }
}
{
  "data": {
    "me": {
      "name": "Marc",
      "friends": [{
        "name": "Robert",
        "age": 30
      }, {
        "name": "Andrew",
        "age": 40
      }]
    }
  }
}

型システム

型システムはGraphQLはスキーマとも呼ばれ、一般的にGraphQL Schema Definition Language(SDL)で表現される。

どの言語でGraphQL APIを実行していてもスキーマSDLで表現されるため、言語に依存せずにスキーマを共有できる。

クライアントが参照するエントリポイントはQuery, Mutation, Subscription*1のいずれかで定義される。

GraphQLの定義済みスカラー型(Stringなど)か、ユーザー定義の型を使用できる。

スキーマの例:

type Shop {
  name: String!
  # Where the shop is located, null if online only. location: Location
  products: [Product!]!
}
type Location {
  address: String
}
type Product {
  name: String!
  price: Price!
}
type Query {
  shop(id: ID): Shop!
}

引数

フィールドは引数を持つことができる。↑の例のshopクエリもidという引数を持っている。

引数は、定義済みスカラー型か入力型(Input Type)で定義される。(入力型は前章の型とは別物。inputキーワードで定義される)

type Product {
  price(format: PriceFormat): Int!
}
input PriceFormat {
  displayCents: Boolean!
  currency: String!
}

変数

クエリ文字列と変数を一緒に送信し、GraphQLサーバー側で処理させることができる。変数を一緒に送信する場合はオペレーション名は省略できない。

query FetchProduct($id: ID!, $format: PriceFormat!) {
  product(id: $id) {
    price(format: $format) {
      name
    }
  }
}

以下のように送信

{
  "id": "abc",
  "format": {
    "displayCents": true,
    "currency": "USD"
  }
}

エイリアス

エイリアスの例はGraphQL公式ドキュメントがわかりやすかったので引用。

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

https://graphql.org/learn/queries/#aliases

hero型のオブジェクトを2件習得する際に、このようなエイリアスをつけることで両者を区別できる。

ミューテーション

ここまでの例はいずれもクエリでのデータ読み取り処理だった。

データの書き込み・変更にはミューテーションを使う。クエリをtype Query配下に定義するように、ミューテーションのエントリーポイントはtype Mutation配下に定義。

type Mutation {
  addProduct(name: String!, price: Price!): AddProductPayload
}
type AddProductPayload {
  product: Product!
}

以下のようにmutationキーワードをつけて呼び出す

mutation {
  addProduct(name: String!, price: Price!) {
    product { 
      id
    }
  }
}

ミューテーションには、クエリと違い以下のような特徴があります。

  • ミューテーションは副作用を持ったり、修正を加えることが許される
  • ミューテーションはサーバーによってシリアルに実行されなければならない。Query, Subscriptionは並列に実行することができる

Enum

いくつかの決まった値のいずれかを返すフィールドについて、Enum型で値のセットを定義することができる

type Shop {
  # The type of products the shop specializes in type: 
  ShopType!
}
enum ShopType {
  APPAREL
  FOOD
  ELECTRONICS
}

抽象型

抽象型には、InterfaceとUnionがある。

Interface

interface Discountable {
  priceWithDiscounts: Price!
  priceWithoutDiscounts: Price!
}
type Product implements Discountable {
  name: String!
  priceWithDiscounts: Price!
  priceWithoutDiscounts: Price!
}
type GiftCard implements Discountable {
  code: String!
  priceWithDiscounts: Price!
  priceWithoutDiscounts: Price!
}

フィールドの型をDiscountableとすることで、ProductまたはGiftCardいずれかを返すことができる

type Cart {
  discountedItems: [Discountable!]!
}
query { 
  cart {
    discountedItems {
      priceWithDiscounts
      priceWithoutDiscounts
    }
  }
}

interfaceで定義されていない型を取得したい場合、型を明示する必要がある

query { 
  cart {
    discountedItems {
      priceWithDiscounts
      priceWithoutDiscounts
      ... on Product {
          name
      }
      ... on GiftCard {
          code
      }
    }
  } 
}

Union

Union型は、フィールドが返す可能性のあるオブジェクトのセット。unionキーワードで定義する

union CartItem = Product | GiftCard
type Cart {
  items: [CartItem]
}

Unionに含まれる型は必ずしも同じフィールドを持つとは限らない。 返される具象型(interfaceでないもの)に対して、それぞれ明示的に取得する値を定義する必要がある。

query { 
  cart {
   items {
      ... on Product {
        name
      }
      ... on GiftCard {
        code
      } 
    }
  } 
}

Fragment

↑で型指定のために使われている… on Product はinline fragment。

fragmentは、部分的なクエリを定義して再利用できる。 以下はGraphQL公式ドキュメントから引用。

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

https://graphql.org/learn/queries/#fragments

Directives

GraphQLでは組み込みで@skip@includeというディレクティブが定義されている。

@includeは、以下のように <フィールド> @include(if <変数>)という形で指定しできる。 この例ではshouldIncludeパラメータがtrueの場合のみmyFieldがクエリされる

query MyQuery($shouldInclude: Boolean) {
  myField @include(if: $shouldInclude)
}

(@skipはその逆。trueが指定されたらそのフィールドを取得しない)

また、カスタムのディレクティブも定義することができる。
PythonのGraphqQLライブラリStrawberryのドキュメントのサンプルがわかりやすかったので引用。

strawberry.rocks

query People($identified: Boolean!) {
  person {
    name @turnUppercase
  }
  jess: person {
    name @replace(old: "Jess", new: "Jessica")
  }
  johnDoe: person {
    name @replace(old: "Jess", new: "John") @include(if: $identified)
  }
}

ディレクティブを付けたフィールドに対し、任意の処理をさせることができる。

Introspection

GraphQLの特徴的な機能がイントロスペクション。クライアントはスキーマに関する情報を取得することができる。 以下は、スキーマで定義されている型名を取得するクエリ。

query {
  __schema {
    types { 
      name
    }
  }
}
{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        }, 
        {
          "name": "Product"
        },
      ]
    } 
  }
}

その他の取得方法については、公式ドキュメントにクエリ例があった。

graphql.org

まとめ

この章ではGraphQLのクエリの作り方について学んだ。
仕事ではGraphQLサーバー側を担当しており、クライアント側で呼び出し方を工夫する、試行錯誤するという経験がないため、クエリの作り方に関して知らないことも多かった。
エイリアスを定義できることなどカスタムのdirectiveを作れることなど)

次の章はスキーマ設計に関する章。
普段開発をしている中で、チーム内でも「設計がGraphQL的かどうか」という観点で議論がされることが多い。 RESTとの考え方の違いを学んでいきたい。

*1:GraphQLサーバーとの接続を保ちながら、リアルタイムにデータの変更をクライアントに通知できる。Subscriptionに関してはこの本にも公式ガイドにも記述がなく情報が少なそう。仕様: 6.2.3 Subscription | GraphQL

デュアルキーボード環境を構築した話

最近デュアルキーボードを導入したので、環境構築方法の紹介や使ってみた所感です


目次

動機

年のせいか最近肩こりや腰痛に悩むようになり、定期的にストレッチしたり整体にいく等のケアをするようになりました。
その一環として、PC使用時の姿勢も改善しようといろいろ調べるようになりました。

私はソフトウェアエンジニアの仕事をしており、最近は常にテレワークなので仕事中はほぼPCの前にいますが、背中は丸まり肩も縮こまり「綺麗な姿勢」とはかけ離れています。

まさにこんな感じ

椅子の高さ等を変えてみたり、ノートパソコンのディスプレイを使用するのをやめクラムシェルで作業するようにしたりと作業環境を変えてみているのですが、いろいろ調べる中で知った概念がデュアルキーボードです。

デュアルキーボード vs 分割キーボード

デュアルキーボードとは、その名の通り2つのキーボードを並べて使用することです。 デュアルキーボードを知る前段階として、「分割キーボードが姿勢改善にいいぞ」という記事を目にしました。

分割キーボードという概念自体はどこかで聞いたことがありましたが、なるほどそんなメリットがあるのね、どれどれ導入しようかしらとAmazonで調べたところ価格帯が2万円弱〜という世界。お試しで導入するには厳しい。。

これってキーボード2つ並べても一緒じゃない?でもそんな奴おるんか?と「キーボード 2つ」でググったところ、下記のデュアルキーボードの紹介記事を見つけ、見たところ悪くなさそうだったので導入を検討することにしました。

developer.aiming-inc.com

購入したキーボード

早速Amazonでキーボードを探してみました。

条件として、

  • mac対応
  • US配列
  • 安価(10000円以内)
  • ワイヤレス
  • テンキーレス

の5つをクリアして欲しかったのですが、下記のAnkerのキーボードが条件に合致。

amzn.to

購入時点で2000円、2つ買っても4000円と安価だったため即決し2つ購入しました。

正直見た目の安っぽさは否めませんが、薄く軽く持ち運びにも便利です。

ファンクションキー問題

大きな難点として、F1 ~ F12 のファンクションキーを使用するときFnキーと同時押し必要がある仕様となっていました。Mac側の設定ではどうにもならないらしい。

それどころか、アプリによってはFnキーと同時押ししてもファンクションキーとして機能していません。(これはアプリ側の問題かもしれない) 意図せず突然音楽を流し始めるspotifyに何度驚かされたことか。

前述のファンクションキーが機能しないアプリについては、姑息な手段ですがファンクションキーを使うショートカットをファンクションキーを使わないものにリマップしてとりあえず解決しました。 私はそこまでファンクションキーを多用しないので現状困っていませんが、ファンクションキーを多用する人にはストレスが大きいのではないでしょうか。

キーボードの買い替えも一度検討しましたが、前述の5条件を満たす安価なキーボードが見つからず断念しました。 なお、Magic Keyboardは10000円程度なので、1つだけ購入して片方だけでもMagic Keyboardとすればファンクションキーの問題は解決するなあ、とは思っています。

感想

さきほど挙げた紹介記事と重複する部分も多いですが、1ヶ月ほど作業してみた感想です

メリット

姿勢がよくなる

劇的に肩こりが改善するようなことはありませんが、1つのキーボードを使用しての作業と比べ、肩が開きいい姿勢を保てていると感じます。

導入費用が安価

繰り返しになりますが、分割キーボードの購入と比べてかなり安価であるため、「合わなかったら辞めればいいや」と軽い気持ちで導入できました。

トラックパッドが中央に来る

私はドラッグアンドドロップの時に両手を使うんですが、キーボード1つ+右側にトラックパッド、という配置と比べて左手の移動量が少なくすみます。 昔一度、macbookのようにキーボードの手前にトラックパッドを置く配置を試したことがありますが、キーボードをトラックパッド越しに触るのがすこし嫌で今まで横に配置していました。 デュアルキーボードであればトラックパッドと干渉せずキーボードを触ることができ、小さいですが地味に嬉しいポイントです。

デメリット

場所を取る

キーボード2つ+マウスorトラックパッドを並べることになるので場所を取ります。 デスク上の作業領域の制限から横1列に並べるのは厳しく、キーボードをハの字においた中央にトラックパッドを置いており、横方向にも奥行き方向にもかなり場所を取ります。

ホームポジションへの復帰

1度トラックパッドを触りに離れた右手がキーボードに復帰する時、キーボード1つの場合は左手との距離感からホームポジションに自然に復帰できますが、デュアルキーボードの場合は左手と距離があるため一発で復帰するのが難しいです。
単純に慣れの問題もありそうですが。
現状、Jキーの突起をゴソゴソ探したり、すぐ見つからなければ目視で確認することも少なくないです。これは少しだけストレス。

好奇の目で見られる

言うほどのデメリットではないですが、見た人からは「なんで2つキーボード並べてるの」と聞かれることにはなるでしょう。 テレワークなので家族以外には特に見られていませんが。

その他

総評

  • 劇的なメリットはないが、今すぐ元のシングルキーボード(?)に戻すデメリットもない

デュアルキーボード用の設定

デュアルキーボード環境のために設定した項目のメモ

キーボードをまたいでのキー同時押し有効化

デフォルトではキーボードをまたいだshiftキーやcommandキーとその他のキーとの同時押しを認識してくれないため、Karabiner-Elementsというツールを使用しています。

様々キーボードの設定を変更できるツールですが、同時押し問題に関してはインストールするだけで解決しました。

その他キーボードまわりの設定

CapsLock ←→ Control キー交換

JIS配列のControlキーの位置に慣れているため、US配列のキーボードを使用する際はCapsLockとControlをリマップしています。
リマップは、Macのシステム環境設定→キーボード→修飾キー からできるのですが、Karabinerを使う場合、設定対象のキーボードにkarabiner~を選択する必要があります。

左右のcommandキーを英数/かなキーに

US配列キーボードを使用する際は左右のcommandキーを英数/かなキーとして使用しています。
いままで英かなというアプリを入れていましたが、Karabinerで同様の設定ができるようなので英かなをやめてこちらで統一するようにしました。

個別アプリ内でファンクションキーを使うショートカットのリマップ

PyCharmとVSCodeでファンクションキーを使っていたので、普段使うものはcontrol+数字キーにリマップしました。
F1 ~ F12キーもFnキーもホームポジションから押しづらいので、慣れれば作業効率も高まりそう。

おわりに

「感想」のあたりでも触れたように、今のところ強烈なメリットは感じておらず、正直デュアルキーボードを爆推しするつもりはありません。
少しでも姿勢改善につながるなら何でも試したい、という方は一度試してみる価値はあるかもしれません。