じぶん対策

日々学んだことをアウトプットして備忘録にしています。

新しくPCを買ったのでDotfilesを作ってみる

参考

https://qiita.com/yutkat/items/c6c7584d9795799ee164
https://dev.classmethod.jp/articles/joined-mac-dotfiles-customize/

はじめに

今週M2 Pro Mac mini(2023)を購入しました!

2023モデルのM2チップ搭載Mac miniは最小構成で84800円と前モデルより安くなっていてライトな使い方にもピッタリのコスパになっています。
そしてM2 Proチップ搭載のMac miniは184800円からですがメモリを32GBまで増設可能で個人使用であれば十分なスペックを選択できるようになりました。
M2とM2 Proのモデルにはほかにもポートの違い等がありますが詳細は割愛します。

私用のMacとしては2019モデルのMacbook Proを最小構成で購入して以来のPCで二台目となります。
また、Apple SiliconのMacを購入するのが初めてなので思い切ってメモリを32GBにカスタマイズして購入しました!

今回は新しいMacの設定をしながら、dotfilesと呼ばれる設定ファイル管理用リポジトリを作成しました。
チーム内で雑談しているときに話題に上げたのですが思ったよりみんな知らない文化だったので布教の意味も込めて記事にしてみます。
自身のdotfilesはまだまだ細部までこだわれてないので今回は文化の紹介という感じでライトな記事にしたいと思います。

dotfilesとは

dotfilesとは、普段自身で使っているソフトウェアの設定ファイルをまとめて管理するリポジトリです。
慣例的に.(ドット)から始めるファイルになっているためdotfilesと呼ばれています。

dotfilesの目的

  • どんな環境で作業することになっても自分の環境をさくっと用意できる
  • docker等で環境を立ち上げた際にも簡単に環境を用意できる

そして何よりすぐ新しいパソコンを買っちゃったとしてもすぐにセットアップができる!

dotfilesのはじめかた

まずは簡単な設定ファイルを置くところから始めます。
GitHubにdotfilesという名前でリポジトリを作成します。
このとき、GitHub Actions等でのCIを考えていたりする場合はpublicリポジトリで作成しておくといいと思います。
当然ながら機密性の高い情報やキャッシュ等のファイルはgitに追加しないようにしておきましょう!

dotfilesはホームディレクトリ直下にcloneするようにしておくと使い勝手がいいかと思います。

今回はまずシェルの設定から追加してみます。
自身のシェルの設定をdotfiles配下にコピーします。
あとでシンボリックリンクを貼るので移動してもいいと思います。

mv ~./bashrc ~/dotfiles/.zshrc

ホームディレクトリに対してシンボリックリンクを作成します。

ln -s ~/dotfiles/.zshrc ~zshrc

これでシェルの設定がdotfiles配下の設定ファイルを参照するようになりました。

dotfilesを工夫してみる

dotfilesのホワイトリスト

エディタやシェルにプラグインを入れたりすると勝手にファイルが作られることがあるため、その更新が見えないように.gitignoreをホワイトリストにします。

