お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
開発::その他ジャンル::仕事

リーダーをするときに意識してることを簡単に書いてゆく。大まかには開発リーダー(チームリーダーレベル)の話。

先を見て動く

先を見て詰まりそうな要素があれば、関係各所と調整するなどで予め詰まらないように動いておくことで、メンバーの手を止めないように立ち回ることができる。

もし詰まることがあればメンバーを遊ばせることになるし、メンバーも好調にやっていたものがいきなり詰まれば不満や不信感を覚えるので、モチベーション低下につながり、これはプロジェクト運営上あまりよくない。

他にもメンバーの懸念を定期的に聞き取り、つぶしておくのも有用だと思っている。

メンバーの心理的安全性を保つ

メンバーの言うことは基本的に信頼し、否定しない。もし改善できるようであれば肯定したうえで、よりよい手法を示す。相手が腹落ちするまで話すなどで信頼を得る。

信頼が得られている場合、メンバーは言いたいことを言えずに抱え込んだり、裏で愚痴を言ったりといった行動が減り、言ってくれるようになるためそういった意見を受け止めて、プロジェクトを改善することができる。

特に相手の気を削ぐ、言葉を言わないように気を付けている。例えば「このプロジェクトは特殊だから」「そういう決まりだから」「どうしようもないことなんだ」はその一例だ。

暇にしておく

リーダーがパンクしているとメンバーが声を掛けづらかったり、声を掛けられても十分に対応できなくなることがある。結果としてメンバーの信頼を失うと、心理的安全の喪失やメンバーのモチベーション低下が起きるため、極力暇にしておく。全く何もしないというわけではなく落ちてるボールを拾ったりレビューに参加したり、今後のことを考えたり、締め切りのないタスクをやっていく感じだ。

具体的にどう暇にするかというとサブリーダーを立て権限を委任する、メンバーにも可能な範囲で権限を委任しておくなどで、自分で極力タスクを持たないようにする。

勿論いきなり委任してもメンバーはこなせないことがあるため、こなせるようにあらかじめ必要な知識をレクチャーしておくのがいい。例えば適切な設計や実装方法を何故それをするのかという意図から話しておくことで、設計作業を委任できる。

サブリーダーが必要な場合ではMTGに同席してもらい、動きを見てもらう、次に徐々に自分の代わりに意見してもらったり、タスク振りをしてもらうなどで慣れていってもらうことで、自分の役割を委任することができる。

間接的に評価する

メンバーのモチベーションが高いほど作業がスムーズに進むため、普段からモチベーションを保ってもらうために間接的な評価を行う。

例えば誰か別の人と話すときに「Aさんはとても作業品質が良い」などと評価を伝えておくと本人が何かの切っ掛けで小耳にはさんだ時にモチベーションが上がるきっかけになるため、誰かを評価できるシーンがあれば積極的にしておく。

こうしておくことで回りからも、その人物の評価が上がり、本人が小耳にはさむことがあれば直接評価よりも自信が持てると考えている。

投稿日:
開発::設計言語::TypeScript

今回は実際の開発現場で遭遇しそうなコードパターンを2つ挙げ、それらを主に可読性向上と保守コスト削減の視点から、どのように改善できるかを具体的に書いてゆく。

サンプルコードはTypeScript + GraphQLを使ったAPサーバーを想定して書いている。

命名とデータ構造の最適化

この項目ではエラーメッセージを返すサーバー側の処理について記述する。

元コード

resolvers/customer-info/helpers/invalid-reason.ts

type ReferencePageLinkMapKey = 'userFormIssue' | 'duplicateUserName' | 'someErrorMessage';

// exportされているがgetInvalidInfo(key, boolean)でreferencePageLinkMap.get(key)が返ってくることを確認するテストに使われているだけ
export const referencePageLinkMap: Map<ReferencePageLinkMapKey, string> = new Map([
  ['userFormIssue', 'https://help.example.com/23843ddvwa'],
  ['duplicateUserName', 'https://help.example.com/xdxdk2t5'],
  ['someErrorMessage', 'https://help.example.com/pp33s'],
  ...
]);

