SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

React Drag & Drop メモ

React Drag & Drop メモ

主要ライブラリは3つ

※2022/5 末時:React18 で実現したければ React DnD を使うか自前で実装するかがよさそう

各ライブラリ所感

React DnD

  • drag & drop 中の見た目:半透明になるのでよい
  • シンプルなものであればこれで十分
  • HTML5API setDragImage を利用して作られているためパフォーマンス面もよい
  • 多少凝ったものを作ろうと思うと大変そう(例えばかんばんボードの複数のタスクが入っている枠の方を Drag できるようにする等。そもそもできるかまで調べられていない)

react-beautiful-dnd

  • drag & drop の見た目:リッチ
  • 2022/5 末、React18 には対応していない(React18 で動かしたければ React.StrictMode を消せば動くらしい?そんな状態では個人なら自己責任で済むが製品等には使えない)。対応は水面下?で進められてはいそうだが、まだ先になりそう (使いたければ React17 を使うしかない)
  • 多少凝ったものでも容易に作れそう(かんばんボードの複数のタスクが入っている枠の方を Drag できるようにするのは容易にできる)

React-Draggable

  • 最低限だけの可能性があるので、必要なものは自分で実装をする必要がありそうで大変そう(以下できるかまでは未確認)
    • サンプル見た限りは Drag & Drop しかできないため、上記2つだと自動でやってくれる位置調整等も必要なら地震で実装する必要があるかもしれない
    • 見た目は頑張ってカスタマイズする必要があるかもしれない

その他:自前で実装(OSS に依存したくなく腕があるなら)

参考(コードリーディング)

以下のコードを読んでみた

目的:React DnD 使用した場合の、よくあるかんばんボードの Todo、Done といった Group 間の Card 移動をどうやって実現してるのか確認したかったため

※Group、Card という表現は適切ではないかもしれないが上記のコードと用語を合わせるために揃える

※以降、ディスる意図等は一切なく単純に思ったことを書き留めるだけのためご容赦を

  • Group 間で Card は跨いで移動する = アプリ全体で参照するから ベースとなるコンポーネント:App.tsx で管理している → 分かる
  • Card の一覧を 1 配列で管理、Group と属する Card を別オブジェクトで管理
    • Card 一覧の配列内は、Group 順に並べていて、Group コンポーネントに Card 一覧の配列内の各 Group の先頭インデックス(firstIndex) を渡すことで、Card 一覧の配列での管理を実現している -> これが分かりにくかった
      • map(key: Group 名、 value: Card の配列)で管理した方がいいのではないか?
      • もしくは、Group の情報を持つデータクラス?を用意した方がよいのではないか?(必要ならそれらを管理するクラスも)
      • firstIndex より Group 名等 各 Group を識別する一意な値を渡して制御するようにした方が分かりやすそう
      • ※リファクタして試せ
  • 以下のコードの「forward」「backward」のコメント制御が最初分からなった(開発者ツールでデバッグしてやっと分かったレベル)
    • 以下の部分は Group 間の Card 移動を担当させている (だからこその dragItem.group === groupType。変数名から変数に格納されるデータを誤解して、コードだけ見てもこの役割が掴かみきれなかった)
      • targetIndex: targetGroupFirstIndex
      • items: itemsInGroup とかの方がまだ伝わる?
    • Draggable.tsx は Group 内の Card 移動にまかせている
    • Group 間移動は、Group.tsx で移動先の Group の末尾に放り込むだけ(目には見えないが)。あとは Draggable.tsx で Drag 時の位置を変えてる

useRef を使っているコンポーネントを重ねての制御ができている(Group と Draggable)ので、Group 自体を drag&drop できるようにすることはできるかもしれない(ただし少々大変そう)

