最初はよい感じだったはずのシステムがなぜ頭痛の種になってしまうのか、どうやったらその流れに逆らえるのかをずっと考え続けてきた。辿り着いた結論はこうだ。
コードが増えるとメンテナンスしにくくなる。コードを減らせばメンテナンスしやすくなる。
馬鹿みたいだが、コードベースを小さく保つのは本当に大事なことだ。調査していたコードがすでに使われていないとわかったときの徒労感は誰しも覚えがあるだろう。同じことをしているはずなのに微妙に異なるコードがいくつも見つかってイライラした経験もあるはずだ。
私の主張はこうだ。コードベースを小さく保つことは、無数にあるやったほうがいいことのひとつではなく、メンテナブルなシステムを実現するための必要条件だ。ひょっとしたらエレガントな設計や網羅的なテストスイートや型システムの導入よりも大事かもしれない。迷ったときやよくわからないときに選んでいい、それぐらいの優先度がある。
なぜ見過ごされるか
重要性は誰もが認める。しかし実際にコードを積極的に減らしているチームは多くない。なぜか。
理由のひとつはエンジニアリングが「新しく作ること」と同一視されがちなことだ。コードを消す、機能を削る、仕組みをシンプルにする、こういった作業は地味で、技術ブログのネタにもなりにくい。派手なアーキテクチャの刷新は発表のタイトルになるが、不要なコードをこつこつ消し続けていることを話題にする人はあまりいない。
もうひとつは日々の忙しさだ。やったほうがいいとわかっていても、目先のリリースに追われているとリファクタリングはTodoリストに入ったまま日の目を見ない。そのまま時間が経つと、いざ手をつけようとしたときには対象のコードへの理解が薄れてしまっていて、何をしていたコードなのかを理解するところから始めなければならない。コストが雪だるまになっていく。
コードのメンテナンスは延命にすぎず、根本的な対応は特別なイベントとして行うものだという認識もある。しかしそれは逆だ。日々当たり前にやるべきことであって、できていないから特別なイベントになってしまう。
コードベースを小さく保つとはどういうことか
コードベースを小さく保つとは、いらなくなったコードが残っていない状態を維持することだ。使われていない関数、到達しない分岐、かつての仕様の名残で残っているモデルやテーブル、こういったものが積み重なってコードベースを不必要に大きくしていく。
小さなコードベースがもたらす利益は複利的だ。grepしたときにヒットする行数が減り、テストの実行時間が短くなり、変更の影響範囲を見通せるようになる。多少設計がまずかろうがバグがあろうが、全体のコード量が少なければ大抵のことには対処できる。同じバグでも対象のコードが100行、1画面に収まる程度なら直すのに1日も掛からないだろうし、最悪でも一から書き直せる。これが1,000行や10,000行になると途端に話が難しくなってしまう。
差分を最小化するのではなく全体の複雑性を最小化する
温泉旅館を思い浮かべてほしい。必要に応じて増築していった旅館は、廊下を歩いていると階段が突然現れたり、棟と棟のつなぎ目に不思議な段差があったりする。個々の増築はそのときの判断として合理的だったはずだ。既存の建物に手を加えず、最小限の変更で部屋を足していった。しかし積み重なった結果として、誰も意図していなかった迷路ができあがる。
コードベースも同じだ。「既存のコードになるべく手を加えない」という一見謙虚なポリシーが、長期的には全体の複雑性を増大させる。
このポリシーはOSS開発の文脈では正しい。コントリビューターは対象のコードベース全体への理解が限られているし、変更の影響範囲を把握しきれないから、余計なところに手を出さないのは妥当な判断だ。問題はそのやり方を仕事にもそのまま持ち込んでしまうことにある。
自分たちのコードベースで日々開発している我々はコントリビューターではなく、コミッターとして振る舞わなければならない。場合によっては全体に波及するような変更を躊躇してはならないし、それができる立場にいる。変更を加えるとき、同じパターンがコードベース全体に存在しないか確認する。できるなら全体に適用する。新しく追加したコードが、まるで元からそこにあったかのように周囲と調和している状態が理想だ。
実践:いらなくなったコードをその場で消す
なにはなくともこれだけは絶対にやろう。
コードがいらなくなった瞬間を逃すとあっという間に既存のコードに紛れてしまい、後からそのコードが使われていないことを確かめるのはしばしばとても困難になる。使われていないコードは壊れることがないので直されることもなく、現役のコードとどんどん乖離していく。やがてシステム内に矛盾する複数のコードが存在することになり、何か変更を加えようとするたびどれが正なのか都度調査する羽目になる。
コードをばっさり消すのは不安だという心情も理解できる。しかし残すことは単なるリスクの後回しというだけではなく、将来的に何倍にもなって跳ね返ってくる地雷を埋める行為だ。今潰せるリスクは自分で責任を持って潰そう。もちろん作業見積もりにはその分の時間を加味するべきだ。
どうしても消せないなら、せめて消せない理由をコメントとして残そう。TODO だけでは不十分で、どうやったら消せるようになるのかがわからないといけない。「Rails 8 へのアップデートで不要になったはずだがちゃんと動作確認できていないので残しておく」と書いてあれば、そのコードを消した状態で動作が変わらないことを確認すれば消せるとわかる。
コードをコメントアウトして残すという手もある。消さないでおくよりは遥かにマシだ。少なくともそのコードが現役ではないことが一目でわかる。ただ、コメントアウトできるということはそのコードがなくても動くことは明白なのだから、特段の理由がない限り消したほうがよい。
実践:コードを書かない判断をする
いらなくなったコードを消すことと並んで重要なのが、そもそもコードを増やさない判断だ。
まず仕組みを早まって作らない。「将来こうなるかもしれない」という予測のもとに作った抽象化やフレームワークもどきは、その予測が外れた瞬間に負債になる。本当に必要になったときに作れば十分で、そのときには何が必要かもずっとはっきりしている。
仕様をコードに合わせて提案することも有効だ。ビジネス上本質的でない箇所、例えばエラーメッセージの文言や細かい表示の仕様は、実装上都合のよい形に積極的に誘導してよい。顧客やプロデューサーに任せると実装コストを度外視した仕様になりがちだが、作る側が提案すればその分のコードを書かずに済む。どう実装されれば都合がよいかを知っているのは自分たちだけだ。
そしてマイクロサービスのような大仰な仕組みを安易に採用しない。シンプルな問題をシンプルに解くことができているなら、それは十分に優れた設計だ。損益分岐点を超えていない複雑さを持ち込んでも、維持するコストだけが増える。
まとめ
コードベースを小さく保つことは、地味で目立たない。新しいアーキテクチャを導入することでも、テストカバレッジを上げることでも、型システムを整えることでもない。使われていないコードを消し、余分な仕組みを作らず、変更を加えるたびに全体が少しずつ整っていく状態を維持することだ。
それでもこれを最優先の指針として選ぶ理由がある。多少設計がまずくてもコードが少なければ対処できる。逆にどれだけ設計が美しくてもコードが膨れ上がれば身動きが取れなくなる。コードの少なさはあらゆる問題への対処能力を底上げする、いわば基礎体力だ。他にやるべきことが山積みでも、これだけは手を抜かないでほしい。
2026-03-08
Gentoo Linux はインストールや運用のハードルが高いディストリビューションとして有名ですが、近年ではビルド済みカーネル や amd64/arm64 向けバイナリパッケージ が提供されて格段に楽になっています。これにより非力な VPS で Gentoo を運用することも現実的になりました(このブログもメモリ 1GB の Gentoo VM でホストしています)。
実録: Gentoo のインストール
実際に Gentoo のインストールがどれほど楽になっているか見てみましょう。GNOME Boxes で適当な VM を作成して、Gentoo source mirrors からダウンロードした Minimal installation CD を使ってブートします。
パーティションを作成します。今回作成した VM は legacy BIOS システムのため、BIOS boot partition を 2MB 取って残りを rootfs にします。
gdisk /dev/vda
ファイルシステムを作成してマウントします。
mkfs.xfs -L root /dev/vda2
mount /dev/vda2 /mnt/gentoo
スワップファイルを作成して有効にしておきます。
cd /mnt/gentoo
fallocate -l 2GiB swapfile
chmod 600 swapfile
mkswap swapfile
swapon swapfile
時計を合わせましょう。
chronyd -q
Gentoo source mirrors から stage3-amd64-systemd-*.tar.xz をダウンロードして展開します。
tar xpvf stage3-amd64-systemd-*.tar.xz --xattrs-include='*.*' --numeric-owner
rm stage3-amd64-systemd-*.tar.xz
/mnt/gentoo/etc/portage/make.conf を編集してバイナリパッケージを利用するための設定をします。binpkg-request-signature はパッケージの署名を検証するオプションです。
FEATURES="getbinpkg binpkg-request-signature"
/mnt/gentoo/etc/portage/binrepos.conf/gentoobinhost.conf を編集して、より最適化されたバイナリを使うようにします (Gentoo x86-64-v3 binary packages available – Gentoo Linux )。
- sync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/17.1/x86-64
+ sync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/17.1/x86-64-v3
/etc/resolv.conf をコピーして Gentoo 環境に chroot します。
cp -L /etc/resolv.conf /mnt/gentoo/etc/
arch-chroot /mnt/gentoo
Portage tree のスナップショットを取得します。ついでにニュースを流し読みしておきましょう。
emerge-webrsync
eselect news read new
全パッケージを更新しておきましょう。ほとんどのパッケージはバイナリインストールになるので大した時間は掛かりません。
emerge -avuDN @world
タイムゾーンを設定します。
ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
date
ロケールを設定します。
echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen
locale-gen
eselect locale set en_US.utf8
. /etc/profile
ファームウェアをインストールします。ファームウェアにはオープンソース互換ライセンスでないものも含まれているので、/etc/portage/make.conf で受け入れ可能なライセンスを設定します。* は任意のライセンスを受け入れるの意です。
ACCEPT_LICENSE="*"
続いて linux-firmware パッケージをインストールします。
emerge -av linux-firmware
カーネルのインストールに移ります。USE フラグの設定を簡単にするため、flaggie をインストールしましょう。
emerge -av flaggie
今回はブートローダとして grub を使うため、適切な USE フラグを設定して installkernel をインストールします。
flaggie installkernel +grub
emerge -av installkernel
カーネルをインストールします。
emerge -av gentoo-kernel-bin
USE フラグの変更が必要だと言われるので dispatch-conf しましょう。u で新しい設定を適用します。
dispatch-conf
あらためてカーネルをインストールします。
emerge -av gentoo-kernel-bin
既存の gentoo-sources はカーネルソースだけをインストールして後のコンフィグとビルドは自分で頑張らないといけなかったのですが、gentoo-kernel-bin はカーネルイメージそのものをインストールします。つまり、これだけでカーネルのインストールはおしまいです。
この新しい方式のカーネルパッケージを Distribution Kernel と呼びます。Distribution Kernel には現状 vanilla-kernel(Gentoo のパッチセットが当たっていない素のカーネル、ソースコードからビルド)、gentoo-kernel(パッチセットを適用したカーネル、ソースコードからビルド)、gentoo-kernel-bin(パッチセットを適用したカーネル、バイナリインストール)の3種類があります。gentoo-kernel-bin 以外の2つはインストール前にコンフィグをカスタマイズすることもできます。プリセットのコンフィグは Fedora のものが使われているそうです。
Distribution Kernel 環境用の USE フラグがあるので設定しておきましょう。これによってカーネル更新時にカーネルモジュールが自動でリビルドされるようになります。
flaggie +dist-kernel
USE フラグを変更したので影響のあるパッケージをリビルドしましょう。
emerge -avuDN @world
/etc/fstab を設定します。
LABEL=root / xfs defaults,noatime 0 1
/swapfile none swap sw 0 0
ルートパスワードを設定して作業用アカウントを作成します。
passwd
useradd -m -G wheel,portage ursm
passwd ursm
sudo をインストールして設定します。
emerge -av sudo
visudo
wheel グループに対して sudo を許可するようにしましょう。NOPASSWD: ALL はお好みで。
%wheel ALL=(ALL:ALL) NOPASSWD: ALL
Systemd の初期設定を行います。
systemd-machine-id-setup
systemd-firstboot --prompt
ブートローダをインストールします。
grub-install /dev/vda
systemd-networkd の設定ファイル /etc/systemd/network/50-dhcp.network を作成します。
[Match]
Name=en*
[Network]
DHCP=yes
chroot 環境を抜けて reboot します。
^D
reboot
作業用アカウントでログインして、ネットワークを使える状態にします。
sudo systemctl enable --now systemd-networkd
これでインストールは完了です。ね、簡単でしょう?
「ビルドせずして何が Gentoo か」という声もあろうかと思うのですが、別に Gentoo はバイナリベースのディストリビューションになろうとしているわけではありません。基本はビルドで、オプションとしてバイナリパッケージという選択肢が提供されただけです。冒頭に挙げた非力な VPS のように、これまで Gentoo の運用が難しかった環境への導入の道が開けますし、初回インストールはバイナリインストールで手早くセットアップして後からシステム全体をビルドし直すようなやり方も取れるようになります。まさに "Choice is another Gentoo design principle" を体現する動きと言えるのではないでしょうか。
2025-04-20
最近の Rails では rails server 以外に複数のプロセスを起動して開発を行うスタイルが一般的になっています。jsbundling-rails や cssbundling-rails, dartsass-rails などですね。この場合 bin/dev を叩くと foreman でまとめてプロセスを起動してくれるのですが、ここで困るのが foreman だと binding.irb や debugger がまともに使えないことです。
この問題を解決するために zellij-foreman というものを作りました。これはターミナルマルチプレクサ Zellij のプラグインで、Procfile を読み込んでプロセスを起動する機能を提供します。起動したプロセスはそれぞれ独立したペインに割り当てられるため、debugger も問題なく使えます。
使い方は Rails.root で zellij を起動した状態で以下のコマンドを実行するだけです。終了は Ctrl+q です。
$ zellij plugin --configuration procfile=Procfile.dev -- https://github.com/ursm/zellij-foreman/releases/download/v0.1.2/zellij-foreman.wasm
是非お試しください。
2025-04-19
OmniAuth .config.request_validation_phase = nil
2025-04-10
ユーザの入力に応じて動的に表示を切り替えたい場面はよくあります。例えば「その他」を選択したときだけ自由入力欄を表示したい、とかですね。これ簡単そうに見えて意外と難しいというか、考えることが多いのです。動的にということで Stimulus を使うまではよいとしても、コントローラをどの粒度で作るのか、処理をクライアントサイドで閉じるのか Turbo Frame などと組み合わせるかなど常日頃悩ましく思っていました。色々試してみて、こういう状況で使えるシンプルで汎用的なテクニックを見つけたのでご紹介します。
まずこんな感じの Stimulus コントローラを作ります。
// app/javascript/controllers/reform_controller.js
import { Controller } from ' @hotwired/stimulus ';
export default class ReformController extends Controller {
static values = {
visitOptions: Object
} ;
visit () {
const data = new FormData ( this .element);
const url = new URL ( location .href);
url.search = new URLSearchParams ( data ). toString ();
Turbo . visit ( url . toString (), this .visitOptionsValue);
}
}
このコントローラは form 要素に挿して使います。
<%# app/views/wishes/new.html.erb %>
<%= form_with(
model: @ wish ,
data: {
controller: ' reform ',
reform_visit_options_value: { frame: ' additional_input ' }.to_json
}
) do | f | %>
<%= f. select :kind , [
[' 金 ', ' money ']
[' 力 ', ' power '],
[' 名声 ', ' fame '],
[' その他 ', ' other ']
], **{
data: { action: ' reform#visit ' }
} %>
<%= turbo_frame_tag ' additional_input ' do %>
<%= if @ wish .kind == ' other ' %>
<%= f.text_field :other_text %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
Rails 側のコントローラはこんな感じです。
# app/controllers/wishes_controller.rb
class WishesController < ApplicationController
def new
@ wish = Wish . new (wish_params)
end
private
def wish_params
params.fetch( :wish , {}).permit( :kind , :other_text )
end
end
これですべてです。どういう風に動くかを見てみましょう。まず、ビューの select を選択したときに reform#visit が呼ばれます。
<%= f. select :kind , [
[' 金 ', ' money ']
[' 力 ', ' power '],
[' 名声 ', ' fame '],
[' その他 ', ' other ']
], **{
data: { action: ' reform#visit ' }
} %>
visit は現在の form の内容をクエリ文字列にシリアライズして、今いるページの URL にくっ付けて Turbo.visit() で送信します。今回 turbo_visit_options_value は { frame: 'additional_input' } なので、返ってきた HTML は additional_input という名前の Turbo Frame に挿入されます。
visit () {
const data = new FormData ( this .element);
const url = new URL ( location .href);
url.search = new URLSearchParams ( data ). toString ();
Turbo . visit ( url . toString (), this .visitOptionsValue);
}
<%= form_with(
model: @ wish ,
data: {
controller: ' reform ',
reform_visit_options_value: { frame: ' additional_input ' }.to_json
}
) do | f | %>
「今いるページ」というのはつまり wishes#new です。ここで wish_params を使ってモデルを初期化することで、ユーザの入力内容を反映した状態でフォームが描画されます。つまり、現在の入力内容を元に wishes#new を丸ごと再描画してその一部を Turbo Frame にはめ込んでいるわけです。
def new
@ wish = Wish . new (wish_params)
end
<%= turbo_frame_tag ' additional_input ' do %>
<%= if @ wish .kind == ' other ' %>
<%= f.text_field :other_text %>
<% end %>
<% end %>
注意点として、wishes#new を直接表示したときは params が空っぽなので params.require や params.expect だとエラーになります。params.fetch で params が空のときのケアをしてやる必要があります。
def wish_params
params.fetch( :wish , {}).permit( :kind , :other_text )
end
別の例を見てみましょう。一覧の各行にチェックボックスがあって、チェックを付けると一括削除ボタンが active になるような画面を考えます。まず先の reform controller に少し手を入れて、form 要素を target でも指定できるようにします。
// app/javascript/controllers/reform_controller.js
import { Controller } from ' @hotwired/stimulus ';
export default class ReformController extends Controller {
static values = {
visitOptions: Object
} ;
static targets = [
' form '
];
visit () {
const form = this .hasFormTarget ? this .formTarget : this .element;
const data = new FormData ( form );
const url = new URL ( location .href);
url.search = new URLSearchParams ( data ). toString ();
Turbo . visit ( url . toString (), this .visitOptionsValue);
}
}
これで form 要素と入力項目が離れていても使えるようになりました。これを使って一覧画面を作ります。
<%# app/views/wishes/index.html.erb %>
< div
data-controller =" reform "
data-reform-visit-options-value =" <%= { frame: ' bulk_destroy ' }.to_json %> "
>
<%= form_with(
url: bulk_destroy_wishes_path,
method: :post ,
id: ' bulk_destroy ',
data: { reform_target: ' form ' }
) do | f | %>
<%= turbo_frame_tag :bulk_destroy do %>
<%= f.submit ' 一括削除 ', disabled: params[ :ids ].blank? %>
<% end %>
<% end %>
< table >
<% @ wishes .each do | wish | %>
< tr >
< td >
<%= check_box_tag ' ids[] ', wish.id, **{
form: ' bulk_destroy ',
data: { action: ' reform#visit ' }
} %> </ td >
</ td >
< td > <%= wish.kind %> </ td >
</ tr >
<% end %>
</ table >
</ div >
チェックボックスをトグルすると reform#visit が呼ばれ、form の内容を元に今の画面を再描画し、最終的に Turbo Frame が更新されて全部辻褄が合う…という一連の流れがおわかりいただけるでしょうか。画面ごとに固有の JavaScript を書く必要もなく、すべてのロジックがサーバサイドに集約されています。
リクエストを GET で送る仕組みの制約として、あまりに巨大なデータやファイルのアップロードは扱えません。そのような場合は万葉の大場さんが紹介されている「Ghost Form パターン 」が参考になるかもしれません。
皆さまの快適な Hotwire ライフの一助となれば幸いです。
2025-01-20