const invalidReason = {
  inputIssue: {
    message: '入力項目にエラーがあります。...',
    links: [
      {
        label: '【登録時のヘルプページをご確認ください】',
        url: referencePageLinkMap.get('userFormIssue') ?? '',
      },
    ],
  },
  ...
};

export const getInvalidInfo = ({
  errorReasonType,
  isModeA,
}: {
  errorReasonType: ErrorReasonType;
  isModeA: boolean;
}): Omit<SomeFormInvalidInfo, 'invalidReason'> => {
  switch (errorReasonType) {
    case 'INVALID_INPUT':
    case 'SOME_INPUT_ERROR':
      return invalidReason.inputIssue;
...
   };
}

このコードの問題点

  • 定義する必要性が希薄な型情報があり、改修コストが高い
  • 変数や関数名、キー名などから相関する機能の関連性を読み取りづらい
  • 定義されたスコープを超えて利用されているもので命名から機能の読み取りが難しいものがある
  • リテラルがべた書きされており将来のリファクタリングに支障がある
  • 不必要にMapが使用されており、実装が冗長になっている
  • 静的な値の取得にNull合体演算子が使用されており、潜在不具合となりやすい
  • 型情報がべた書きされすぎており、見通しが悪い
  • 複数のフラグ値の正規化を利用側で行っており、複雑になっている

改善後コード

リソースファイルと実装が一緒くたであると読みづらいためファイルを分割する。パスも書いているがパスについては深く考慮できていない。

resources/messages/CustomerError.ts

export const ERROR_MESSAGES = {
  formValidationError: {
    message: '入力項目にエラーがあります。...',
    helpLinks: [
      {
        label: '【登録時のヘルプページをご確認ください】',
        url: 'https://help.example.com/23843ddvwa',
      }
    ],
  },
  ...
} as const;

まずtype ReferencePageLinkMapKeyは無駄に型の管理コストが増えるだけなので削除する。意味がないとは言わないが、型の追加削除によって発生するデグレードは普通は型検査でわかるためなくてよいだろう。

次にreferencePageLinkMapでは意味がわからないのでERROR_MESSAGESにリネームする。中身のキーもinputIssueでは分かりづらいのでformValidationErrorとし、フォームのバリデーションエラーであることが分かるようにする。linksについても何のリンクなのかわからないためヘルプのリンクであることを明示できるようにリネームする。

helpLinks.urlについては、共通のURLを参照する概念は普通ないか、あったとしても別に切り出して管理するのはコストなので直に書いてよいと考える。元のコードでもテストコード以外には使われていない。テストコードには実値を書かないと回帰テストとして機能しないため、この方式のほうがより良いと考える。

例えば以下のようなテストコードは回帰テストの観点では意味が薄い。

it('INVALID_INPUTの時にurlが正しい結果になること', () => {
  const invalidInfo = getInvalidInfo('INVALID_INPUT', false);

  expect(invalidInfo.links[0].url).toBe(invalidReason.get('inputIssue'));
});

これは以下のように書くことでページのURLが変更されたときにテストが失敗するため、より価値のあるものになる。但しこれはexpect(invalidInfo).toStrictEquals({ ... })の形で一括判定したほうが、テストコードの可読性の観点からより良いだろう。

it('INVALID_INPUTの時にurlが正しい結果になること', () => {
  const invalidInfo = getInvalidInfo('INVALID_INPUT', false);

  expect(invalidInfo.links[0].url).toBe('https://help.example.com/23843ddvwa');
});

ERROR_MESSAGESをリソース変数としてみなせば、これはべた書きというより定数定義とみなせるため、基本的には問題ないと考える。勿論これは同じURLを持つものが大量にあるなど、ケースによっては考慮の余地はあるだろう。