// Group.tsx
const [, ref] = useDrop({
  accept: ItemTypes,
  hover(dragItem: ItemWithIndex) {
    const dragIndex = dragItem.index;
    if (dragItem.group === groupType) return;
    const targetIndex =
      dragIndex < firstIndex
        ? // forward
          firstIndex + items.length - 1
        : // backward
          firstIndex + items.length;
    onMove(dragIndex, targetIndex, groupType);
    dragItem.index = targetIndex;
    dragItem.group = groupType;
  },
});

// data.ts
export type Item = {
  id: number;
  type: ItemType;
  group: GroupType;
  contents: Contents;
};
export type ItemWithIndex = Item & {
  index: number;
};

refs

ついでに面白そうなの見つけた(あまり更新されてないけど)

React コードフォーマット(ESLint + Prettier + TypeScript)

React コードフォーマット(ESLint + Prettier + TypeScript)

XXXX 番煎じ。3 年前に webpack で一から設定したことがあるが年月経っている上、React でやるのは初めてなので残す

Code Format

  • Prettier : コードスタイルのフォーマッタ。 ESLint とは異なり、コードスタイルのみをフォーマット
  • ESLint : 問題や構文エラーを検索し、コードスタイルをよりきれいにする

※max-len、no-mixed-spaces-and-tabs、keyword-spacing、カンマスタイルなどのルールは、Prettier でよく使われているフォーマットルール。

※no-unused-vars、no-extra-bind、no-implicit-globals、prefer-promise-reject-errors などのルールは ESLint の一般的なルール。

ref: ESLint + Prettier + Typescript and React in 2022 ※eslint-plugin-prettier 使ってるので良い手順か怪しい

以下 npx create-react-app task-mgmt-app-clone --template=typescript を行ったあとの手順

ESLint インストール

npm install eslint --save-dev
npx eslint --init

Need to install the following packages:
  @eslint/create-config
Ok to proceed? (y) y

? How would you like to use ESLint? ...
  To check syntax only
  To check syntax and find problems
> To check syntax, find problems, and enforce code style

√ How would you like to use ESLint? · style
? What type of modules does your project use? ...
> JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

? Which framework does your project use? ...
> React
  Vue.js
  None of these

? Does your project use TypeScript? » No / Yes

? Where does your code run? ...  (Press <space> to select, <a> to toggle all, <i> to invert selection)
√ Browser
√ Node

? How would you like to define a style for your project? ...
> Use a popular style guide
  Answer questions about your style

? Which style guide do you want to follow? ...
> Airbnb: https://github.com/airbnb/javascript
  Standard: https://github.com/standard/standard
  Google: https://github.com/google/eslint-config-google
  XO: https://github.com/xojs/eslint-config-xo

? What format do you want your config file to be in? ...
> JavaScript
  YAML
  JSON

? Would you like to install them now? » No / Yes

? Which package manager do you want to use? ...
> npm
  yarn
  pnpm

↓ 必要なものを入れる

npm install -D eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
npm install -D eslint-plugin-import @typescript-eslint/parser eslint-import-resolver-typescript
npm i --save-dev eslint-plugin-unused-imports
npm i --save-dev eslint-config-airbnb-typescript
npm i -D eslint-plugin-import
// npx install-peerdeps --dev eslint-config-airbnb

Prettier インストール & ESLint 設定

npm i -D prettier eslint-config-prettier

eslintrc.js 編集。

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    'plugin:react/recommended',
    'airbnb',
+   'airbnb-typescript'
+   'airbnb/hooks',
+   'plugin:@typescript-eslint/recommended',
+   'plugin:@typescript-eslint/recommended-requiring-type-checking',
+   'prettier',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
+    tsconfigRootDir: __dirname,
+    project: ['./tsconfig.json'],
  },
  plugins: [
    'react',
    '@typescript-eslint',
+   'unused-imports', // 使っていないimportを自動で削除用
  ],