# 全ての階層を無視
/*
/.**

# ホワイトリスト
!/.zshrc
!/README.md
!/LICENSE
!.gitignore

# ほかにも追加したいものを追加

macOSの場合はHomebrewのインストールファイルも作成する

今の環境からインストールリストファイルを作成するには下記のコマンドを実行します。

brew bundle dump

新しい環境で下記コマンドを実行すると先ほど作成した.Brewfileが読み込まれ、一括インストールされます。

brew bundle --global

私の環境では最低限下記のようなBrewfileを作成しました。

tap "homebrew/bundle"
tap "homebrew/cask"
tap "homebrew/core"
brew "anyenv"
brew "php"
brew "composer"
brew "jq"
brew "tree"

cask "authy"
cask "docker"
cask "iterm2"
cask "postman"
cask "sequel-ace"
cask "visual-studio-code"
cask "google-chrome"
cask "kindle"
cask "slack"

よく使う構文をいくつか紹介しておきます。

tap

Homebrewに正式に登録されてはいないライブラリをインストールする際にtapを使用します。
GitHubリポジトリをHomebrewに登録する仕組みとなっています。

brew

Homebrewに正式に登録されたライブラリをインストールする際にはbrew構文を作成します。

cask

Macアプリをインストールする際にはcask構文を使います。

mas

mas構文を使用するとApp StoreからMacアプリをインストールできます。

シンボリックリンクやPATHも自動化

先ほど説明したシンボリックリンク作成コマンドや最低限必要なPATHを通すためのコマンドもシェルスクリプトで用意しておくと楽です。

ln -s ~/dotfiles/.zshrc ~zshrc

また、シェルスクリプトを作成する際には以下のような内容に気をつけます。

  • 冪等性を保つ(すでにインストールされている場合はスキップしたりして何度実行しても同じ結果になるようにしておく)
  • 実行する場所がどこでも正常に終了するようにパスを工夫しておく

CIの作成

dotfilesにもCIの考えを導入することができます。
GitHub Actions等を用いてインストールコマンドが成功するかどうかを確認できます。
今回は作成していないんですがいずれ挑戦したいと思います。

参考: https://dev.classmethod.jp/articles/joined-mac-dotfiles-customize/

所感

新しいPCを買うのと合わせてセットアップの自動化スクリプトをいじった週末でした。
エンジニアのこういう自動化文化自体が大好きなのでこれからもメンテナンスして自分なりのdotfilesを育てていきたいと思います!
また、Docker等を使用する上でもLinux、というよりUnix系のコマンドを理解しておくと色々と捗りそうなのでそのあたりも学習していきたいと思います。

おまけ

シェルスクリプトにちょっと入門してみる

先ほど気をつけた方がいいと書いた

  • 冪等性を保つ(すでにインストールされている場合はスキップしたりして何度実行しても同じ結果になるようにしておく)
  • 実行する場所がどこでも正常に終了するようにパスを工夫しておく

について、シェルスクリプトでどう工夫するかについて調査してみます。

実行する場所の設定

スクリプトの配置位置や実行位置を気にせずにスクリプトを実行するには

SCRIPT_DIR=$(cd $(dirname $0); pwd)

使用する際は$SCRIPT_DIRで使用できます。 シェルスクリプトの場合は「変数名=値」のように指定します。=の前後に空白を入れることができない点に注意が必要です。
定義した変数の値は「$変数名」で参照できます。

参考: https://qiita.com/koara-local/items/2d67c0964188bba39e29

冪等性を保つには

インストール済みの場合はコマンドを実行しないように工夫してみます。

if ! type コマンド >/dev/null 2>&1; then
 # インストール処理
fi

基本的には上記で書けるのですが、内容についても調査してみます。

typeコマンドを使用します。typeコマンドはコマンドのタイプに関する情報を表示するコマンドです。

Unixには下記のように入出力に番号が振られています。

これら入出力をOSが判別するために割り当てられた番号をファイルディスクリプタといいます。

  • リダイレクト

標準出力に出力した結果を別の場所に出力する際に>を使用できます。

  • /dev/nullとは

/dev/nullUnixスペシャルファイルで、空ファイルを指します。

これを利用して、typeコマンドの結果を捨てて分岐ロジックを書いていることが理解できました。

参考:
https://qiita.com/yn-misaki/items/3ec0605cba228a7d5c9a
https://qiita.com/ritukiii/items/b3d91e97b71ecd41d4ea
https://qiita.com/i35_267/items/158cd20ed26f73a3d894

Docker Composeについて

はじめに

前回の記事でDockerについて調査した続きです。
今回は実際にアプリを作成するとなると使うことになるであろうDocker Composeについて調査します。
前回同様公式ドキュメントのGet startedの内容を確認し、自分なりの設定方法を考えてみます。

Docker Composeとは

複数コンテナのアプリケーションを定義、共有するためのツールです。
YAML形式のファイルを作成することでコマンド1つで複数のコンテナを立ち上げたり、解体することができます。

Macの場合、Docker Desktopをインストール済みであれば、Docker Composeのインストールは不要です。
下記コマンドでバージョンが確認できればOKです。

$ docker-compose version

Composeファイルの作成方法

アプリのプロジェクトルートでdocker-compose.ymlという名前でファイルを作成します。

今回記載するコンテナの内容は下記になります。

$ docker run -dp 3000:3000 \
  -w /app -v "$(pwd):/app" \
  --network todo-app \
  -e MYSQL_HOST=mysql \
  -e MYSQL_USER=root \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_DB=todos \
  node:12-alpine \
  sh -c "yarn install && yarn run dev"
version: "3.7"

services:
  app:
    image: node:12-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos
  • version ... docker composeのスキーマバージョン。詳細はこちら
  • services ... コンテナの一覧を定義します。
  • app ... コンテナ名です。任意の値に変更可能で、自動的にネットワークエイリアスになります。
  • command ... コマンドを記載します。通常はimage定義のすぐ近くに書きます。
  • ports ... ポートを指定します。記載方法がいくつかあります。今回の書き方はHOST:CONTAINERの書き方です。
  • working_dir ... ワーキングディレクトリです。コマンドでいうと-wで指定したディレクトリです。
  • volumes ... ボリュームの指定です。コマンドでいうと-v で指定したディレクトリです。ポート同様に記載方法がいくつかあります。Docker Composeにボリュームを定義する場合はカレントディレクトリからの相対パスで記載することができます。
  • environment ... 環境変数の指定です。コマンドでいうと-eで指定していた部分です。

以上でアプリ用のコンテナの定義を記載できました。

続いてMySQLサーバーの定義を記載していきます。

$ docker run -d \
  --network todo-app --network-alias mysql \
  -v todo-mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=todos \
  mysql:5.7

先程のアプリ用コンテナの定義の下にMySQL用のサービスを定義します。

 version: "3.7"

 services:
   app:
     # The app service definition
   mysql:
     image: mysql:5.7

次はボリュームマッピングの定義ですが、docker runを使用すると名前付きボリュームが自動生成されていました。
Composeの場合は最上位項目としてvolumes:というセクションを作成し、サービス定義の中のマウントポイントをここに指定します。 ボリューム名だけを指定すれば、デフォルトのオプションが適用されます。composeにおけるボリュームについてはこちら

概要としては、docker compose自体が複数のコンテナの利用を前提としているため、マルチサービスにまたがって使用できる名前付きボリュームを生成します。

ボリューム自体の理解についてはこちら

ボリュームが設定できれば、後は必要な環境変数environmentとして定義します。

ここまでで完成したdocker-compose.ymlが以下のようになります。

version: "3.7"

services:
  app:
    image: node:12-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

アプリケーションの起動には、docker-compose up -dコマンドを利用します。
-dオプションの指定でバックグラウンドで実行されるようになります。

ちなみに、Docker Composeを利用するとネットワークは自動的に生成されます。

ログを確認する場合はdocker-compose logs -fコマンドを実行することでサービスのログを1つにまとめて表示する事ができます。
-fコマンドを指定するとログ出力を継続する事ができます。また、特定のサービスのログのみを確認したい場合はdocker-compose logs -f appのようにログコマンドの最後にサービス名を指定します。

アプリケーションのコンテナをまとめて削除する場合はdocker-compose downコマンドを実行します。
この場合は名前付きボリュームはdocker-compose downでは削除されないため、--volumesフラグをつける必要があります。

ここまでのまとめ

ここまでの内容で公式ドキュメントにあるチュートリアルについて確認しました。

  • docker composeを利用することで複数コンテナの立ち上げ、削除をコマンド一つで簡単に行う事ができる。
  • docker composeファイル作成の際にはボリュームネットワークの理解があると楽

ベストプラクティス

こちらにイメージビルドのベストプラクティスがまとまっています。

この中で、ぱっと理解できなかったキャッシュ処理とマルチステージビルドについてまとめます。

レイヤーのキャッシュ処理

Dockerは1つのレイヤーに変更が入ると、それ移行に続く全レイヤーは再生成されます。

イメージがどのように構成されているかはdocker image historyコマンドで確認できます。

Dockerfileの各コマンドはイメージ内の1つのレイヤーに対応しています。

つまりイメージに変更があった場合、yarn install --productionが再度実行され、依存パッケージが再インストールされます。
これをビルドのたびに何度もインストールするのは無駄なため、キャッシュ処理を考えます。

# syntax=docker/dockerfile:1
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

Nodeベースのアプリケーションの場合、依存パッケージはpackage.jsonファイルに定義されます。
このファイルに変更があった場合にのみyarnによる依存パッケージの更新を行うにはどうすればいいでしょうか。

 # syntax=docker/dockerfile:1
 FROM node:12-alpine
 WORKDIR /app
 COPY package.json yarn.lock ./
 RUN yarn install --production
 COPY . .
 CMD ["node", "src/index.js"]

Dockerfileと同じフォルダー内に.dockerignoreという名前のファイルを生成して、その内容を以下とします。

node_module

.dockerignoreファイルを利用することでDockerCLIはそこに記述されたパターンにマッチするようなファイルやディレクトリを除外してコンテキストを扱います。
今回、node_modulesフォルダを記載しておくことでRUNコマンドによって生成されたファイルを上書きしてしまうことを避ける事ができます。
node.jsを利用したベストプラクティスはこちら

下記コマンドを実行して新たなイメージをビルドするとキャッシュが使用されていることを確認できます。
キャッシュが使用されている箇所ではUsing cacheと出力されます。

$  docker build -t getting-started .

マルチステージビルド

マルチステージビルドとは、イメージの生成に複数ステージを利用するというツールです。

  • ビルド時の依存パッケージと実行時の依存パッケージを分離します。
  • アプリとして実行する必要のあるものだけを作り出すことによって、イメージ全体のサイズを削減します。

マルチステージビルドを行うには、Dockerfile内にFROM行を複数記述します。
FROM命令のベースイメージはそれぞれ異なるもので、各命名から新しいビルドステージが開始されます。
これを利用して片方のビルドステージで生成した内容を他方にコピーして破棄するといった使い方ができます。

例えば公式にあるReactアプリケーションの例を見てみます。

# syntax=docker/dockerfile:1
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

上記のnode:12イメージはビルド処理を行ったあと、その出力結果をnginxコンテナにコピーします。
ビルドが終了したあとのコンテナは放置されます。

FROM node:12 AS buildのように指定していますが、これはビルドステージを表します。
デフォルトではビルドステージに名前はつかず、最初のFROM命令を0として順番に整数値が割り振られます。
FROM命令にAS <NAME>という構文を加えることでステージ似名前をつける事ができ、COPY命令においてその名前を使用しています。
COPY --from=build /app/build /usr/share/nginx/htmlの部分です。

また、--targetを指定するとイメージをビルドする際に特定のステージのみを対象とすることもできます。
この機能を使用すると、デバッグ用にdebugステージを用意してデバッグツールを導入し、本番のproductionステージではスリムなイメージを使用する事ができます。また、テストの場合も同様です。

このように、イメージがどう構成されているかを理解できれば、イメージのビルドをより効率的にすることができます。
キャッシュを利用することでビルドがより早くなり、マルチステージビルドをうまく使えばイメージサイズ全体を小さくする事ができます。

まとめ

  • docker composeを利用することで複数コンテナの立ち上げ、削除をコマンド一つで簡単に行う事ができる。
  • docker composeファイル作成の際にはボリュームネットワークの理解があると楽
  • マルチステージビルドを利用して基本的にイメージのサイズを小さくするように工夫ができる
  • キャッシュを利用してビルドは必要なときに必要なだけ行うようにする

所感

あらためて公式ドキュメントを読むとまだまだDockerを雰囲気で使ってしまっていたなと反省しました。
今回である程度基本は理解できたので、CI/CDでの活用やクラウドへのデプロイ時にコンテナを利用してみたいと思います。
これらはまた個人開発等で使用してみる際に再度調査してみたいと思います。

Dockerを雰囲気ではなく理解して使う

はじめに

以前Dockerについてごく簡単にまとめた記事を書きましたが、Webエンジニアになって一年が経とうとしているので改めてDockerについてまとめようと思います。
公式ドキュメントを自分なりに解釈してまとめたいと思います。
Dockerについての入門からみていき、普段業務で使用しているコマンドの中身を理解していきます。

まとめ

  • Dockerを使えばコードを管理するようにインフラも管理できる。
  • イメージとは、Dockerコンテナを作成する命令が入った読み込み専用のテンプレートのこと。
  • Dockerfileは、イメージの作成の際に使用する。
  • コンテナとは、イメージが実行状態となったインスタンス(実体)のこと。
  • コンテナ内でのファイルの変更を保存するには、ボリュームを使用する。
  • 複数のコンテナ同士の接続にはネットワーク機能を使用する

Dockerとは

公式には以下のように説明があります。

Docker はアプリケーションを開発(developing)、移動(shipping)、実行(running)するためのオープンなプラットフォームです。Docker はインフラストラクチャ 1 とアプリケーションを切り離すため、ソフトウェアを短時間で提供できます。Docker があれば、アプリケーションを管理するのと同じ方法で、あなたのインフラも管理できます。Docker 的な手法を最大限活用しますと、テストやコードのデプロイを素早くできますので、コードを書いてから、プロダクション(実行環境)で動かすまでにかかる時間を著しく軽減できます。

Dockerを使用することで、コンテナというものを使ってインフラをGitのようなバージョン管理ツールを用いて管理することができます。
コンテナは隔離された環境です。
ホストコンピュータ上に何がインストールされているかに関係なく、コンテナ上にアプリケーションのパッケージ化、実行が可能です。
コンテナは、アプリケーションの配布とテストをする単位です。
Dockerはこのコンテナという技術のライフサイクルを管理するツールとプラットフォームです。

Dockerが解決する問題

CI/CD

開発するアプリケーションやサービスをローカルのコンテナ内で実現することで、開発者は標準化された環境で作業が進められます。
コンテナを使っての開発はCI/CDのワークフローに適しています。
コンテナは開発者のローカル環境だけではなく、本番環境を含めた様々な環境の組み合わせにおいて実行可能です。
いろんな環境で実行できる可搬性のおかげで、

  • 処理負荷を動的に管理できる
  • スケールアップやサービス終了時に簡単に行える

といったメリットがあります。

同じハードウェア上で負荷の高い処理を実行

以下公式からの引用です。

Docker は軽量かつ高速です。ハイパーバイザ・ベースの仮想マシンに取って変わる、実用的で費用対効果の高いものです。したがってコンピュータ性能をフルに活用してビジネス目標を達成できます。Docker は高度に処理集中する環境に適しており、さらには中小規模の、より少ないリソースの中でのシステム構築にも適しています。

Dockerにおける用語

雰囲気でDockerを使わないために、イメージとコンテナという用語についてどういうものなのかを理解する必要がありそうです。

イメージとは

  • Dockerコンテナを作成する命令が入った読み込み専用のテンプレートのこと
  • 通常、他のイメージをベースにカスタマイズして利用する
  • イメージを自分で作る場合はDockerfileというファイルを生成する

コンテナとは

  • イメージが実行状態となったインスタンス(実体)のこと。
  • 複数のネットワークへの接続、ストレージの追加を行う事ができ、現時点の状態にもとづいた新たなイメージを生成する事もできる。
  • ローカルマシン上や仮想マシン上でも実行でき、クラウドにもデプロイができ、可搬性があります。
  • コンテナを削除すると永続的なストレージに保存されていないものは消失します。

Dockerのアーキテクチャ

具体的なDockerの動作イメージについては下記公式を参考.

https://docs.docker.jp/get-started/overview.html#docker-architecture

Dockerfileとは

アプリケーションを構築するには、Dockerfileを使います。 Dockerfileとは、コンテナイメージの作成で使う命令が書かれたスクリプトです。

以下のようなファイルを用意し、docker buildコマンドを使ってコンテナイメージを構築します。

# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY ..
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000

内容は

  • イメージのダウンロード
  • アプリの依存関係をインストール
  • CMD ... このイメージでコンテナを起動するときにデフォルトで実行するコマンドを指定
$ docker build -t getting-started .

-tタグでイメージにタグをつけることができます。コンテナの実行時にこのイメージ名を指定できます。
最後の.で現在のディレクトリのDockerfileを探します。

コンテナの起動

コンテナの起動には下記の様なコマンドを実行します。

$ docker run -dp 3000:3000 getting-started

-dオプションでバックグラウンドで実行されます。
-pオプションでコンテナのポートとホスト側のポートをマッピングします。

コンテナの停止、削除

$ docker ps

コンテナのIDが出力されます。

$ docker stop <コンテナID>

コンテナが停止されます。

$ docker rm  [-f] <コンテナID>

コンテナを削除します。-fオプションでコンテナと削除を同時に行います。

コンテナのデータの永続化

前提として、各コンテナではコンテナのファイルシステムに対する変更は他のコンテナからは見えません。
動作させて確認する場合は下記公式を参照してください。
https://docs.docker.jp/get-started/05_persisting_data.html

コンテナのボリューム

コンテナはファイルの作成、更新、削除ができますが、コンテナを削除すると、それらの変更は失われます。
ボリュームをコンテナ内にマウントすると、ディレクトリに対する変更はホストマシン上からも見ることができます。
コンテナの再起動の際にも同じディレクトリをマウントしていれば再起動後も同じファイルが見えます。

ボリュームには名前付きボリュームバインドマウントがあります。
Dockerがディスク上で物理的な場所を確保するので、ボリュームの名前を覚えておくだけで利用できます。

$ docker volume create <ボリューム名>

その後、コンテナを起動する際に-vフラグを追加することでボリュームをコンテナにマウントできます。
これでこのパスに生成されたすべてのファイルを保存します。

$ docker run -dp 3000:3000 -v <ボリューム名:マウントするパス> イメージタグ名

ボリュームの実体

公式の例では下記のようにdocker volume inspectコマンドでボリュームの詳細を見ることができます。

$ docker volume inspect todo-db
[
    {
        "CreatedAt": "2019-09-26T02:18:36Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

MountPointにディスク上で実際のデータが保管されます。

バインドマウントの使用

シンプルにデータを保存したい場合は名前付きボリュームが優れていますが、ホスト上でどこにマウントされるかを管理したい場合はバインドマウントという方法があります。

バインドマウントはデータ保持に使えますが、使用時はコンテナに対する追加データの指定が度々必要です。
アプリケーションの動作中でも、バインドマウントを使ってソースコードをコンテナ内にマウントするとコードの変更が見えたり反映したりできるようになります。

名前つきボリュームを利用した場合はホストマシン上に新たなディレクトリが生成され、そこがDockerの保存ディレクトリになりますが、バインドマウントはホストマシンのファイルシステムに依存します。
バインドマウントはDockerの初期のころから存在していて、今後は原則名前付きボリュームのほうが便利そうです。
https://matsuand.github.io/docs.docker.jp.onthefly/storage/bind-mounts/

複数コンテナのアプリ

アプリケーションとは別に例えばMySQLを用意したい場合、通常1つ1つのコンテナが1つのことをしっかりと実行すべきです。
公式には下記の理由が記載されています。

  • データベースとは別に、 API とフロントエンドをスケールする良い機会
  • コンテナを分けると、現在のバージョンと更新したバージョンを分離できる
  • 今はローカルにあるデータベースをコンテナが使っているが、プロダクションではデータベースのマネージド サービスを利用したくなるかもしれない
  • 複数プロセスの実行にはプロセスマネージャが必要であり(コンテナは1つのプロセスのみ起動するため)、コンテナの起動や停止が複雑になる

先述したとおりコンテナは、外部とは隔離された状態で実行されるため、基本的には同じマシン上の他のプロセスやコンテナを一切知りません。
他のコンテナと通信するために、ネットワーク機能と呼ばれる機能を使います。

ネットワーク機能の利用には以下の二種類の方法があります。

  • 起動する前にネットワークに割り当てる
  • 既存のコンテナに接続する

ネットワークを作成するコマンドは下記

$ docker network create <ネットワーク名>

公式のチュートリアルにあるコンテナ起動とネットワーク接続用コマンド(Apple Siliconの場合)が下記です。
todo-appという名前のネットワークに接続しています。

$ docker run -d \
    --network todo-app --network-alias mysql \
    --platform "linux/amd64" \
    -v todo-mysql-data:/var/lib/mysql \
    -e MYSQL_ROOT_PASSWORD=secret \
    -e MYSQL_DATABASE=todos \
    mysql:5.7

--networkで接続するネットワークを指定しています。
--network-aliasmysqlという文字列を指定しているので、IPアドレスを調べる際に使用する事ができます。

$ dig mysql

-vはボリュームの指定、-e環境変数を設定できます。

データベースが実行中であることを確認するには、下記のコマンドを使用します。

$ docker exec -it <mysql-container-id> mysql -u root -p

これでMySQLのコンテナを作成することができました。

あらためてまとめ

  • Dockerを使えばコードを管理するようにインフラも管理できる。
  • イメージとは、Dockerコンテナを作成する命令が入った読み込み専用のテンプレートのこと。
  • Dockerfileは、イメージの作成の際に使用する。
  • コンテナとは、イメージが実行状態となったインスタンス(実体)のこと。
  • コンテナ内でのファイルの変更を保存するには、ボリュームを使用する。
  • 複数のコンテナ同士の接続にはネットワーク機能を使用する

また、アプリケーションの起動に必要なコンテナの作成をより簡単な方法で実現するために、Docker Composeという仕組みがあります。 こちらについてはまたの機会に調べてみることにします!

所感

業務で携わるたいていのプロジェクトではすでに環境構築がされている場合がほとんどかと思います。
ただ、新規プロダクトの立ち上げであったり、より効果的なインフラ構成や開発者体験を求めようとすると避けては通れないものだと思います。
個人的には個人開発で環境構築する際に自分がDockerについて全然わかっていないことを改めて認識しました。
CI/CDとの相性もよく、簡単にデプロイできる環境を構築しておくことは取れる工数の少なくなりがちな個人開発においてもとても有用なものだと思いました。

astroに入門してみる

はじめに

最近フロントエンドへの自身の理解の甘さを感じるようになってきました。
ここ最近TypeScriptなどフロントエンドに関係する記事をいくつか公開しましたが、そろそろ実際になにか作ってみようと感じるようになりました。
そこで今回は気になっていたastroというフレームワークを触ってみようと思います。
題材としては、自作ブログにしたいと思います。
目的はブログ記事の管理を簡単にすることと、フロントエンドの学習、また、レンタルサーバーなどを用いてインフラ周りを含めた実践です。

今回はフロントエンド関連のキーワードについて調べながらastroの公式ドキュメントを読み進めていきたいと思います。

3行まとめ

astroとは

astroというフレームワークについて特徴をまとめておきます。

  • 高速なWebサイトの構築のためのオールインワンWebフレームワーク
  • サーバーファーストでサーバーサイドレンダリングを最大限活用する。
  • アイランドアーキテクチャと呼ばれるアーキテクチャを採用している
  • 高価なハイドレーションをデバイスから取り除く
  • 様々なインテグレーションによってカスタマイズが可能
  • ロードが遅くなる原因となる JavaScript をデフォルトではクライアントで起動しない
  • React, Preact, Vue, Svelte, Solidなど様々なフレームワークをサポートしています。

コアコンセプト

https://docs.astro.build/ja/concepts/mpa-vs-spa/

astroはMPAの考え方に沿ったフレームワークです。
そのため、フロントエンドにおいてMPAとSPAの違いを再確認してみます。

MPA

マルチページアプリケーションの略。複数のHTMLページから構成されるWebサイトのこと。
新しいページに移動すると、ブラウザはサーバーに新しいページのHTMLを要求します。
従来のフレームワークの例としてはRuby on RailsPython Django, PHP LaravelWordPressなども静的サイトジェネレータです。
ただ、astroがその他のMPAフレームワークと違う点はサーバー言語とランタイムにJavaScriptを使用している点です。
結果として、Next.jsやその他のモダンWebフレームワークとよく似た感覚で、MPAサイトの利点であるパフォーマンス特性を備えた開発者体験が得られます。

SPA

シングルページアプリケーションの略。 ユーザーのブラウザに読み込まれ、ローカルでHTMLをレンダリングする単一のJavaScriptアプリケーションで構成されるウェブサイトのこと。
ブラウザ上でJavaScriptアプリケーションとしてウェブサイトを実行し、ページ遷移したときに同じHTMLのページを再描画する機能が特徴的。 従来のフレームワークとしてはNuxt.js, Next.js, SvelteKit, Gatsby, Create React Appなどが挙げられます。

MPAと比較した場合のメリット

  • 最初のページロード以降はユーザー体験としてはMPAより良くなる。
  • SPAはページレンダリングに関連する多くの制御を行うため、遷移時のアニメーションを提供する事ができる。MPAの場合はTurboのようなツールを使う必要がある。
  • アプリケーションが複数のページに渡って状態とメモリを維持できるため、複雑な複数ページの状態管理を扱うWebサイトとして優れている。

MPAと比較した場合のデメリット

  • ブラウザでHTMLをレンダリングするため、最初のページ読み込み時のパフォーマンスはMPAに劣る。
  • シンプルさ、パフォーマンスではMPAノフが優れている。

結論

MPAとSPAでは「どちらが優れている」ということはなく、常にトレードオフの関係にある。

  • 複雑な状態管理を必要とするページの場合はWebサイト全体を単一のJavaScriptアプリケーションのように扱えるSPAのほうが向いている
  • コンテンツに特化してシンプルさやパフォーマンスを重視するような場合はMPAのほうが向いていて、astroはMPA

astroアイランド

https://docs.astro.build/ja/concepts/islands/

astroアイランドとはざっくりいうとHTMLの静的なページ上にあるインタラクティブコンポーネントを指します。
アイランドは常に独立したコンポーネントとして表示され、静的なコンテンツの海に浮かぶ島に比喩されます。
astroでは、ReactSvelteVueなどのコンポーネントを使用して、ブラウザ上でアイランドをレンダリングする事ができます。
アイランドの考え方から、同じページで様々なフレームワークを混在させることも可能です。
astroはデフォルトではクライアントサイドでは一切JavaScriptを使用しません。
ReactSvelteVueなどのコンポーネントを使用した場合には自動的に前もってHTMLとして生成し、JavaScriptを取り除いて表示します。
インタラクティブにしたい部分のみをclientディレクティブと呼ばれるものをつけて描画するよう指示します。

参考

https://jasonformat.com/islands-architecture/

astro2.0について

https://astro.build/blog/astro-2/ 2023年の1月24日にastro2.0がリリースされました。
特に大きなアップデートとして、MarkdownとMDXに型安全性が追加されました。
WebでMarkdownを使用する場合に型安全に取り扱う事ができます。

例えば、ブログ、ニュースレターなど多くのファイルが存在する場合にプロパティを設定しておくと一貫性を保つことができます。
この一貫性のために、型安全性が追加されました。

また、astro2.0にて追加されたハイブリッドレンダリングという機能で、静的コンテンツと動的コンテンツを組み合わせて使用することができます。
これによって、既存の静的サイトにAPIを追加したり、ページのレンダリングパフォーマンスを改善する事ができます。

MarkdownとMDXについて

https://docs.astro.build/ja/guides/markdown-content/
astroでは、インテグレーションをインストールすることで各種UIフレームワークを使用できるようになりますが、MDXファイルも対応しています。

MDXの機能について

ざっくり簡単なMDXの概念をまとめておきます。

公式: https://mdxjs.com/

参考: https://zenn.dev/spring_raining/articles/3eb62ff93df1eb

MDXはMarkdown + JSXを名前の由来としています。
MDXを使用することで以下のメリットがあります。

公式サイトから引用した例は下記の様になります。
一見MarkdownにHTMLが埋め込まれたもののように見えます。

export const Thing = () => <>World</>

# Hello <Thing />

これがJavaScriptに変換され、下記のようになります。

/* @jsxRuntime automatic @jsxImportSource react */