またMapをやめ、敢えて直値を書くことで万一存在しなかった場合に、元にあった以下の空文字が返ってきて機能しなくなる問題も解決している。そもそも通る余地がないロジックなので存在しなくてよい。

url: referencePageLinkMap.get('userFormIssue') ?? ''

またERROR_MESSAGESas constを付与することで、意図しない破壊が起きる可能性を減らすことができる。

handlers/error/GetCustomerInfoError.ts

export const getCustomerInfoError = (errorType: CustomerErrorType) => {
  switch (errorType) {
    case CustomerErrorType.INVALID_INPUT:
    case CustomerErrorType.SOME_INPUT_ERROR:
      return ERROR_MESSAGES.formValidationError;
...
   };
}

まずgetInvalidInfo()getCustomerInfoError()にリネームすることで意味を分かりやすくしている。次に複雑な型情報を除去してシンプル(= (errorType: CustomerErrorType) =>)にする。errorTypeerrorReasonTypeisModeAを合成した結果を渡すことを想定している。これによって、この関数内の複雑性を減らすことができる。またCustomerErrorTypeはENUMなので、case 'CODE1':のようにリテラルべた書きを回避できるうえ、仕様変更などで値が変わった時にも対応がしやすくなる。

また戻り値の型も削除する。これは静的解析により自明であるほか、Omit<SomeFormInvalidInfo, 'invalidReason'>とあるが、実際に返しているのはinvalidReasonであり、関連性がないためだ(おそらく、偶々よく似た型を転用しているのだろう)。またこの事例では更にinvalidReasonという変数名があり、Omitの内容と視覚的に競合し、コードの読解が難しいため、この修正により可読性が増す。

また改修前はエラーメッセージオブジェクトのキーと、ヘルプページのキー名、そしてswitchのキーの全てが食い違っており、見ていて混乱する内容だったが、ヘルプページのURLをべた書きするように変えたため、混乱する要素を減らせている。

まとめ

コードの記述量を減らし、長大なコードを別々のファイルに分けることや、過度に分割しすぎないことによって可読性が向上し、コード量が減ったため保守コストも削減できたと考えている。

半面、静的解析のコストは上がっているが、静的解析のコスト増よりも可読性がよく、保守コストが低いコードのほうが効率的な開発に寄与するだろう。

また静的な値を取得するためにMapを使い、TypeScriptを利用しているため、理論上undefinedが返ってこないのに、それを期待するロジックを削除することで、コードを読んだ人が混乱する可能性も削減している。

型安全で見通しの良いエラーハンドリング

この項目ではエラーメッセージを返すサーバー側のエラーハンドリングについて記述する。

元コード

try {
  // API呼び出しなど
} catch (error) {
  if (error instanceof ApolloError) {
    if (error.graphQLErrors[0]?.extensions?.ERROR_KBN === 'ERR_XYZ') {
      // エラー処理
    } else if ...
    ...
  }

  if (error instanceof SomeError) {
    ...
  }
  ...
}

このコードの問題点

  • エラーハンドリングが長々とべた書きされていて見通しが悪い
  • 配列の添え字が直に指定されているが意図が読み取れない
  • 例外処理なのにOptional chainingが多く確実な例外処理に支障がある(例外処理で例外が発生するリスクは最小限にする必要がある)
  • extensions配下の型情報が暗黙的でわからない
    graphQLErrors: ReadonlyArray<{
     extensions: {
         [attributeName: string]: unknown;
     }
    }>
    

改善後コード

const handleApolloError = (err: ApolloError) => {
  if (err instanceof ApolloError) {
    // graphQLErrorsが空であれば例外をスローし、そうでなければgraphQLErrorsを返す
    const gqlErrors = parseGQLErrors(err.graphQLErrors);
    // graphQLErrorsの中身をパースし、意味のある型を付けて返す
    const extensions = parseThisFunctionErrors(gqlErrors);
    if (extensions.ERROR_KBN === 'ERR_XYZ') {
      // エラー処理
    } else if ...
  }
}