+ "ignorePatterns": [
+   ".eslintrc.js"
+ ],
  rules: {
+   'no-use-before-define': 'off', // 関数や変数が定義される前に使われているとエラーになるデフォルトの機能off
+   '@typescript-eslint/no-use-before-define': 'off',
+   'import/prefer-default-export': 'off', // named exportがエラーになるので使えるようにoff
+   '@typescript-eslint/no-unused-vars': 'off', // unused-importsを使うため削除
+   'unused-imports/no-unused-imports': 'error', // 不要なimportの削除
+   'unused-imports/no-unused-vars': [ // unused-importsでno-unused-varsのルールを再定義
+     'warn',
+     { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
+   ],
+   "react/function-component-definition": [ // アロー関数以外受け付けない設定
+     2,
+     {
+       namedComponents: "arrow-function",
+       unnamedComponents: "arrow-function",
+     },
+   ],
+   'no-param-reassign': [ // パラメーターのプロパティ変更を許可
+     2,
+     { props: false }
+   ],
+   'import/extensions': [ // importのときに以下の拡張子を記述しなくてもエラーにしない
+     'error',
+     {
+       js: 'never',
+       jsx: 'never',
+       ts: 'never',
+       tsx: 'never',
+     },
+   ],
+   'react/jsx-filename-extension': [ // jsx形式のファイル拡張子をjsxもしくはtsxに限定
+     'error',
+     {
+       extensions: ['.jsx', '.tsx'],
+     },
+   ],
+   'react/react-in-jsx-scope': 'off', // import React from 'react'が無くてもエラーを無くす
+   'react/prop-types': 'off', // TypeScriptでチェックしているから不要。offにする
+   'no-void': [ // void演算子の許可
+     'error',
+     {
+       allowAsStatement: true,
+     },
+   ],
+ },
+ settings: {
+   'import/resolver': {
+     node: {
+       paths: ['src'],
+       extensions: ['.js', '.jsx', '.ts', '.tsx']
+     },
+   },
+  },
};

補足

  1. アロー関数

以下を設定しないと

"react/function-component-definition": [ // アロー関数以外受け付けない設定
  2,
  {
    namedComponents: "arrow-function",
    unnamedComponents: "arrow-function",
  },
],

以下の書き方ができない(ESLint に function App() の形にフォーマットされる)

export const App = () => (
  <div className="App">
    <header className="App-header" />
  </div>
);

ref: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/function-component-definition.md

  1. 不要 import

plugings に 'unused-imports' を入れ忘れると、import 文で unused-imports/no-unused-imports のエラーが出る

plugins: [
  'react',
  '@typescript-eslint',
  'unused-imports' // 使っていないimportを自動で削除用
],
  1. import 整列

refs

import の整列をしたいときは以下参照。(自身のプロジェクトのフォルダ構成に合わせて好きな順になるよう pathGroups を指定することになる)

OpenID Connect 概要まとめ (In progress)

OpenID Connect 概要まとめ (In progress)

随時追加

OIDC イメージ

OIDC = OAuth + IDトークン + UserInfoエンドポイント

  • IDトークン:認証
  • アクセストークン:認可用(有効期限は短い)
  • リフレッシュトークン:アクセストークン更新用(有効期限は長い)

SpeckerDeck - 30分でOpenID Connect完全に理解したと言えるようになる勉強会

IDトークンが分かれば OpenID Connect が分かる

OAuth 2.0 / OIDC を理解する上で重要な3つの技術仕様

OpenID Connect セッション管理 概要

gRPC (by golang)

gRPC (by golang)

動機:SOAのとあるシステムで、新規のデータを新規DBに持ち、かつそのデータを既存サービス&新設サービスの2つ(a) が利用するようになるため、新規のデータ群の管理を担う新規サービス(b)を立て、(a)(b)両者のやりとりに gRPC を用いるのが適切と思ったため Study

gRPC とは

Google が開発した RPC フレームワークで、gRPC を使うと異なる言語で書かれたアプリケーション同士が gRPC により自動生成されたインターフェースを通じて通信することが可能。 データのシリアライズには Protocol Buffers を使用。