export const Thing = () => <>World</>

export default function MDXContent() {
  return <h1>Hello <Thing /></h1>
}

変換後のコードを見てみると、exportが記載されており、コンポーネントとして出力されることがわかります。

下記のようなmdxコンポーネントを作成し、

# Hi!

下記のようにインポートできます。

import React from 'react';
import ReactDom from 'react-dom';
import Example from './example.mdx';

ReactDom.render(<Example />, document.querySelector('#root'));

jsxとして出力されていますが、Reactに結合しているわけではなく、Preact, Vue, Emotionなどでも使用できるようです。
あくまでプログラミング言語ですが、jsxを理解できればあまり学習コストは高くないように感じました。

astroにおけるルーティング

https://docs.astro.build/ja/guides/markdown-content/#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%99%E3%83%BC%E3%82%B9%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

astroでは、先述したMarkdown及びMDXを使用することで簡単にルーティングを行うことができます。
src/pagesディレクトリ内に.mdファイルもしくは.mdxファイルを置くだけです。
また、サブディレクトリ配下も自動的にページルートを構築します。
また、front-matterを使用することで下書きページ扱いできたりもします。

---
title: タイトル
draft: true
---

markdownファイルの結果はastro.glob()関数にて取得できるので、ビルドに含めないようにフィルタリングすることで下書き機能が実現できます。
astro.glob()については公式リファレンスを参照すると動きがわかりやすいです。
https://docs.astro.build/en/reference/api-reference/