try {
  // API呼び出しなど
} catch (error) {
  if (error instanceof ApolloError) {
    handleApolloError(error);
  } else if (error instanceof SomeError) {
    ...
  }
  ...
}

べた書きされているとコードの見通しが悪いため、catch句の中では例外種別ごとにハンドリングする処理に飛ばし、そっちで処理できるようにする。

error.graphQLErrorsは要素が0の場合があるため、parseGQLErrors()のような共通関数を作り、要素があれば中身を返す、なければ例外をスローして、より上位の処理に飛ばすなどの処理を共通的に行うようにする。この仕組みを共通化することで、この要素に対する処理の一貫性を持たせることができる。

またgraphQLErrorsの詳細については処理によって内容が異なるであろうことから、ドメインごとにparseThisFunctionErrors()のような関数を作り、その中で適宜データを整形するのが望ましいだろう。

そうして結果的に意味のある型情報を持ったextensions、あるいは適当な結果情報をハンドリングすることで、型安全かつ、責務が別れ、疎結合な実装に寄与する。

この形式であればhandleApolloError()は必要に応じて別ファイルに切り出し単体テストを書くこともできるし、このままでもあっても規模が小さければ十分テスト可能だろう。

なお、parseGQLErrors()parseThisFunctionErrors()の具体的な内容については今回は省略する。

投稿日:
開発::設計ライブラリ::Next.js

Next.jsの全体設計を考えるときに疎結合性やテスト容易性を達成するときに考えているアーキテクチャについて簡単に書いてみる。

Page Router向けに作っていて、API Routesについては考慮していない。過去にこれに近い設計で開発していたことがあったが、単体テストによるデグレードや不具合、仕様漏れの検出はよくできていたと思う。今回書いたものは過去に考案し、開発していたもののブラッシュアップになる。

アーキテクチャ図

基本的に各レイヤー間はTypeScriptのtypeで仕切り、依存性を逆転させることで、テスト容易性や疎結合性を重視している。

next-js-arch.png

ディレクトリ構成

アーキテクチャ図にないlibrariesが登場するが、これは汎用的な共通処理だ。

src
  ├─adaptors
  │  └─User
  │     ├─index.spec.ts
  │     └─index.ts
  ├─components
  │  └─form
  │      └─TextInput
  │         ├─index.spec.ts
  │         └─index.ts
  ├─libraries
  │  └─HttpClient
  │     ├─index.spec.ts
  │     └─index.ts
  ├─pages
  │  └─hoge
  │     ├─ServerSideProps.ts
  │     ├─ServerSideController.spec.ts
  │     ├─ServerSideController.ts
  │     └─index.page.ts
  └─usecaes
      └─hoge
         ├─controller.spec.ts
         ├─controller.ts
         ├─state.spec.ts
         ├─state.ts
         ├─style.scss
         ├─usecase.tsx
         ├─view.spec.tsx
         └─view.tsx

登場する各要素について

Page

画面本体。UsecaseとgetServerSidePropsを配置し、その橋渡しを行うだけの存在。

Usecase

StateとController、Viewを橋渡しする存在。

画面全体を別物にすり替える場合もここで行う。

useEffect()はここに書くが、中のロジックはController側に書く。

一見するとファサードであり、pageにべた書きしてもいいような内容だが、コードを書く時のコロケーションの観点から敢えて分離している。

また、画面のページレイアウトが全く別物になるなど劇的な変化がある場合は、この階層で分岐制御(ユースケース別の切り替え)する。

State

useState()で作った状態を定義する場所。それ以外は何もしない。

Controller

イベントハンドラによる処理を配置する場所。APIコールもここから行う。

状態については、typeを経由してState側で宣言した状態を注入して利用する。

状態を外部から注入するため、状態変化時のテストがしやすい。また画面からロジックをはがしているため、ロジック単体のテストが可能。

