3歩進んで2歩下がる

Software Engineer

『エンジニアリングマネージャーのしごと』を読んだ

タイトルの通り『エンジニアリングマネージャーのしごと』を読みました。

www.oreilly.co.jp

本書籍を読もうと思ったきっかけは、仕事でピープルマネジメントに片足を突っ込むようになったためです。 印象に残った点を備忘録として残しておこうと思います。

  • マネジメント活動は4種類(情報収集・意思決定・ナッジング・ロールモデル)に大別できる
    • ナッジングは日頃あまり実行できていない、というか実行自体結構難易度が高い実感がある
  • 部下にタスクを委譲しても、説明責任はマネージャー自身に残る
    • 委譲はいいが任せっきりも良くない、"説明責任" は端的な表現でしっくりきた
  • 1on1の話題どうするか
    • 日々の1on1でテーマに困ることは少なくないのでヒントにしたい(4.4.4 会話の話題のアイディア)
  • その人にあった仕事とは何か
    • 発達の最近接領域(学習者が支援ありでできるタスク)をお願いするのが良い
    • しかし、実際の業務と結びつけて考えると難しい、その人に向いているタスクが落ちているとも限らないので
  • 評価の準備はどうするか、どう振る舞うか
    • 評価をする側になったことはまだないが、もしするタイミングが来たなら改めて読み返したい
  • 情報の取り扱いに気をつける
    • EMはエンジニアとBizの仲介をしたり、部門の上長と部下との橋渡しをしたり、と中間に立つことが多い
    • どの情報を渡して、どの情報を渡さないか、を考えなければならない

まとめ

正直まだまだできていないことが多いなあと実感しました。 マネジメント経験がまだ浅いこともあり、日々不安な気持ちと葛藤しているのが正直なところです。 本書の内容を少しずつでも実践していきます。

関連

logmi.jp

note.com

出来る限りフォームライブラリに依存しないコンポーネント設計

はじめに

Reactでライブラリを利用してフォームバリデーションを実現する時、ナイーブな実装だとすぐにUIとフォームライブラリが密結合してしまいます。例えば、React Hook Formの場合は以下のようになります。

import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
  example: string,
  exampleRequired: string,
};

export default function App() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();
  const onSubmit: SubmitHandler<Inputs> = data => console.log(data);

  console.log(watch("example")) // watch input value by passing the name of it

  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register("example")} />
      
      {/* include validation with required or other standard HTML validation rules */}
      <input {...register("exampleRequired", { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}
      
      <input type="submit" />
    </form>
  );
}

フォーム全体の設定を useForm で宣言し、その結果を子要素に伝搬させています。結果的にフォームの外側と input 要素に渡す直前でライブラリに依存することになります。サンプルとして React Hook Form を取り上げましたが、他のフォームライブラリでも同様の状況が発生しうるはずです。 このような状況を避けるため、出来る限りフォームライブラリに依存しないコンポーネント設計について考えてみました。

最終系イメージ

最終的な利用者側のイメージです。実装には RemixRVF を利用しています。

  • UI要素とバリデーションライブラリ要素を分離する
  • FormField だけがライブラリに依存する
  • Formschema を渡す
  • 各フィールドは Field から返却されるpropsの中から使いたい属性をピックしてUI要素に渡す

引数付きchildrenが便利なので利用していますが、カスタムフックで代用してもいいと思います。

import z from "zod";
import { Form, Field } from "~/libs";
import { Button, TextField } from "~/components";

const schema = z
  .object({
    email: z.email(),
    password: z.string().min(1),
    confirmPassword: z.string().min(1),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: "custom",
        message: "Passwords do not match",
        path: ["confirmPassword"],
      });
    }
  });