const posts = await Astro.glob('../pages/post/*.md');
const nonDraftPosts = posts.filter((post) => !post.frontmatter.draft);

また、デフォルトでShikiとPrismといったシンタックスハイライトをサポートしているのもエンジニアからすると嬉しい機能です。

まとめ

  • astroはパフォーマンスを重視したMPAフレームワーク
  • React, Vue, Svelteのような異なるフレームワークを使用できる
  • MarkdownやMDXを使用したコンテンツ重視のWebサイト構築にはかなり向いている。
  • シンタックスハイライトやファイルベースルーティングなどmarkdownを中心にサイト構築する際に楽な要素が多い

所感

個人的には特にエンジニアにとって、技術ブログや個人サイトを構築する際に最有力になり得るフレームワークだと感じています。
現在担当プロダクトでReactを導入しているプロダクトはないのですが、これを機にastroでReactを使ってみたいと思っています。
現在主流のNext.jsとはすこし方針が違いますが、個人のブログではこちらのほうが向いてそうだなと感じています。(いい意味で少し変態的な部分に惹かれています)

また、いままで個人サイトの構築経験がなかったため、ドメインの取得やサーバーへのデプロイなど含めてチャレンジしてみようと思ってます。
最近は下記のリポジトリでastroのチュートリアルを試してみたり、ファイルベースルーティングなどを試しながら遊んでいます。
デプロイ方法としてはRoute53でドメインを購入して、astro公式ドキュメント通りNetlifyにデプロイしています。
astroを布教して社内でもエンジニアの技術発信がもっと活発になればいいなと思ってます。