RPC(Remote Procedure Call):

  • ネットワーク上の他の端末と通信するための仕組み。
  • 「クライアント−サーバー」型の通信プロトコルであり、サーバー上で実装されている関数(Procedure、プロシージャ)をクライアントからの呼び出しに応じて実行する技術
  • (RESTのようにパスとメソッドの指定ではなく)メソッド名と引数を指定する
  • (リソースと機能(関数)の紐づけがされるため、時折 REST API 設計で発生するリソースと機能のマッピングで困る点が解消される?)

利点/欠点

利点:

  • HTTP/2による高速な通信が可能。(データはバイナリデータでやり取りする仕様)
  • Protocol Buffersによるスキーマファーストの開発。protoファイルというIDLからコードの自動生成が可能。
  • 様々なストリーミング方式の通信が可能。

欠点:

  • クライアントとサーバの両方に特別なソフトウェアを導入しなければならない
  • クライアントとサーバが別環境の場合、protoファイルの変更の追随を解決しなければならない
  • gPRCで生成されたコードはクライアントとサーバのビルドプロセスに組み込まなければならない
  • HTTP2通信ができる環境が必要

適したケース

  • マイクロサービス間の通信
    • バックエンド間は恩恵が多く得られる
  • モバイルユーザが利用するサービス
    • 通信量削減
  • 速度が求められる場合

APIとの比較 (個人の主観)

  • 大量データ送受信
  • 仕様変更追従
    • REST APIの場合 (OpenAPI記述のyaml等から自動生成)
      • とあるサービスorデータストア(サーバ側)のAPI仕様に変更が入っても、それを利用する(クライアント)側に影響のない変更であれば即追従の必要はない(そのまま運用できる)
      • 裏を返せば、追従漏れを起こすリスクもある
    • gRPCの場合
      • 変更が入ったら、利用する(クライアント)側も即追従する必要がある?
      • 裏を返せば、即追従が必要なほどサービス同士が密な関係であれば、(即追従する必要があるため)追従漏れは起きず、有効に働くのではないか

環境構築

protocol buffer install (Windowsの場合):

https://github.com/protocolbuffers/protobuf/releases から zip取得。環境変数にbinのパス追加

Go plugins install:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

確認

protoc --version
protoc-gen-go --version
protoc-gen-go-grpc --version
go get -u google.golang.org/grpc

Protocl Buffers

# 利点 欠点
JSON ・あらゆる言語で利用可能
・複雑な形式(配列/ネスト)を扱える
・データスキーマを強制できない
・データサイズが大きくなりがち
Protocol Buffers ・型が保証される
・データサイズは小さい(バイナリ)
・複雑すぎる構造には不向き
・一部言語は未対応

.protoを作成 → 開発言語のオブジェクト自動生成 → (送信時データを)バイナリ形式へシリアライズ

.proto ファイル

書き方

  • message

  • tag

    • (Protocol Buffersは) フィールドをタグ番号で識別。一意な番号
    • 最小値:1,最大値:229 - 1 (536,870,911)
    • 19000~19999 は予約番号のため使用不可
    • 1~15番は1byteのためパフォーマンス良。よく使うフィールドを割り当てるのが吉
    • タグは連番にする必要がない
  • enum (列挙型)

    • タグ番号が0から始まる
  • 各種フィールド

    • repated: 配列相当のフィールド。複数の要素を含めることが可能
    • map:連想配列相当のフィールド。
    • oneof:いずれかの型を持つフィールド。repatedフィールドにはできない
  • import/package が可能

syntax = "proto3";

package employee

impot "proto/date.proto";

message Employee {
  int32 id = 1;
  string name = 2;
  string email = 3;
  Occupation occupation = 4;
  repeated string third_party_account = 5;
  map<string, Company.Product> products = 6;
  oneof profile {
    string text = 7;
    URL url = 8;
  }
  date.Date joinedDate
}