export default function Home() {
  return (
    <div style={rootStyles}>
      <Form
        schema={schema}
        defaultValues={{ email: "", password: "", confirmPassword: "" }}
      >
        {(form) => (
          <div style={formStyles}>
            <Field name={"email"}>
              {(field) => (
                <TextField type="email" error={field.error} {...field.input} />
              )}
            </Field>
            <Field name={"password"}>
              {(field) => (
                <TextField
                  type="password"
                  error={field.error}
                  {...field.input}
                />
              )}
            </Field>
            <Field name={"confirmPassword"}>
              {(field) => (
                <TextField
                  type="password"
                  error={field.error}
                  {...field.input}
                />
              )}
            </Field>

            <Button type="submit" disabled={!form.isValid}>
              submit
            </Button>
          </div>
        )}
      </Form>
    </div>
  );
}

const rootStyles = {
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  height: "100vh",
} as const;

const formStyles = {
  display: "flex",
  flexDirection: "column",
  gap: 8,
  width: "640px",
} as const;

各構成要素

各構成要素について解説します。

Form

useForm でフォーム全体の設定を行い、FormProvidergetFormProps でUIに設定を紐付るコンポーネントです。ChildrenProps は依存性を逆転させ、children として指定される要素がライブラリに依存しないようにします。

import { FormProvider, useForm, type FieldValues } from "@rvf/react-router";

type ChildrenProps = {
  isTouched: boolean;
  isDirty: boolean;
  isValid: boolean;
  isSubmitting: boolean;
};

type Props<T extends FieldValues> = {
  children: (form: ChildrenProps) => React.ReactNode;
} & Parameters<typeof useForm<T>>[0];

export const Form = <T extends FieldValues>({
  children,
  ...props
}: Props<T>) => {
  const form = useForm<T>(props);

  const childrenProps: ChildrenProps = {
    isTouched: form.formState.isTouched,
    isDirty: form.formState.isDirty,
    isValid: form.formState.isValid,
    isSubmitting: form.formState.isSubmitting,
  };

  return (
    <FormProvider scope={form.scope()}>
      <form {...form.getFormProps()}>{children(childrenProps)}</form>
    </FormProvider>
  );
};

Field

Form 配下で利用されることを前提とした、各Input要素にバリデーションの仕組みを与えるためのコンポーネントです。Form で設定される schema との紐付けをするために name を props で受け取り、バリデーションエラーが発生した場合はそれを表示させます。依存性を逆転させるために ChildrenProps を定義しているのは Form と同じです。Controlledなコンポーネントとして利用できるよう valueonChange を受け渡しできるようにしていますが、用途によって onBlur など他のイベントハンドラを受け渡してもいいと思います。ユースケースとライブラリの制約次第ですね。

import { useField } from "@rvf/react-router";

type ChildrenProps = {
  input: {
    name?: string;
    type?: string;
    defaultValue?: any;
    value?: string | number;
    onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
    onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
  };
  error: string | null;
  dirty: boolean;
};

type Props = {
  name: string;
  value?: string;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
  children: (field: ChildrenProps) => React.ReactNode;
};

export const Field = <T,>({ name, value, onChange, children }: Props) => {
  const field = useField<T>(name);

  const childrenProps: ChildrenProps = {
    input: field.getInputProps({ value, onChange }),
    error: field.error(),
    dirty: field.dirty(),
  };

  return (
    <div style={rootStyles}>
      {children(childrenProps)}
      {field.error() && <span style={errorStyles}>{field.error()}</span>}
    </div>
  );
};

const rootStyles = {
  display: "flex",
  flexDirection: "column",
  gap: 4,
} as const;

const errorStyles = {
  color: "red",
  fontSize: 12,
} as const;

最後に

出来る限りフォームライブラリに依存しないコンポーネント設計について考えてみました。 実装はこちらのリポジトリです。

github.com

【SQL】 WHERE 1 = 1

実務で WHERE 1 = 1 というSQLを見かけたのだけど、意味が分からなかったので調べたメモ。

WHERE 1 = 1 は常にtrueとなるので、それ自体に意味はない。 が、SQLで複数の条件式が追加される可能性がある場合にクエリが書きやすくなる。

サンプルコードを見た方が分かりやすいので以下に示す。

WHERE 1 = 1 を使わない場合