リポジトリ https://github.com/taiseimiyaji/astro-blog

デプロイ先 https://www.lyricrime.com/

TypeScriptの型について

TypeScriptの型付け

TypeScriptはJavaScriptに対して型を付与するという思想で仕様が定められています。
TypeScriptでは型を付与する方法として、様々な方法が用意されていますが、どこまで利用するかは費用対効果を考えながら行う必要があります。

any型 最もゆるい型付け

function example(args: any){
    // argsにhogeが存在するかのチェックはしないのでコンパイルエラーとはならない
    console.log(args.hoge);
}

any型を使った場合、TypeScriptの型チェックの恩恵を受けることができません。
any型は型チェックを無効化する型です。any型の変数になにかを代入することや、any型の値を他の型の変数に代入することに対してもコンパイルエラーは発生しません。
JavaScriptからの移行時等に一時的に利用するなど以外は原則使用しないようにするべきです。

unknown型

unknown型はany型と似ています。
「型安全なany型」と呼ばれ、何でも入れられる型です。
unknown型にはどのようなデータもチェックなしに入れる事ができます。
any型と違う点は変数を利用する場合に型アサーションを使ってチェックを行わないとエラーになる点です。

any型はどのような型の変数にも代入できますが、unknown型の値は具体的な型へ代入できません。

const value: any = 10;
const int: number = value;
const bool: boolean = value;

また、ジェネリクスを使ったクラスや関数のうち、自動で型推論で設定できなかったものがunknownとなります。
この型変数のunknownに関してはエラーチェックなどが行われることがなく、anyのように振る舞います。

unknownの用途としてはany型の値をより安全に扱うことです。unknownに対して許可される操作は限定的です。
例えば、ピリオドを使用してメンバーアクセスをしたり、メソッド呼び出しをしようとするとコンパイルエラーとなります。
any型を扱う場合は一旦unknown型にしておくことで存在しないプロパティへのアクセスにコンパイル時に気づきやすくなります。

  • unknownと型の絞り込み

unknownの値を実用的に使うためには型を絞り込む必要があります。
型の絞り込みにはtypeofinstanceofなどを条件式に含んだif文やswitch文を使います。 これは型ガードといい、それ以降の処理では絞り込まれた型として扱う事ができます。

function useUnknown(value: unknown) {
    if (typeof value === "string") {
        // この時点でvalはstring型になる
        console.log(value.toUpperCase());
    }
}

ユニオン型とインターセクション型

日本語でいう「または」や「かつ」を表現する型です。

  • ユニオン型

「型Tまたは型U」のような表現ができる型です。
「T | U」のように書きます。

type Animal = {
    species: string;
}

type Human = {
    name: string;
}

type User = Animal | Human;

const tama: User = {
    species: "calico"
}

const you: User = {
    name: "Your Name"
}

「ユーザーには動物と人間の2種類がある」という場合、つまりユーザーは動物または人間である」という場合を想定しています。
下記のように、コンパイラによる型チェックを受けられます。

// エラーが発生する
const book: User = {
    title: "Software Design"
};

User型を持つことだけがわかっている場合は実際にはそれがAnimalなのかHumanなのか不明です。

function getName(user: User): string {
    return user.name;
}

userはAnimalかもしれないしHumanかもしれません。Animalにはnameというプロパティが存在しないため、userがnameを持たない可能性がある場合はコンパイルエラーになります。
つまり、上記で作成したUser型は全くプロパティアクセスができません。

反対に、必ず存在するプロパティの場合は下記のようになります。

type Animal = {
    species: string;
    age: string;
}

type Human = {
    name: string;
    age: number;
}

type User = Animal | Human;

const tama: User = {
    species: "calico",
    age: "永遠の17歳"
}

const you: User = {
    name: "Your Name",
    age: 25
}

function showAge(user: User) {
    const age = user.age;
    console.log(age); // ageは string | number型となる
}

ユニオン型に対するプロパティアクセスが可能である場合、その結果はユニオンの構成要素それぞれのプロパティの型を集めたユニオン型となります。

  • インターセクション型