Adaptor

APIを呼ぶだけの存在。データの加工や例外ハンドリングは呼び元で行う。

APIを呼ぶだけの責務とすることで、複数のコンポーネントから呼ばれたときに同じAPIを呼ぶコードが重複したり、呼び出し元によってデータ加工手法を分けるなどの煩雑な実装を回避するのが目的。

テストとしては引数や戻り値、呼び出し方法が実装時から変わっていないかを見る観点のみあれば回帰テストとして機能する。

View

ほぼ純粋なJSXを書く場所。ロジックは原則として書かない。booleanを使ったDOMの切り替えは記述してよい。

制御はUsecaseでStateを合成したControllerで行う。

表示非表示の分岐のみにすることでtesting-libraryを利用したJSXの表示切替を単体テストとして実装できる。

UI Component

TextInputみたいな細かいパーツや、再利用されるフォームUIなど、UI系の共通部品。

基本的に状態は持たないが、無限ループが起きず、再利用されない状態(親に渡す必要がなく、自分自身に閉じた状態)については持ってよいと考えている。例えば、OK/CancelのあるモーダルでOKが押された時だけ呼び元に返す状態は持ってよい。

View, Controller

ページコンポーネント向けの内容に準ずる。Usecaseを持つほど大規模なコンポーネントはないと思うので、Usecaseなしで繋ぎ合わせてよいと考えている。

ServerSideProps

getServerSideProps()の中身。Page側では以下のようにして呼び出す想定。

import { execServerSideProps } from './ServerSideProps';

export const getServerSideProps = (async () => {
  execServerSideProps();
});

ServerSideController

ServerSidePropsの中で利用するロジック。APIを呼ぶ場合はAdaptorとも繋がる。

利点

  • 各レイヤーやコンポーネントでの単体テストが容易
  • MVC的な構造のため理解しやすい
  • SOLID原則で得られる利益を享受しやすい
  • レビュー時に意図しないファイルに差分があった場合に、問題を検知できる
  • コードマージ時のコンフリクト範囲を限定しやすい

欠点

  • ボイラープレートコードが増える
    • とはいえ、DDDよりは少ない
  • Modelに相当するものがないため、ControllerがFatになる。またModel処理の共通化ができない
    • Modelをどこに配置すべきかを検討できていない
  • typeに破壊的変更が起きると数珠繋ぎに修正が必要になり、コストが重い
    • その代わり型で各コンポーネントの関係性がわかる利点もある

あとがき

構想自体は4年前に考えたものだがアウトプットができていなかった。まだ煮詰まっていない上に考慮出来ていない部分もあるが、AppRouterの登場からだいぶ経ち、陳腐化してきそうだったので、取り敢えず吐き出した。

最近思っているNext.jsを使った画面設計に関する考えを箇条書きで雑に殴り書きしていく。この記事は考えの垂れ流しなので深い説明はしない。AppRouterではなく、PageRouterの考え。

  • TypeScriptで実装し、型が騙せるような実装は極力避け、コードによる戻り値の型指定は不具合の原因になることがあるため、可能な限り型推論に任せる
  • SOLIDな設計を意識することで疎結合でテストしやすい設計になる
  • Clean Archtectureを意識することでSOLIDのSを意識しやすくなる
    • 画面として考える場合、実装レイヤーとしてはAPIを呼ぶ以外何もしないAdapter、ビジネスロジックやイベントハンドリングの実処理などを行うController、画面要素を配置しただけのView、画面状態を保持するState、それらをつなぎ合わせるUsecaseが、Usecaseを置くだけのPage(Next.jsのpageコンポーネントに埋め込むコンポーネント)、SSGやSSRをする場合のServer Side Controllerがあるとよいと考えている。大まかには下図のような感じで考えていて、過去の実務でもこれに相当するものを作ったことがある。
      think-archtecture-diagram.png
    • ただこれはModelに相当するものがなく、ビジネスロジックの共通化に課題が出てくるのと、ControllerがFatになりすぎると考えており、そこが課題になると考えている。
  • テストが容易なコードは必然的に疎結合になる
  • 疎結合にする場合、命名を抽象的にしておくと処理の入れ替えが容易になる(命名が具象、つまり実装の詳細に依存しないため)
  • 疎結合にするとパーツが増えるので認知負荷が上がる
  • 疎結合でかつ、命名が抽象化されている場合、仕様を知らない人にとっては実際の処理内容を推測しづらくなる
    • つまりこれは属人性が増えると考える
  • 例外についてはErrorクラスを継承し、カスタム例外を作成して、用途に応じたハンドリングができるようにする
    • 原則として処理を止める場合にのみ用いるべきで、続行する場合には使わない
    • 例外は原則としてスローして、カスタムエラーは特定の階層でフィルタしてハンドリング、全てすり抜けてきたものはルート処理でキャッチしてハンドリングすることで、取りこぼしをゼロにする。例外の握り潰しは原則行わない
      • 基本的にすべてロギングする
    • 処理を継続するものについては例外とせず、ワーニング用の処理フローを作成し、それに則って行う(例えば入力バリデーションはワーニング)