func GetArticle(id, title *string) (Article, error) {
    sql := "SELECT id, title FROM article"
    if id != nil {
        sql += " WHERE id = ?"
    }
    if id == nil && title != nil {
        sql += " WHERE title = ?"
    } else if id != nil && title != nil {
        sql += " AND title = ?"
    }

         // 省略
}

WHERE 1 = 1 を使う場合

func GetArticle(id, title *string) (Article, error) {
    sql := "SELECT id, title FROM article WHERE 1 = 1"
    if id != nil {
        sql += " AND id = ?"
    }
    if title != nil {
        sql += " AND title = ?"
    }
    // 省略
}

WHERE 1 = 1 をつけることで条件式の2番目以降がWHEREなのかANDなのかを判定する処理が不要になり、クエリがシンプルになった。

Goでテスタブルに現在日時を扱いたい

アプリケーションを開発していると現在日時を使いたい場面に遭遇します。 このような場面で time.Now() の出力はその時々で変化していくため、テストを書く際に工夫が必要です。

例えば、JavaScriptのテストライブラリJestでは以下のように実行時の日時を固定化する モック機構 が提供されています。

jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

test('waits 1 second before ending the game', () => {
  const timerGame = require('../timerGame');
  timerGame();

  expect(setTimeout).toHaveBeenCalledTimes(1);
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

しかし、Goの標準ライブラリではこのような機構が用意されていません。 今回は Go の Context を利用することでテスタブルに time.Now() を取り扱えるのではないか?と考えたので検証してみました。

検証

日時をContextに詰めたり、Contextから取り出すためのutilを用意します。

package timeUtil

import (
    "context"
    "time"
)

type TimeKey string

const Key TimeKey = "now"

func GetNow(ctx context.Context) time.Time {
    return ctx.Value(Key).(time.Time)
}

func SetNow(ctx context.Context, now time.Time) context.Context {
    return context.WithValue(ctx, Key, now)
}

利用の仕方は以下のイメージ。サンプルとして昨年を取得する処理を書いてみます。Webアプリケーションなら middleware で context に time.Now() をセットして後続のハンドラにリレーしていきます。

package main

import (
    "context"
    "time"

    "example.com/timeUtil"
)

func GetLastYear(ctx context.Context) int {
    now := timeUtil.GetNow(ctx)
    return now.AddDate(-1, 0, 0).Year()
}

func main() {
    ctx := context.Background()
    // 本来はmiddlewareなどでcontextにtime.Now()をセットする
    ctxWithNow := timeUtil.SetNow(ctx, time.Now())
    println(GetLastYear(ctxWithNow))
}

テストを書きます。モックの日時を用意し context にセットします。テストしたい処理の引数にモックした context を渡せばOK。

package main

import (
    "context"
    "testing"
    "time"

    "example.com/timeUtil"
)

func Test_GetLastYear(t *testing.T) {
    t.Run("正常系", func(t *testing.T) {
        mockTime := time.Date(2024, 1, 10, 13, 10, 10, 10, time.UTC)
        mockCtx := timeUtil.SetNow(context.Background(), mockTime)
        actual, expected := GetLastYear(mockCtx), 2023
        if actual != expected {
            t.Errorf("got %v\nwant %v", actual, expected)
        }
    })
}

テストがpassしました。

調べてみると自前でライブラリを作っている人もいますが、context を利用する方法がシンプルでやりやすいのではないかと思います。

Goのreflect.DeepEqualでsliceを比較する時は要素の並びに注意する

Goでreflect.DeepEqualを使ったテストを書いていた時の話。

PR作成時点ではCIのテストをPassしていたのでmainにmerge。しかしその後mainでテストが失敗。

「おや...?」と思って調べてみるとreflect.DeepEqualでsliceを比較する際にテストが失敗していました。

発生した事象

実際のコードは載せられないですが、イメージとしては以下のようなMapからSliceに変換するようなコードが原因でテストが落ちていました。

package main

func MapToSlice(m map[string]string) []string {
    var s []string
    for _, v := range m {
        s = append(s, v)
    }
    return s
}

簡易のテストコードを書いてみます。

package main

import (
    "reflect"
    "testing"
)

type TestData struct {
    name string
    args map[string]string
    want []string
}

func Test_MapToSlice(t *testing.T) {
    tests := []TestData{
        {
            name: "Success",
            args: map[string]string{
                "key1": "value1",
                "key2": "value2",
                "key3": "value3",
            },
            want: []string{"value1", "value2", "value3"},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := MapToSlice(tt.args)
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("MapToSlice(): got %v, want %v", got, tt.want)
            }
        })
    }
}