「T & U」のように書く、「T型かつU型」を意味する型です。交差型とも呼ばれます。
例えば、下記のように「HumanはAnimalの一種である」ことを表現します。

type Animal = {
    species: string;
    age: number;
}

type Human = Animal & {
    name: string;
}

const tama: Animal = {
    species: "calico",
    age: 3
};

const you: Human = {
    species: "Homo sapiens",
    age: 26,
    name: "Your Name"
}

Human型は、Animal型を拡張してstring型のnameプロパティを持ちます。
つまり、下記と同様になります。

type Human = {
    species: string;
    age: number;
    name: string;
}

また、&で作られた型はそれぞれの構成要素の型の部分型となります。
HumanはAnimalの部分型となります。

ユニオン型とインターセクション型の関係

type Human = {name: string};

type Animal = {species: string};
function getName(human; Human) {
    return human.name;
}
function getSpecies(animal: Animal) {
    return animal.species;
}
const mysteryFunc = Math.random() < 0.5 ? getName : getSpecies;

変数mysteryFuncにはgetNameが入るかもしれないしgetSpeciesが入るかもしれません。
この場合、mysteryFuncは以下の様な型になります。

((human: Human) => string | ((animal: Animal) => string))

mysteryFuncを関数として呼び出したい場合、Human型を受け取るとは限らないのでHumanを渡すことはできないし、その一方でAnimalを受け取るとは限らないのでAnimalを渡すこともできません。
ユニオン型を持つ関数はどんな引数を受け取るのか不明なので扱いが困難です。

ここでmysteryFuncを呼び出す方法は、Human & Animal型を渡すことです。
つまり、ユニオン型とインターセクション型は全くの無関係ではなく、ユニオン型からインターセクション型が生み出される場合もあります。
「AND」と「OR」は論理学的にも表裏一体の関係なので不自然ではないです。

異なるプリミティブ型同士でインターセクション型を作った場合はnever型が出現します。

never型

unknown型の真逆の存在で、「当てはまる値が存在しない」という性質を持ちます。
never型にはnever型以外何も代入できません。
正規の手段でnever型の値を得ることは不可能であり、言い換えるとnever型の値が存在しているコードは実際には実行されません。
ただし、never型はどんな型にも代入することができます。

const nev = 1 as never;
const a: string = nev;
const b: number = nev;
const c: string[] = nev;

これは、「never型はすべての型の部分型である」からです。
また、never型はユニオン型の中では消えることも把握しておく必要があります。
例えば、string | never はstringと同じです。

ユーザー定義型ガード(user-defined type guards)

ユーザー定義型ガードとは、型の絞り込みを自由に行うためのしくみです。
注意点として、ユーザー定義型ガードはanyやasの仲間であり、型安全性を破壊する恐れのある危険な機能の一つです。
型述語(type predicates)と呼ばれるものを返り値の型に書きます。
型述語の書き方には以下の2種類があります。

  • 引数名 is 型
  • asserts 引数名 is 型
function isStringOrNumber(value: unknown): value is string | number {
    return typeof value === "string" || typeof value === "number";
}

const something: unknown = 123;

if(isStringOrNumber(something)) {
    //この時点でsomethingは string | number型
    console.log(something.toString());
}

下記のように、isStringOrNumberの返り値をbooleanに変えてみると、コンパイルエラーとなります。

function isStringOrNumber(value: unknown): boolean {
    return typeof value === "string" || typeof value === "number";
}

const something: unknown = 123;

if(isStringOrNumber(something)) {
    // エラー: Object is of type 'unknown'.
    console.log(something.toString());
}

ユーザー定義型ガードは関数を絞り込みに使うことができます。ですが、その関数の型定義でユーザー定義型ガードを使わなければいけません。
注意点としては、ユーザー定義型ガードは関数の実装内容はTypeScriptの保証する範囲ではないことです。
下記のような間違った実装をしてしまった場合でもコンパイルエラーは発生しないため、型安全性を破壊することになります。

function isStringOrNumber(value: unknown): value is string | number {
    // 実装を間違えているがエラーが起きない!
    return typeof value === "string" || typeof value === "boolean";
}

所感

今回はTypeScriptの型の一部について調査しました。
特にユニオン型とインターセクション型は使用頻度が高くなりそうな型でした。
TypeScriptには他にも高度な型が用意されていますが、今回の記事である程度基本的な型をおさえることができたので、残りについてはなにか作ってみてから調査したいと思います。
個人的には最近のフロントエンド界隈ではastroが気になっているので入門記事も今度書いてみようと思います。

参考

https://future-architect.github.io/typescript-guide/typing.html
https://typescriptbook.jp/  

TypeScriptでAtCoderに挑戦してみる

はじめに

スマレジでは、副業が可能です。
https://corp.smaregi.jp/recruit/culture/workstyle.php

個人的に最近、副業にも挑戦しようかなと考え始めています。
副業に割ける稼働時間的には週10h程度となりそうなので、比較的タスクを細かくしやすいフロントエンドの案件を中心に探していたりします。
担当するプロダクト的にはバックエンドを中心に開発してきましたが、最近TypeScriptやReactに興味があるので、そちらのキャッチアップの加速にも役立つかなと考えています。
そこで今回はTypeScriptに慣れることと、コーディングテスト等への対策としてAtCoderの問題をTypeScriptで解いてみたいと思います。

AtCoder

競技プログラミングと呼ばれるプログラミングコンテストサイトです。
後の説明は省略します。

https://atcoder.jp/?lang=ja

過去問は下記サイトから閲覧できます。

https://kenkoooo.com/atcoder/#/table/

なお、問題に色がついていますが、

灰色 -> 茶色 -> 緑色 -> 水色 -> 青色 ...

の順番で難しくなっていきます。
この辺りのレートのシステムについても公式を参考にしていただければと思います。

以前少しやったことがあるんですが、その際の個人的な感触としては以下の様な感じです。
業務上はC問題くらいはすらすら解けるといいな〜と思ってたりします。

A問題...標準入出力とプログラミングの基礎がわかれば誰でも解ける   
B問題...問題文をきちんと理解できてfor文とif文が書ければ愚直に解ける   
C問題...愚直に解くだけでは大抵計算量で弾かれてしまうのでなにかしら対策が必要   
D問題以降...何回か挑戦したけど試行回数少なくてあまりわかってない   

環境構築

前提として、真面目に競技プログラミングに取り組む場合はTypeScriptはおすすめしません。
大人しくC++Pythonを使用するべきだと思っています。
が、今回はTypeScriptに慣れることが目的のため、あえてTypeScriptを使用していきます。

nodeやnpmのインストールは今回は省略します。
まずはパッケージ管理のためにpackage.jsonを下記コマンドで作成します。

npm install

必要なものは以下のパッケージとなります。

  • typescript@3.8 (本記事執筆時点でのAtCoderでのTypeScript対応バージョン)
  • ts-node (TypeScriptのまま実行するためのライブラリ)
  • @types/node (TypeScriptの標準入出力ライブラリ等を含むライブラリ)

忘れずにtsconfig.jsonの作成もしておきましょう。

npx tsc --init

標準入出力

TypeScriptで一番めんどくさいのが標準入出力になります。

windowsでは使えないですが、以下が一番シンプルな記述かと思います。

const input = require('fs').readFileSync('/dev/stdin', 'utf8');

毎回書くのも面倒なのである程度テンプレートとしておくのがいいと思います。
Ctrl + Dで入力終了です。
他にもいくつか手段はあるので気になる方がいれば調べてみてください。

簡単な問題に挑戦!(ABC285)

以下、AtCoderの問題にいくつか挑戦してみます。
正解できるコードではあるはずですが、より効率的なものやわかりやすい処理等あると思いますのであくまで参考としてください。

A問題 Edge Checker 2

まずは一番簡単なA問題に挑戦してみます。
記事執筆時の直近のA問題に挑戦します。

