こんな感じにフォーカスが当たるとOS標準の枠が出て、矢印キーの左右で選択状態を切り替えられる<input type="radio" />
要素の作り方。
大まかには<label><input type="radio" /></label>
の構造にして、UI上はラベルを疑似要素的に表示し、ラジオボタン本体はUI上見えなくするが、display: none;
にせず、画面に残すことによってタブ遷移できるフォーカス可能要素として設計する内容。
サンプルコード
CSS
/* ラジオボタンを等間隔に整列し、左右に余計な空白を持たせない */
.radio-container {
display: flex;
column-gap: 0.25rem;
}
.hidden-radio {
/* チェックを消す */
appearance: none;
/* 非推奨要素を使っているが、iOS Safari対策 */
position: absolute;
clip: rect(0, 0, 0, 0);
pointer-events: none;
/* デフォルトマージンがチェックボックスの位置にあるので消す */
margin: 0;
}
.radio-label {
/* 文字列要素を綺麗に中央寄せする */
display: flex;
justify-content: center;
align-items: center;
/* 枠装飾、基本的にフォーカス枠が角丸であり、違和感がないようにborderも角丸にしておく */
padding: 5px;
border: 1px solid #60bce9;
border-radius: 4px;
/* 見かけ上ボタンなのでカーソルをボタン用にする */
cursor: pointer;
}
/* ラジオチェック時にラベルの背景色を変化させる */
.radio-label:has(.hidden-radio:checked) {
background-color: #60bce9;
}
/* ラジオフォーカス時にラベルのフォーカス枠を出す */
.radio-label:has(.hidden-radio:checked):focus-within {
outline: auto;
}
HTML
<form>
<div class="radio-container">
<label class="radio-label">
<input name="r2" type="radio" class="hidden-radio" value="1" checked />
<span>道明寺</span>
</label>
<label class="radio-label">
<input name="r2" type="radio" class="hidden-radio" value="2" />
<span>長命寺</span>
</label>
</div>
</form>
Windowsに入っている古くて使いづらいPowerShellをアップグレードするところから始める。
- Powershell 7.xを入れる
- PowerShellを管理者権限で起動
Set-ExecutionPolicy RemoteSigned
を流す- hoge.ps1などの名前で適当にファイルを作り、
C:\Program Files\PowerShell\7\pwsh.exe
に関連付けする
自動生成されたHTMLから社員情報を引っこ抜くスクリプトを作った時に考えたことを書き出してみる。ブラウザのDevToolsを使った簡易ツールを書いていて、頭に浮かんだことだ。データやコードは架空の内容なので実在しない。
ベースのHTML
大まかに最初に目に入ってきたのはこんな内容だった。機械生成されているせいかフォーマットがガタガタ気味だ。
<span class="Content">
<span id="undefined">SHAIN.XLS page1</span>
</span>
<span class="Content">
<br>
<span id="undefined">社員リスト</span>
<span id="undefined"></span>
</span>
<span class="Content">
<br>
<span id="hoge1">00001</span>
<span id="fuga1">:山菱喜一</span>
</span>
<span class="Content">
<br>
<span id="hoge2">00002</span>
<span id="fuga2">:鈴木与一</span>
</span>
<span class="Content">
<br>
<span id="hoge3">00011</span>
<span id="fuga3">:茨城妙子</span>
</span>
<span class="Content">
<span id="undefined">SHAIN.XLS</span>
<span id="undefined"> page2</span>
</span>
<span class="Content">
<br>
<span id="hoge4">00012</span>
<span id="fuga4">:奈良楓</span>
</span>
...
まずは関数ベースで考える
ひとまず普段は関数ベースで実装しているので関数ベースで実装する。書いていてgetEmployee(name, list)
の仮引数にあるlist
は冗長ではないかということに気づく。
const emp_list = document.getElementsByClassName('Content');
[...emp_list].reduce((acc, cur) => {
if (cur.id === 'undefined' || cur.children.length !== 3) {
return acc;
} else {
const id = cur.children[1].innerText;
const name = cur.children[2].innerText.replace(/^:/, '');
if (/^\d+$/.test(id)) {
acc.push({ id, name });
}
return acc;
}
}, []);
const getEmployee = (name, list) => {
const re = new RegExp(`.*${name}.*`);
const result = list.filter((emp) => re.test(emp));
console.log(result);
};
クラスにしてみる
そしてクラスにしたほうが素直にならないかと考え、クラスにしてみた。getEmployee(name, list)
はsearch(name)
となり、冗長性が減ったほか、抽象的な表現で分かりやすくなった気がする。
class EmployeeSearch {
constructor() {
const emp_list = document.getElementsByClassName('Content');
this.list = [...emp_list].reduce((acc, cur) => {
if (cur.id === 'undefined' || cur.children.length !== 3) {
return acc;
} else {
const id = cur.children[1].innerText;
const name = cur.children[2].innerText.replace(/^:/, '');
if (/^\d+$/.test(id)) {
acc.push({ id, name });
}
return acc;
}
}, []);
}
search(name) {
const re = new RegExp(`.*${name}.*`);
const result = this.list.filter((emp) => re.test(emp.name));
console.log(result);
}
}
追加仕様に合わせ関数を新設し、クラスを改良する
動作確認をしていく中で表示と名前が分かれたパターンを発見したので、これに合わせて直してゆく。
<span class="Content">
<br>
<span id="hoge2">00002</span>
<span id="fuga2">:飯田</span>
<span id="piyo2">太郎</span>
</span>
この対応をしようとすると分岐が必要だと考えたが、愚直にやってしまうとコンストラクタの中がごちゃつく上、テストもしづらいと考え、DOMをパースする処理を外部関数に切り出し、それを呼ぶことにした。
const getEmployeeFromList = (domList) => {
const id = domList[1].innerText;
const name1 = domList[2].innerText.replace(/^:/, '');
if (domList.length === 3) {
return {
id,
name: name1
}
} else {
const name2 = domList[3].innerText;
return {
id,
name: `${name1}${name2}`
}
}
};
class EmployeeSearch {
constructor() {
const emp_list = document.getElementsByClassName('Content');
this.list = [...emp_list].reduce((acc, cur) => {
if (cur.id === 'undefined' || cur.children.length < 3) {
return acc;
} else {
const { id, name } = getEmployeeFromList(cur.children);
if (/^\d+$/.test(id)) {
acc.push({ id, name });
}
return acc;
}
}, []);
}
search(name) {
const re = new RegExp(`.*${name}.*`);
const result = this.list.filter((emp) => re.test(emp.name));
console.log(result);
}
}
更により良くするには?
ブラウザのDevToolsにあるConsoleで書いたコードなので、現実問題テストコードも書かないし、そこまで凝る必要もないのが、よりよい設計にするにはコンストラクタでパースせず、事前にパース処理した結果をコンストラクタを通じて注入して処理するようにするとよいだろう。そうすると責務が分離され、テストがしやすく、コードもきれいになると考える。
クラスにするか、関数にするか
これはまだ正直答えが見えていないが、インスタンスを引き回す場合はクラスのほうが都合がいい可能性がある気がしてきている。
関数でもできるにはできるが、毎回引数にインスタンス相当のものを渡すのは冗長である可能性がある。疎結合という文脈では関数のほうがより良い可能性もあるが、人間の認知負荷や、保守性との兼ね合いだと思う。
明確な損益分岐点はまだ見えていないし、今回のケースだと割とどっちでもいい部分もあるとは思う。
InterfaceDescriptionの取り方(Get-NetAdapterだけでは見切れる)
Get-NetAdapter | Format-List -Property Name,InterfaceDescription
Shell Scriptとは思想が違うためif ('Up' -eq ($adaptor | Format-List -Property status))
では上手くいかない
while ($true) {
$adaptor=Get-NetAdapter -InterfaceDescription "Intel(R) Ethernet Connection XXX" -ErrorAction SilentlyContinue
if ($adaptor -eq $null) {
Write-Host 'イーサネット3のStatus: UPを待機しています...'
Start-Sleep -Seconds 5
} else if ('Up' -eq $adaptor.status) {
Write-Host 'イーサネット3の疎通が確認できたため、Hogeを起動します...'
Start-Process -FilePath 'C:/path/to/Hoge.exe'
break
} else {
Write-Host 'イーサネット3のStatus: UPを待機しています...'
Start-Sleep -Seconds 5
}
}
例外設計についての個人的に思っていることを書き出してみる。整理できていないがいったん現状。TypeScriptのケースを意識して考えているが、根幹は例外スローのある言語ではどれも共通だと考えている。
例外とは何か?
本記事で扱う例外とはthrow new Error("ほげほげ");
のように、いわゆるスローされ、try-catchされるものを指して扱う。
例外を多用しない
例外は多用しないことが望ましく、原則として処理が続行不能になる場合を除き、使わないほうが良いと考えている。これはスローされた例外は可視化されづらく、適切にハンドリングされないケースがよくあるからだ。
例外は構造化されたプログラムを破壊する
構造されたプログラムとは順次・反復・分岐の基本構造を階層的に組み合わせたものであり、いわゆる上から下に読めばわかるもの、構造化プログラミングによって作られるものだ。上から下に流れるため非常にわかりやすい。
これに対して存在するのがgoto
を用いたプログラムだ。goto
を使うとコードの色んな場所に前後しながらジャンプすることが可能で、可読性を損ねてしまう。
そして例外は基本的にgoto
のような存在である。例外が起きると例外オブジェクトが投げられ、これはどこに行きつくか予想することが難しい。行きつく先がなければ最悪プログラムがクラッシュする恐れすらある。
例外は処理コストが重い
恐らく大抵の言語において例外をcatchする行為はコストが重い。これはJavaでは特に有名な話だと思うが、Java以外でもそうだと思う。例えばNode.jsでtry-catchで例外を処理するプログラムと、if-elseで処理するプログラムを書き、その実行速度を比較するとif-elseの方が早い。
以下はエラーハンドリングをifで行うプログラムと、try-catchで行うプログラムを作り、100万回走行させたときの処理時間だ。catchに入るケースでは処理速度が低下することが分かる。例外が投げられず、tryの中に納まる限りは低下しない。
処理 | 処理時間(ms) |
---|---|
If正常系 | 4,453 |
If異常系 | 4,204 |
try-catch正常系 | 4,341 |
try-catch異常系 | 7,883 |
これは例外が投げられる場合、最寄りのcatchポイントを探索するのに時間がかかるからではないかと考えている。
Node.js向け検証コード
前述の処理時間を出すのに使ったプログラム
// ==== 実行用共通部品
const execIf = (input) => {
if (input === 1) {
return true;
} else {
return false;
}
};
const execThrow = (input) => {
if (input === 1) {
return true;
} else {
throw new Error('ERR');
}
};
// ==== コールバック処理計測用関数
const getExecFuncElapsed = (cb) => {
const startAt = +new Date();
for (let i = 0; i < 1_000_000; i++) {
cb();
}
return +new Date() - startAt;
};
// ==== If/try-catch検証用、コールバック処理群
const execIfOk = () => {
const ret = execIf(1);
if (ret) {
console.log('OK=');
}
};
const execIfNg = () => {
const ret = execIf(0);
if (ret === false) {
console.log('ERR');
}
};
const execThrowOk = () => {
try {
execThrow(1);
console.log('OK=');
} catch (e) {
console.log('ERR');
}
};
const execThrowNg = () => {
try {
execThrow(0);
console.log('OK=');
} catch (e) {
console.log('ERR');
}
};
// ==== 計測処理
const elapsedIfOk = getExecFuncElapsed(execIfOk);
const elapsedIfNg = getExecFuncElapsed(execIfNg);
const elapsedThrowOk = getExecFuncElapsed(execThrowOk);
const elapsedThrowNg = getExecFuncElapsed(execThrowNg);
// ==== 結果出力
console.table({ elapsedIfOk, elapsedIfNg, elapsedThrowOk, elapsedThrowNg });
いつ例外を使うか?
基本的には大域脱出がしたいケースのみで使うべきだろう。最悪ハンドリングされずとも、基底階層でcatchされれば、それでよいケースで使うのが最も無難だと考える。
大域脱出とは要するにgoto
だ。多重ネストや深い高階関数から浅い階層に一気にすり抜けるときに有効だろう。逆に一階層とか、抜ける階層が浅いレベルでは使わないほうが良い。
例えば処理が続行不能になったケースでは例外をスローし、基底階層でcatchし、エラーログを吐く、クライアントに対してエラーメッセージを返すなどの処理があればよいと考える。
但し例外を使うときは極力カスタム例外を使ったほうがよいと考える。
フレームワークやライブラリの例外をどう扱うか?
例えばバリデーションエラーやHttpClient系の4xx, 5xx系の例外スローはラッパーを作り、例外をエラーオブジェクトに変換するのも一つだと考える。
HTTP GETを行うクライアント関数であれば以下のようなラッパーを作り、正常時は正常レスポンスを返し、異常時でかつ想定内であればエラーオブジェクトを返す、そして想定外の例外であればリスローする、といった処理をすることができる。
const httpGet = (url) => {
try {
return fetch(url);
} catch (e) {
if (e instanceof AbortError) {
return createHttpErrorObj(e);
} else {
throw e;
}
}
}
こうすることで呼び出し元は正常時であればHTTPリクエストをそのままステータスコードに応じて処理し、Abortされた場合はリトライ、完全に予期せぬ内容であれば例外を基底階層まで飛ばして処理を中断するといったこともできる。
リトライに規定回数失敗した場合は、この関数の呼び元でnew OutOfTimesHttpRetryError()
みたいな例外をスローするとよいだろう。
カスタム例外を使う
カスタム例外とは例外クラスを継承した例外クラスだ。
例えばC#ではExceptionのほかに、それを継承したSystemExceptionや、IndexOutOfRangeExceptionなど、様々なカスタム例外が存在する。
LaravelにもAuthorizationExceptionをはじめとし、多様なExceptionが存在する。
これらに限らず、大抵の言語やフレームワークには相応のカスタム例外が用意されているのが一般的で、業務システムやC2のプロダクトでも、例外をスローするケースではカスタム例外を作ることが望ましいと考える。
カスタム例外があると例外種別ごとにフィルタリングすることが可能になり、柔軟にハンドリングしやすく、ロギングの際にも例外種別を記録することで、後々の障害調査でも便利になるため有用だ。
例外を握りつぶさない
例外は時として握りつぶされることがある。そういう必要があるケースも少なからずあるだろう。しかし基本的に例外は握りつぶさないほうがよい。
例外は望ましくない現象が起きているはずで、本質的にハンドリングする必要がある。単に握りつぶしているだけでコメントやテストも書かれていなければ、もし例外が起きたとき、処理が正常に進むのかどうかコードから読み取ることが困難だ。
仮にテストがあったとしてもコードを読むときのコストが増えるので、基本的に握りつぶさないほうがよいだろう。
スローする場合は、例外クラスを用いる
JavaScript系では以下のようなエラーオブジェクトを作るケースもあると思うが、スローする場合は使わないほうが良い。
{
__type: 'HogeError',
message: 'fufagfuga',
payload: someObject
}
スローする場合はErrorクラスを継承したカスタムエラーをスローすべきだ。これはカスタムエラーにはスタックトレースなど、障害調査をするときなどに例外として標準的な情報が格納されているほか、instanceof
でフィルタしやすい、クラスは型情報を持つのでリファクタが容易、error.__type === 'HogeError'
は言語機能の再実装に近く標準的ではないからだ。テストフレームワークでも例外をキャッチするためのアサーションではErrorクラスの継承が必須となっているケースがあるため、TypeScriptでは型検査が上手く通らないケースがある。