テストを実行してみます。成功する時もあるのですが、何回か試すと失敗する場合もあります。

テスト結果

テスト失敗の原因

今回テストが失敗した原因は2つ。

  1. reflect.DeepEqualでsliceを比較する場合、要素の並びまで完全に一致している必要がある
  2. mapは反復処理(range)で回す場合、順序が保証されない

つまり「rangeによってmapから取得されるvalueは毎回順序が異なるが、テストではSliceをreflect.DeepEqualでチェックしているため、期待値の順序と齟齬が発生し、テストが失敗していた」ということになります。

参考

解決策

解決策は2つです。

  1. 実装側でスライスをsortする
  2. テスト側で順序を問わず内容の一致度をチェックできるようにする

1つ目は以下のようなイメージ。

func MapToSlice(m map[string]string) []string {
    var s []string
    for _, v := range m {
        s = append(s, v)
    }
    // ソートして順序を固定化する
    sort.Strings(s)
    return s
}

2つ目は以下のようなイメージ。

package main

import (
    "testing"
)

type TestData struct {
    name string
    args map[string]string
    want []string
}

func Test_MapToSlice(t *testing.T) {
    tests := []TestData{
        {
            name: "Success",
            args: map[string]string{
                "key1": "value1",
                "key2": "value2",
                "key3": "value3",
            },
            want: []string{"value1", "value2", "value3"},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := MapToSlice(tt.args)
            // 順序に関わらず、要素が一致しているかチェック
            for _, want := range tt.want {
                ok := false
                for _, g := range got {
                    if g == want {
                        ok = true
                        break
                    }
                }
                if !ok {
                    t.Errorf("MapToSlice(): got %v, want %v", got, tt.want)
                }
            }
            // 要素数が一致しているかチェック
            if len(got) != len(tt.want) {
                t.Errorf("MapToSlice(): got %v, want %v", got, tt.want)
            }
        })
    }
}

これで何度テストを実行しても失敗しなくなりました。

トランザクションを考慮したアプリケーション実装

オニオンアーキテクチャやクリーンアーキテクチャなど、関心ごとのレイヤーに分離した実装をするとき、トランザクションをどのレイヤーで管理するかは悩みの種だと思います。

ユースケース層でトランザクションを管理する

個人的な推しは「ユースケース層でトランザクションを管理する」です。リポジトリ層ではユースケース層から渡されるトランザクションを利用してデータアクセスを試みます。こうしたい理由は1つのユースケースが複数のデータに渡った更新制御をすることが少なくないからです。また、この方法ではユースケース層がトランザクションに依存してしまう(依存が逆転してしまう)のですが、実装はシンプルになります。

例えば以下は、SNSで新規投稿をする際に記事とタグを合わせて作成するケースです。

func (a *articleUseCase) CreateArticle(ctx context.Context) error {
    tx, err := a.db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        // トランザクション処理が失敗した場合、DBをロールバックしてエラーを返す
        if err != nil {
            tx.Rollback()
        }
    }()

    article, err := domain.NewArticleToCreate(...)
    if err != nil {
        return ...
    }

    err = a.articleRepository.CreateArticleWithTx(ctx, tx, article)
    if err != nil {
        return ...
    }

    tag, err := domain.NewTagToCreate(...)
    if err != nil {
        return ...
    }

    err = a.tagRepository.CreateTagWithTx(ctx, tx, tag)
    if err != nil {
        return ...
    }

    err = tx.Commit()
    if err != nil {
        return err
    }

    return nil
}