https://atcoder.jp/contests/abc285/tasks/abc285_a

  • 考え方

競技プログラミングではある程度スピードが求められるので、問題を見て実装方法を判断します。
今回のA問題では2パターンぱっと思いついたのでそれぞれ考え方を整理しておきます。

  1. 規則性を見つける方法
    直接結ばれている親と子には規則性があります。
    親の数字をnとしたとき、2n,2n+1であれば直接結ばれています。

上記を考慮してコードを書くと以下の様になります。(競技プログラミングの特に簡単な問題では変数名の命名が適当になりがちなんですがご容赦ください)

import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8").split(/\s/);
const a = +input[0];
const b = +input[1];

const ans = (Boolean)(b === (2 * a) || b === (2 * a + 1));

if (ans) {
    console.log("Yes");
} else {
    console.log("No");
}
  1. 愚直に書く方法
    これはA問題のようなパターンが少なく、制約がある問題で使える方法なんですが、愚直に全てのパターンを実行することでも正解することができます。 説明するよりコードを見てもらった方が早いと思います。
import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8").split(/\s/);
const a = +input[0];
const b = +input[1];

const tree =
{
    1: [2, 3],
    2: [4, 5],
    3: [6, 7],
    4: [8, 9],
    5: [10, 11],
    6: [12, 13],
    7: [14, 15],
};
console.log(tree[a] && tree[a].includes(b) ? "Yes" : "No");

console.log()の中でtree[a]の存在をチェックして7より大きい場合は全てNoとしています。
あとは愚直にオブジェクトを使用しています。
今回の様なわかりやすい例ではあまり使用しませんが、いざコンテストとなると早く解かないとという焦りなどからA問題で混乱してしまうこともあるので、この様な愚直な解き方も覚えておくと使えるかもしれません。

B問題 Longest Uncommon Prefix

https://atcoder.jp/contests/abc285/tasks/abc285_b

個人的にB問題を解く時の注意点は仕様の誤解をしない様にすることです。
愚直な方法で解けることが多いですが、少し問題文が複雑になっていたりするので、そこだけ気をつければ解ける問題が多いと思います。(もちろんたまに難しいやつもあります)

いや難しい、、、。普通に問題の意味を理解するのに時間かかりました。
しかもバグらせて一回WA(不正解)出しました、、、。

考えた方法を整理していきます。

  1. まずはTypeScriptの標準入力でちゃんと数値と文字列として受け取る(ここで少し調べたりしてロス)
  2. 問題文を見てループ変数iのループをまず書く
  3. 問題文のケースを見てチェックするためのループ(ループ変数がk)のものを考える。
  4. i = 1の時チェックはS5まで行う(最後のチェックはS5とS6)
  5. i = 2の時チェックはS4まで行うが途中で条件を満たしbreak
  6. i = 3の時チェックはS3まで行う(最後のチェックはS3とS6)
  7. ここまででkの最大値がn-6であることがわかる。また、チェックする文字は(SkとSk+i)
  8. 求めるのは条件を満たす最大値lなので、問題の条件を満たしていれば更新、条件を満たさない場合はbreakする;
  9. 問題文より、breakする条件はSi === Sk+iのとき。

上記をもとにまずは愚直にループを回してみます。

import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8").split(/\s/);
const n: number = parseInt(input[0]);
const s: String = input[1];

for (let i = 0; i < n; i++) {
    let l = 0;
    for (let k = 0; k < n - i; k++) {
        if (s.charAt(k) === s.charAt(k + i)) {
            break;
        } else {
            l++;
        }
    }
    console.log(l);
}

愚直に用件を実装しましたがこれで正解(AC)取れたのでよしとします。

C問題

C問題以降は多少実装に工夫が必要になる場合があります。
今回の問題の鍵は問題文をみて入力文字列を26進数の数値だと考えることだと思います。
と、個人的に考えていたのですが公式解説を回答後に確認すると26進数として考えるのが一番目の解説方法じゃなかったです。
類題をみたことがあったのでパッと解法思いつきましたが大変なのはここからでした。

  • 一回目の提出(WA)
import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8").split(/\s/);
const s: String = input[0];

const length = s.length;
const charCodeAt = 64;
let num = 0;
for (let i = 0; i < length; i++) {
    num += (s.charCodeAt(i) - 64) * (26 ** (length - (i+1)));
}
console.log(num);

上記の提出で50ケース中47ケースACで3ケースWAとなりました。
このパターンが一番きついですね。ある程度のロジックは合っていますし、人間は自分の書いたコードを信じたがる業の深い生き物なので。

経験上ですが、こういったほぼほぼ正解でいくつかのテストケースのみ落ちる場合は境界値系のテストで落ちている可能性が高いです。
今回の場合はJavaScriptのnumber型の最大値を超えていました。
BigIntを使用するよう修正します。合わせて少し解説コメントもいれてみやすくしました。

import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8").split(/\s/);
const s: String = input[0];

const length = s.length;
const charCodeAt = 64;
let num = BigInt(0);
for (let i = 0; i < length; i++) {
    const charNum = (s.charCodeAt(i) - charCodeAt); // アルファベットを文字コードを使用して数値に変換
    // 1桁目の場合は 26の0乗 * 文字列の番号
    // 2桁目の場合は 26の1乗 * 文字列の番号
    num += BigInt(charNum * (26 ** (length - (i+1))));
}
console.log(num.toString());

所感

今回はAtCoderにTypeScriptで挑戦してみました。
C問題くらいまでであれば言語に関係なく解くことができそうです。(D問題以降は挑戦してみる価値はありそう)
今回はB問題で思いのほか時間を取られましたが、C問題はさくっと解くことができました。
これからもアルゴリズム力の強化のためにコンテストに参加してみたいと思います。
TypeScriptは使用しているユーザーは多いと思うので興味があれば過去問等からでも解いてみてください。

積読解消 「レガシーコード改善ガイド」を読んでリファクタリングの知識をつける

はじめに

自チームで担当しているプロダクトでは常にリファクタリングを意識した開発を行っています。
なかにはかなりレガシーなプロダクトもあり、その改善への指針として書籍「レガシーコード改善ガイド」を購入していました。
去年の秋頃に買ったんですがしばらく放置していたため、年明けのこのタイミングで積読の解消をしていきます。

対象書籍

レガシーコード改善ガイド

概要

書籍には、ざっくり以下の内容が書かれています。C++/Javaのコードがサンプルとして使用されています。
具体的なコードの修正方法のような技術的な部分だけでなく、「良い設計とは」のような設計論、ペアプログラミングのような方法論についても触れています。
今回はテストに対する考え方の部分を中心に自分の考えと比較しながらのメモを残していきます。
具体的なコードの修正方法についてはぜひ書籍を手にとってみてください。

  • 仕様がわからないコードの分析方法、修正方法
  • レガシーなコードを疎結合な設計に部分的に改善する方法
  • 良い設計についての原則とその例

読書メモ

そもそもソフトウェアの変更とは?

ソフトウェアに変更が必要となるケースには以下のような理由がある。

  • 要件の追加
  • バグ修正
  • 設計の改善
  • リソース利用の最適化

変更を行いながら既存の振る舞いを維持するのはとても難しく、大きなリスクを伴う。
以下3点を考慮する必要がある

  • どんな変更を行わなければならないか
  • 変更が正しく行われたことをどうすれば確認できるか
  • なにも壊していないことをどうすれば確認できるか

変更の難易度が高いからといってコードの変更を避けると全体の見通しが悪くなる。そのため将来的に更に変更の難易度が上がる。 また、コードの理解にかかる時間も増えてしまう。

所感
->近年のエンジニアの転職事情を考えると属人化したコード管理は避けたい。
->変更が容易で理解も容易(部分ごとに関心事が分離されている)コードの価値が高い

フィードバックを得ながらの作業