enum Occupation {
  UNKNOWN = 0;
  ENGINEER = 1;
  DESIGNER = 2;
  MANAGER = 3;
}

message Company {
  message Product {}
}
message URL {
}
package date;

message Date {
  int32 year = 1;
  int32 month = 2;
  int32 day = 3;
}

コンパイル

protoc -I. --go_out=. proto/*.proto 

gRPC用のコードも出力

protoc -I. --go_out=. --go-grpc_out=. proto/*.proto

gRPC 4つの方式 & Servicea定義

Unary RPC

  • 1リクエスト1レスポンス方式
  • 通常の関数コールのように扱える
  • 用途:API
message SayHelloRequest {}
message SayHelloResponse {}

service Greeter {
    rpc SayHello (SayHelloRequest) returns (SayHelloResponse);
}

Server Streaming RPC

  • 1リクエスト・複数レスポンス方式
  • クライアントはサーバから送信完了の信号が送信されるまでStreamのメッセージを読み続ける
message SayHelloRequest {}
message SayHelloResponse {}

service Greeter {
    rpc SayHello (SayHelloRequest) returns (stream SayHelloResponse);
}

Client Streaming RPC

  • 複数リクエスト・1レスポンス方式
    • サーバはクライアントからリクエスト完了の信号が送信されるまでStreamからメッセージを読み続ける。全部受け取ってからレスポンスを返す
message SayHelloRequest {}
message SayHelloResponse {}

service Greeter {
    rpc SayHello (stream SayHelloRequest) returns (SayHelloResponse);
}

Bidirectional Streaming RPC

  • 複数リクエスト・複数レスポンス方式
  • クライアントとサーバのStreamが独立
  • リクエストとレスポンスの順序は問わない
  • 用途:チャット、オンライン対戦ゲームなど
message SayHelloRequest {}
message SayHelloResponse {}

service Greeter {
    rpc SayHello (stream SayHelloRequest) returns (stream SayHelloResponse);
}

Interceptor

  • メソッド前後に処理を挟むための仕組
  • 認証やロギング、監視、バリデーションなど複数のRPCで共通して行いたい処理で利用する
  • サーバ側/クライアント側どちらも対応
    • サーバ
      • UnaryServerInterceptor
      • StreamServerInterceptor
    • クライアント
      • UnaryClientInterceptor
      • StreamClientInterceptor

以下を満たす関数を実装

type UnaryServerInterceptor func(
  ctx context.Context
  req interface{}
  info *UnaryServerInfo // メソッド等のサーバ情報
  handler UnaryHandler  // クライアントからコールされるhandler
) (resp interface{}, err error) // resp: handlerからのレスポンス

Interceptor追加方法

// サーバ
server := grpc.NewServer(
  grpc.UnaryInterceptor(myInterceptor())
)

// クライアント
connection, err := grpc.Dial(
  "localhost:50001",
  grpc.WithUnaryInterceptor(myInterceptor())
)

ロギング

func logging() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
        log.Printf("request: %+v", req)
        resp, err = handler(ctx, req)
        if err != nil {
          return nil, err
        }
        log.Printf("response: %+v", resp)
        return resp, nil
    }
}

認証

https://github.com/grpc-ecosystem/go-grpc-middleware

例:

  • サーバ側
func main() {
  // :
  server := grpc.NewServer(
        grpc.UnaryInterceptor(
            grpc_middleware.ChainUnaryServer(
                buildLogging(),
                grpc_auth.UnaryServerInterceptor(authorize),
            ),
        ),
    )
  pb.RegisterFileServiceServer(server, &Server{})
  // :
}

func authorize(ctx context.Context) (context.Context, error) {
    token, err := grpc_auth.AuthFromMD(ctx, "Bearer")
    if err != nil {
        return nil, err
    }

    if token != "xxxxx" {
        return nil, status.Error(codes.Unauthenticated, "token is invalid")
    }
    return ctx, nil
}
  • クライアント側
func callServerMethod() {
    md := metadata.New(map[string]string{"authorization": "Bearer xxxxx"})
    ctx := metadata.NewOutgoingContext(context.Background(), md)

  // :
}

認証エラー出力(クライアント側)

2022/05/25 21:35:44 rpc error: code = Unknown desc = bad token
exit status 1

エラーハンドリング

公式ドキュメント: https://www.grpc.io/docs/guides/error/

例:

  • サーバ側
return nil, status.Error(codes.NotFound, "not found")
  • クライアント側
  res, err := client.serverMethod(ctx, &pb.ServerRequest{})
    if err != nil {
        resErr, ok := status.FromError(err)
        if ok {
            if resErr.Code() == codes.NotFound {
                log.Fatalf("Error Code: %v, Error Message: %v", resErr.Code(), resErr.Message())
            }
        } else {
            log.Fatalln("unknown error")
        } 
  } else {
    log.Fatalln(err)
  }

Deadlines

サーバからレスポンスを待つ時間(超えたらタイムアウトでエラー)

例:

  • クライアント側
func callServerMethod() {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

  res, err := client.serverMethod(ctx, &pb.ServerRequest{})
  if err != nil {
        resErr, ok := status.FromError(err)
    if resErr.Code() == codes.DeadlineExceeded {
          log.Fatalln("deadline exceeded")
    }
    // :
  }
}

SSL通信化

例:

  • サーバ側
func main() {
  // :
  credentials, err := credentials.NewServerTLSFromFile(
        "ssl/localhost.pem",
        "ssl/localhost-key.pem",
    )
    if err != nil {
        log.Fatalln(err)
    }

    server := grpc.NewServer(
        grpc.Creds(credentials),
    // :
  )
  // :
}
  • クライアント側
func main() {
  certfile := "xxxxxx/rootCA.pem"
  creds, err := credentials.NewClientTLSFromFile(certFile, "")
  conn, err := grpc.Dial("localhost:50000", grpc.WithTransportCredentials(creds))
  // :
}

ref

環境構築:

参考:

Try Code: https://github.com/Symthy/golang-practices/tree/main/go-gRPC

shell 1行目のおまじない shebang(シバン)

shell 1行目のおまじない shebang(シバン)

bash を使用するか sh を使用するかの指定+α

  • #!/bin/bash : 固有の機能が使える。移植性等考えなくていいならこちらを使うと便利
  • #!/bin/sh : 環境に拘らず使える。(ただしUbuntuの場合はdashという物が使われるらしい)

オプションの意味

#!/bin/bash -eu → オプション指定は set -eu と同じ

refs

「sh」と「bash」の使い分け

シェルスクリプトの1行目に書くおまじないで使える便利オプション集

シェルスクリプトの冒頭でbashを明示する(提案)

指定できるオプション -e と -u についてわかる
bash スクリプトの先頭によく書く記述のおさらい

参考:/bin と /usr/bin の違い
Linuxのディレクトリ構造

Refactor Diary 1 (Java: APIレスポンス解析)

Refactor Diary 1 (Java: APIレスポンス解析)

例:ユーザロールチェック

ユーザ情報はAPIにより以下のようなものが取れるものとする

// API Response Data Model
public class User {
    private String userName;
    private List<RoleEnum> roles;
    // 他にも複数のフィールドがあるものとする
    // Getter,Setter等省略
}

リファクタ前

public class UserRoleValidator {

    private final Logger logger = LoggerFactory.getLogger(UserRoleValidator.class);
    private final ApiClient apiClient;  // OkHttpClient ラッパークラスとする
    private static final List<RoleEnum> REQUIRED_ANY_ROLES = Lists.of(RoleEnum.Admin, RoleEnum.Manage);

    UserRoleValidator(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    // APi実行~レスポンスボディ変換~ロールチェックまで全部入り
    public void execute() {
        OkHttp.Response response = apiClient.getUser(userName);
        if (!response.isSuccessful()) {
            LOGGER.error("Response Status Code: " + response.code());
            if (Objects.nonNull(response.body())) {
                LOGGER.error("Error Response Contents: " + response.body().string());
            }
            LOGGER.error("message");
            throw new IOException("message");
        }
        User user = new Gson().fromJson(response.body().string(), User.class)
        List<Role> roles = Objects.isNull(user.getRoles()) ? List.of() : user.getRoles();
        if (roles.isEmpty() || 
                REQUIRED_ANY_ROLES.stream().anyMatch(r -> roles.contains(r))) {
            LOGGER.error("message");
            throw new InsufficientRoleException("message");
        }
    }
}

何が問題か

  • (レスポンスに対して共通で行うような) 共通処理が入り込み、流用できない
  • ロールチェックのパターン網羅するテストを書くにあたり、全テストで apiClient をmockし、返すresponse body data を指定する必要がある等、テスト自体が見にくくなる

リファクタ後

public class UserRoleValidator {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserRoleValidator.class);
    private final ApiClient apiClient; 

    UserRoleValidator(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    public void execute(String userName) {
        var apiResponse = new DataStoreApiResponse(apiClient.getUser(userName));
        if (apiResponse.isSuccessful()) {
            LOGGER.error("message");
            throw new IOException("message");
        }
        validateUserRole(apiResponse.deserialize());
    }

    public void validateUserRole(User user) {
        if (RequiredRoles.isSatisfy(user.getRoles())) {
            LOGGER.error("message");
            throw new InsufficientRoleException("message");
        }
    }
}

// API Response Data Model
public class User {
    private String userName;
    private List<RoleEnum> roles;
    // Getter,Setter等省略
}

// レスポンスに対する共通処理の集約
class DataStoreApiResponse {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(DataStoreApiResponse.class);
    private final isSuccessful;
    
    DataStoreApiResponse(OkHttp.Response response) {
        if (!response.isSuccessful()) {
            LOGGER.error("Response Status Code: " + response.code());
            if (Objects.nonNull(response.body())) {
                LOGGER.error("Error Response Contents: " + response.body().string());
            }
            isSuccessful = false;
            return;
        }
        isSuccessful = true;
    }

    boolean isSuccessfull() {
        return isSuccessful;
    }

    int getStatusCode() {
        return response.code();
    }

    <T> T deserialize(Class<T> cls) {
        return new Gson().fromJson(response.body().string(), cls)
    }
}

// 知識の確立
public class RequiredRoles {

    private static final List<RoleEnum> REQUIRED_ANY_ROLES = Lists.of(RoleEnum.Admin, RoleEnum.Manage);

    boolean isSatisfy(User user) {
        List<Role> roles = Objects.isNull(user.getRoles()) ? List.of() : user.getRoles();
        if (roles.isEmpty()) {
            return false;
        }
        return REQUIRED_ANY_ROLES.stream().anyMatch(r -> roles.contains(r));
    }
}

先にどういうところでどういうテストが必要になるかを考えて、テストしやすい部品を作る

  • 不良を作りこみやすいような重要な箇所(上記例ならロールチェック処理)を見極め
  • そこに対して、容易にテストができるよう部品に分ける

それにより、複数の重要な知識が1か所に混在し、結果として見通しが悪くなるようなことが防げる。テストも見やすくなり、どういったケースがあり得るのか等テストから読み取りが容易になる

VSCode Markdown

VSCode Markdown

フォーマット

VS CodeのMarkdownフォーマットについて

VSCodeでMarkdownの自動フォーマット&整形ルールを自由に設定

https://github.com/remarkjs/remark/blob/main/packages/remark-stringify/readme.md#options

スライド作成(Marp)

【VS Code + Marp】Markdownから爆速・自由自在なデザインで、プレゼンスライドを作る