実際の実装では、トランザクションをctxとして渡せるように抽象化しますが、今回はイメージしやすいのでユースケース層でそのままトランザクションオブジェクトを取り扱っています。

リポジトリ層でトランザクションを管理するとどうなるか 

シンプルなユースケース(単一のテーブルを更新するようなケース)ではリポジトリ層でトランザクションを管理しても問題ないと思います。しかし、そうではない場合、データの一貫性を管理したいためにロジックがリポジトリ層に書かれてしまい、ユースケース層がスカスカになってしまいます。「シンプルなユースケースのみリポジトリ層でトランザクションを管理する」とするのもありかもしれませんが、書き方に統一感がなくなってしまうので「トランザクションユースケース層で管理する」と決めてしまう方がいいかなと考えています。

ミニマムなパッケージマネージャを作ってみる

ミニマムなパッケージマネージャを作ってみました。実装はGoでおこなっています。パッケージマネージャの仕組みについて少しばかり学べたので記録として残しておこうと思います。

github.com

参考元

動機は以下のリポジトリ・記事で、挙動としてカバーしているスコープも同じです。

github.com zenn.dev

過程で学べたこと

詳細な解説はこちらの記事が分かりやすいため、割愛します。ここでは私がパッケージマネージャの自作を通して勉強になったことをつらつらと書いていきます。

zenn.dev

Lockfileの役割

Lockfileとは npm における package.lock.json や、 yarn における yarn.lock です。install の際にはこのLockfileの情報を元にインストールが行われ、パッケージの依存関係解決後に最新情報をLockfileに記録しておきます。

Lockfileの役割としては大きく以下です。

  • パッケージ全体の依存ツリーを正確に表現することで、開発者、デプロイ、CI等の間で常に同じバージョンのパッケージを利用できる
  • node_modules のその時の状態をバージョン管理(e.g. git) できる
  • パッケージインストール時に、マニフェストの取得やバージョンの解決をスキップできる

qiita.com

セマンティックバージョニング

セマンティックバージョニングはソフトウェアのバージョンを表現する際によく用いられるルールです。Major.Minor.Patch という形で表記され、上から順に以下のような意味合いを持ちます。

多くのnpmパッケージでは、^(キャレット)~(チルダ) がバージョンの前に付与されており、これによって、後方互換性を保ちつつ一定のレンジ内で最新のバージョンをインストールすることが可能となります。

qiita.com docs.npmjs.com

再帰的な依存関係解決

各パッケージの依存関係はネストするため、再帰的な依存関係の解決とバージョンのコンフリクト解消が必要です。 例えば、以下のようなパッケージを例として考えてみます。

{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "eslint-scope": "^5.0.0",
    "estraverse": "^3.0.0"
  }
}

この場合、依存解決の過程で estraverse が3バージョン必要となり、それぞれのメジャーバージョンが異なるため衝突してしまいます。

  • ^3.0.0 (直下)
  • ^4.1.1 (eslint-scope@^5.0.0 -> estraverse@4.1.1)
  • ^5.2.0 (eslint-scope@^5.0.0 -> esrecurse@4.1.0 -> estraverse@^5.2.0)

本家の npm install 実行時、これらのパッケージは最終的に以下のように node_modules 配下へ展開されます。

node_modules/eslint-scope/node_modules/estraverse
node_modules/esrecurse/node_modules/estraverse
node_modules/estraverse

つまり、ルートで指定されたパッケージのバージョンはそのまま node_modules 直下へ、それ以外の各パッケージが依存しているバージョンはそれぞれのパッケージ配下へさらに node_modules ディレクトリが掘られていきます。

これはどうやらパッケージのインポート時にNode.jsがパッケージを探索するアルゴリズムに基づくものらしいです。

maku77.github.io

まとめ

考慮不足なところも多いですが、最低限の動きだけでも実現できて嬉しいです。 普段何げなく利用しているパッケージマネージャですが、install 1つとっても考えることが多く勉強になりました。