コードの変更は難易度が高い作業だが、フィードバックを得ながらの作業によってその難易度やエンジニアのストレスを低減できる。
テストを書くことでコードの振る舞いの大部分を固定し、意図した箇所しか変更していないことを確認する。

結合テストも大事だが、だからといって単体テストを省略するといくつか問題がある。なによりテスト頻度が落ちるのが良くない。

  • エラー発生箇所の特定が難しくなる
  • 実行時間が長い
  • テストケースを動かすための値の準備が大変

レガシーコードの変更手順

  1. 変更点を洗い出す
  2. テストを書く場所を見つける
  3. 依存関係を排除する
  4. テストを書く
  5. 変更とリファクタリングを行う

テストの最大の障害は依存関係なので依存関係を整理できる設計(ex.クリーンアーキテクチャ)への理解が必要。

所感
->C/C++においてはプリプロセッサを利用してテストが書ける(Cの経験はあったがプリプロセッサをテストに使用する考え方を知らなかった)
->結局テストしやすい設計を初めから意識する必要があるのでは???

テストによる時間の節約

コードが書かれる機会より読まれる機会が多い。
一切変更がない、バグも潰しきったコードならテストの効果は最小限になってしまうがそんなプロジェクトは殆どない。
最終的にテストコードの整備は開発作業全般を早めることになるため、ほとんどの開発組織にとって重要なトピックになる。
コードは自分の家であり、その中で暮らさなければいけない。日々きれいにしていくべき。

スプラウトメソッド(Sprout Method)

具体的な改善手法としていくつか紹介されているうちの一つ。 スプラウトとは、「発芽」のような意味合い。
古いコードに対してテストを書くことは難しいが、新しく追加する要件に対してテストを書くことは容易。
特に独立した1つの機能としてコードを追加する場合や、まだメソッドのテストを整備していない場合に推奨される。

  1. 変更が必要なコードを洗い出す
  2. もしその変更が、メソッド中の1つの場所で一連の命令文として実現できるなら、必要な処理を行う新しいメソッドを呼び出すコードを書いて、それをコメントアウトしておく
  3. 呼び出されるメソッドで必要となるローカル変数を特定し、それらを新しいメソッド呼び出しの引数にする
  4. 新しいメソッドから呼び出し側のメソッドに値を返す必要があるかどうかを決定する。必要があるなら、戻り値を変数に代入するように呼び出し側を変更する
  5. 新しく追加するメソッドをテスト駆動開発により作成する
  6. 呼び出し側のコメントを外して、新しく追加↓メソッドの呼び出しを有効にする

短所
- 元のメソッドとクラスは放置される

長所
- 古いコードと新しいコードを明確に区別できるようになる

こんな感じのレガシーコードに対する改善テクニックがいくつか紹介されている。

所感
->大きなコードを改善する際の最大の障害は既存のコードであり、一度にリファクタリングするのはかなり難しいのでリファクタリングによって一時的に多少形がいびつになろうとも日々きれいにしていくほかない。
->ラップメソッドやラップクラスを使って一時的にコードが汚くみえたとしても将来のために依存関係を整理し、きっかけを作る事が重要。設計の観点からは理解しがたい手法かもしれないが、明確な新しい責務と古い責務を分離することがより優れた設計へ向かう唯一の方法。
->人は汚い箇所にゴミを捨てたくなる。右に倣えでコードを書かない勇気が必要。

オブジェクト指向によるコードの複雑化

オブジェクト指向の悪しき歴史(継承等)からどう脱却するか。
テストの書きづらさやコードの複雑さは継承やオーバーライドから来ているパターンが多そう。
グローバル変数を使用するためのsingletonパターンの例から、一般的に言われる「グローバル変数は悪」の例とテストの書きにくさを実感できる。

テストコードは本番コードと同じルールに従う必要はない(変数をpublicにするとか)が、かんたんに理解でき、変更できるものでなければならないため、きれいであるべき。

所感 ->LaravelにおけるDI等で当たり前のように書いていたコードがどういった問題を避けることができ、以下にテストが書きやすくなるかを実感できる。 ->例えば、コンストラクタのパラメータ化とか。コンストラクタ内でオブジェクトが生成されており、生成の依存関係がない場合には簡単にコンストラクタのパラメータ化ができる。

->あまりキャリアが長くなくて、かつ恵まれた環境にいたため、目にすることのなかったレガシーなコードの例とその対処法を知ることができた。

変更をどのように進めるべきか

変更する場合にどのメソッドをテストすればよいか - 変更内容を検討する - 何に影響するかを調べる - 影響を受けたものがさらに何に影響するかを調べる

この影響範囲の調査の方法
影響を受ける変数と戻り値が変わる可能性のあるメソッドについてスケッチを書く
このスケッチが単純であればあるほどソフトウェアとして良い構造

privateをキーワードを適切に使用して不要な影響範囲をなくすこと、なにがmutableでなにがImmutableなのかは言語仕様によって異なるため、使用する言語についてよく知ることが必要

ときにカプセル化と依存関係を排除しテストでコードを保護することが対立することがあるが、その場合はテストによる保護を優先する。
結果的に将来のカプセル化を強めるために役立つことが多い。

一箇所にたくさんの変更が必要な場合、関係するすべてのクラスの依存関係を排除するべきか。

いくつかの変更を一度にまとめてテストできる場所を見つけることができれば、より粒度の大きなテストを行うことができる。
変更のためのテストだけでなく、該当箇所を更にリファクタリングするためのテストも手に入る。
ただ、単体テストの代わりに使うのではなく、単体テスト整備のための最初の一歩とするべきである。

仕様化テスト

ソフトウェアが実際にどう動いているかをテストコードとして表現することで変更を検知したり、リファクタリング時の振る舞いの固定化使用する。 作成手順は以下

  1. 変更する部分のためのテストを書く。コードの振る舞いを理解するために必要と考えられるテストケースをできるだけたくさん書く
  2. テストを書いたあと、変更したい具体的な事柄について調べ、それに関するテストを書く
  3. 機能の抽出や移動をしようとする場合、既存の振る舞いや振る舞い同士の関係を検証するテストを個々に記述する。それにより、移動対象のコードが実行されること、及びそのコードが適切に関係し合っていることを検証する。その後でコードを移動する。

試行リファクタリング

コードを変更する場合、変更箇所や影響箇所について深く理解する必要がある。
その手法の一つとして試行リファクタリングという手法が存在する。

ブランチを切って、あらゆる方法を用いてリファクタリングする。
そしてそのコードをチェックインすることなく破棄する。
最終的に破棄してしまうため一見無駄に思えるかもしれないが、短い時間で効果的にコードについて学ぶことができる。

所感

幸せなことに、書籍に紹介されているほどひどいコードに実際に出会ったことはないですが、良い設計のために留意すべきことがより具体的に理解できました。

具体的な例を見ることで、実際の業務等でのリファクタリングの作業イメージを持つことができました。
また、設計についてもテストの容易性の観点や依存関係の複雑さ観点から改めて考えるきっかけになりました。
書籍自体は多少内容が古い部分もありますが、設計等本質的な部分は現在でも十分参考にできると感じています。
古いと感じている箇所としては、具体的なコード修正に関する箇所で、現代においてはIDEやツールで解決する問題が結構登場します。
また、全体的に説明を他の章に移譲している箇所が多く、書籍としては読み進めにくい部類でした。

特に印象に残ったワード

設計等についての記述等から、特に印象に残った記述についてまとめます。

  • 良い設計はテスト可能であり、テスト可能でない設計は悪い設計である
  • 書籍全体を通して推奨されているテスト駆動開発は一度に一つのことを行うための手段であり、プログラミングとは、一度に一つのことを行う技術である。

課題

書籍を読み進めるにあたって、あまり同意できない箇所があり今後の課題としてまたの機会に調査してみようと思います。

  • sealedやfinalはテスト容易性を下げるため控えめに使う