投稿日:
開発::Webジャンル::調査Webサービス

スマホでWebを見てるときにキーボードがUIにかぶって操作しづらくなることがあるので、いくつかのサイトでどうなっているか調べてみた。

去年の9月にAndroid Edgeで調べた内容なので、今とは事情が異なるケースもある。Android Chromeでは起きなかったケースもあったので、Edge特有の挙動と思われる。

Twitter

ログイン画面

キーボードが入力欄やログインボタンにかぶる。

tw1.jpg

ミュート設定

入力欄が上にあるためかぶらない。

tw3.jpg

投稿画面

入力欄が上にあるためかぶらない。

tw4.jpg

YouTube

コメント画面

キーボードがボタンにかぶる。

yt1.jpg
yt2.jpg

通報モーダル

キーボードがボタンにかぶる。

yt3.jpg
yt4.jpg

GitHub

Issueのコメント画面

入力欄が丸ごとキーボードにかぶる。

gh3.jpg
gh4.jpg

一休

検索モーダル

宿泊予算の入力が丸っとかぶる。

ikyu1.jpg
ikyu2.jpg

じゃらん

検索画面

キーボードと被らないようにするためか入力UIをモーダルにして画面トップに出すように工夫されている。これなら大抵の端末やブラウザで対応できそうなので、よくできていると思う。

jara1.jpg
jara2.jpg

Amazon.co.jp

レビュー画面

入力状態になると若干のラグの後に画面下に余白ができ、入力状態が外れると同様のラグの後、画面下の余白が消えるという挙動をする。

割と凝ったJSで何かしらの計算を行い、かなり頑張って調整しているようだった。タイムラグがあるのはイベント発火もあるだろうが、キーボードの検出や画面サイズに応じた余白計算に時間がかかっているのもあるのだろう。

ここまで凝った実装をしているのは他のサイトでは見られず、Amazon.co.jp特有に見えた。なお、Amazon.comのほうは見ていない。

あとがき

この調査時点では、じゃらんとAmazon.co.jpを除き入力欄を画面の上部に配置するなどレイアウトで調整しているサイトが比較的多く、どうしてもボタンなどが隠れる傾向があるように思った。

じゃらんは強制的に画面上部に入力欄を出すようにし、Amazon.co.jpは気合でキーボードが隠れないように調整していて、腐心の跡が見られた。

なお、今回動画を作成するにあたり一部をぼかす必要があったため、やり方を調べ実践したのでAviUtlで動画の一部にモザイクをかけ、動かす方法という記事を作成し、その過程を残している。

本動画の作成過程では上手くモザイクをかけられなかったが、上記の記事を作っているときに上手く行くようになったので、本記事の動画はモザイクではなく、ぼかしとなっている。