るいもの戯れ言
#1218
2022/08/22 04:27

Clean Craftsmanshipをいただきました。いつものボブおじさんのClean XXXシリーズ。

ボブおじさんの書籍シリーズの良いところは、自分は以下のように見ている。

  • 前提知識が少なくても出来る限り理解できるように練られている。
  • 高尚な話ではなく、現場叩き上げの話が中心。
  • なんとなく、これおかしいよなぁと薄々感じつつも、現場の空気で言えないようなことを、ズバっと言ってくれる(Clean Codeでの「コメントは害悪」など)。

今回も個人的に面白かった部分を取り上げたい。

第2章

この章は基本的にTDDの実践をサンプルを使って詳しく記載している。基本的な部分から解説されているので、TDDについては初めてという初心者の人でも全く問題なく理解できるだろう。前提知識不要というのは素晴しい。

「デバッガーが得意になることを目指すべきではない」

テストコードを数行書き、エラーやテスト失敗をてがかりに本番コードを書く。というやり方を繰り返す中ではデバッガーの出番はほとんど無い。

そういえば会社に入った頃(30年くらい前)は、会社での開発のやり方がかなり違うことに驚いた記憶がある。ある先輩はひたすらコードを仕様書に従って組み上げた後、巨大なプログラムを動かし始めて、なかなか動かずに難解なバグに頭を悩ませていたのだ。その後、研修で「コーディング」と「テスト」は局面が別れているので、先輩のやり方が会社としては正しいのだと知った。

自分がそれまでにやっていた趣味のプログラミングでは、ちょっとだけコードを実装してみて、動かしてみて確認。Okならまた先に少しだけ前進。というのを繰り返していた。なにせ当時はC/C++なので慎重に進んでいかないと、メモリを破壊するデバッグ困難なバグで長時間足をとられてしまう。少しずつ進んでいかないと、根本的な設計ミスに気付いた時に大手術となってしまうので、自然とそういうやり方をとっていた。

TDDの普及によってようやくコーディングとテストは不可分なものになったわけだ。

第3章

TDD応用編だ。テストの時に使う「ニセモノ」。テストダブル。これにはフェイク、ダミー、スタブ、スパイ、モックがあるという解説。前はモックとスタブくらいしか無かった気がするけど、今はそんなにあるのね。

スパイを使ったテストは壊れやすい

sin()の計算をテイラー級数で求める時のテストをどうするか。sin()の引数は無限にあるので何をもって充足していると言えるか。そこで、テイラー級数の各項を教えてくれるクラスを抽出して、それをスパイに置き換えて様々な条件を与えてやれば、テイラー級数の計算ロジックが正しいことを検証できる。しかし計算方法をCORDICに変えると全てが無駄になってしまう。それがスパイの限界だ。テストの壊れにくさを追求すには、なるべく本番コードとの結合を弱める必要があるが、そうするとより詳細にテストを行うことが困難になる。これはトレードオフなのだ。

ここでふと思ったのだけど、このあたりのテストダブルを自分は最近全然使っていないなと。全く使っていないわけではないが非常に限られたケースでしか使っていないように思う。なぜなのか考えてみたが、まずE2Eテストの方が主でUnitテストはE2Eでカバーできないところだけ補足的やる感じのスタイルになったので、こうした仕組みの重要性が薄れた。E2Eテストなら、内部の実装を変えても外部からみた振舞いが変わっていなければテストは壊れない。

ScalaやRustを使う機会が増えてきて、なんでもかんでもOOで書くというのがなくなったので、テストダブルがあまりうまくはまらないというのもあるかもしれない。基本は関数を渡すように設計するので、テスト用の関数をテストの時に渡すやり方でテストしている。

本書の方針はコンポーネントの境界を越えないならテストダブルを使っても良い。そうでなければなるべく使わないだ。これまで感じていた「なんかテストダブルって分かりにくいし嫌だな」という感覚が明快に言語化されていて良い。

第4章

「GUIをテストするな」

これはSeneniumのようなツールによるテストを否定しているのではなく「GUIでレンダリングされた見た目が正しいことをテストするな」ということだ。見た目は、頻繁に変わるのであっという間にテストが壊れて作り直しの繰り返しになる。まぁこれは昔から言われているやつ。

GUIの領域は設計によってかなり小さくできるので、それを目指せば大きな問題ならないとのこと。

ただ典型的なWebアプリはともかく、インタラクティブ性の高いGUIアプリはそうもいかないのではないかなぁ。自分はドロー系のアプリを作るのが好きで趣味で良く作ったりするが、例えばオブジェクトを複数選択してドラッグして移動するとか、ドラッグ中に画面の端までいったら自動的にスクロールするとか、そういう部分はGUIと切り離してテストするのは困難だし、良いテストツールも無いのが現状だと思う。

「XXXクラスにXXXTestクラスを用意するというやり方をやめる」

TDDの初期の頃の解説にあったXXXクラスにXXXTestクラスを用意するやり方は間違いだったと認めている。これはテストと本番コードとの結合度を高め、構造の改善をしようとすると大量のテストが失敗してしまう。本書ではレンタルビデオの例を使い、あるビジネスケースを実行するテストを紹介する。1ビジネスケースはCustomerとかRentalやVideoRegistryなど複数の協調で実現されるが、これらのクラスを個別にテストするのではなく「ビデオxxxを顧客yyyがレンタルする」というビジネスケースでテストをする。

今はWebアプリもSPAで、サーバサイドはAPIサーバになっているので、このAPI単位でテストを書くのが妥当なのかもしれない。APIは一旦公開したら勝手に変更できないから、そこにテストが依存しても将来壊れるリスクは小さいだろう。自分が数年前からE2Eテスト重視に転向したのもこれが理由だ。

git reset --hardは友達

リファクタリングを始めてみて行き詰まったらすっぱとあきらめて元に戻そう。

まったくその通りだ(笑)。

第6章

YAGNI

フックは悪との解説。

自分が会社に入った当時はUser Exitと呼ばれていた。この仕組み、当時はなるほどと思ったものの、これをうまく活用するのは想像以上に難しい。User Exitに書いたものが本体コードにどういう影響を与えるのか分からないので、試行錯誤でうまい具合に動く「局所解」を探す旅になるのだ。何より当時は本体側のソースコードが開示されていないので文字通り手探りだった。本体コードを開発する側も、ユーザ側がどんなUser Exitを書くか事前に全てを想像できるわけではないので無理筋な仕組みだろう。でも今も「プラグイン」というのは残っているよね... とも思うが。

第7章以降

基本的に読み物として楽しめば良いように思う。ボブおじさんの経験談にもとづいた教訓の膨大な集積場だ。

コメントを書き込む
#1187
2022/06/05 05:21

手が凄く太いので大きくなりそう。

コメントを書き込む

ソースコードを変更すると、cargo runで毎回ライブラリのビルドが走ってしまうようになった。起きる環境はLinuxで、同じソースコードをMac(Intel)に持っていっても再現しない。 色々調べていると、ソースコードを変更すると自動的に裏でrustcが走っていることがpsで確認できた。

どうやらRustのLanguage ServerがVS Codeのpluginであるrust-analyzerで自動インストールされ、これが裏で自動的にソースコードの変更を検出してビルドを実行するようだ。Emacs用のlsp関係のパッケージを入れていても同じ事が起きる。

雑にググると、.cargo/config の中にtargetディレクトリを指定しろというのが出てくるが、これはvs codeでもcargoの直接起動でも効くので意味がない。そりゃそうだ。vs codeの時だけtargetディレクトリを変更したいのだ。.vscode/launch.json にenvで指定せよというのが見つかったのでやってみたが、効かなかった。

仕方ないのでcargoを直接起動する時用のラッパを作って逃げることにした。

#!/bin/sh
CARGO_TARGET_DIR=cargo-target cargo $*

これを使って、cargo runのかわりに、./c runとする。少々面倒だが仕方無い。

コメントを書き込む
#1154
2022/03/27 07:18

曇りがちで風も強かったけど、近所の公園へ。

コメントを書き込む
#1122
2022/03/23 09:17

去年は咲かなかったけど、今年は無事咲いた。

コメントを書き込む
#1090
2022/02/23 08:14

今年の開花は遅いみたい。三分咲きという感じ。

コメントを書き込む

USBメモリにISO焼いて起動しても途中でエラー。Windows10はDVD-DLに焼かないとダメなのね。多分USBのドライバが入ってないのだと思う。

普段はXubuntuなのでWindowsは別のドライブに導入してマザーのブートメニューで切り替え。しばらく大丈夫だったのが突然Xubuntu側が起動できずBusyboxが上がるだけに。GRUBかMBRが壊れたのかなとUbuntuのDVDで起動してfdisk -lするとNVMeドライブがいない! SSDが突然死かと思って外してUSBアダプタに付けて確認すると問題無し。まさかマザーのNVMeが死んだのかな、ちょっと考えにくいなとUEFIのメニューを確認したら、NVMeのストレージモードがAHCIからIntel Optaneに変更されていた。なんだこれと思ってAHCIに戻したら普及。

当然Windowsの方は起動しなくなってしまったので再インストール。マザーのチップセットドライバを入れ直していて気付いた。チップセットドライバの中にIntelのストレージドライバがあるのだけど、これが脳天気に「AHCIなんてやめてOptane入れちゃいなよ」とサジェストしてくるのだった。最初の時は何も考えずにそもまインストールしたので、Optaneモードに変更されてしまったのだな。

確かWindows8の時にキャンペーン的なのがあって、手持ちのXPのライセンスをWindows8に格安でアップグレードしたんだけど、当時はどうもライセンス回りの処理がいまいちだったようで、何度かライセンス・エラーになって面倒になって放置していた。今回は、そのライセンスとWindows10のISOで導入してみたのだけど、最初にMicrosoftアカウントでログインするだけで自動的にライセンスが有効になっていたので改善されたのだろう。

コメントを書き込む
#996
2021/03/27 05:29
近所の公園を散歩。 宴会禁止になっていたので静かで良かった。





コメントを書き込む
#995
2021/02/06 09:28

三分咲きといったところ。

コメントを書き込む

「プログラミング言語Rust公式ガイド」を大分前に送って戴いていたのを、ようやく読んだ。

Rustは、個人的にかなり気に入った言語で、早く組込み開発で普通に使えるようにならないかなと思っている。Rustの公式のガイドがとても良く出来ており、おそらく多くの人がここをとっかかりに始めているのではないかと思う。本書は、このガイドを読んだ人、読んでいない人のどちらにも勧められる良書だ。ただ中盤以降の翻訳は一部こなれていない箇所もあり、意味が良く分からない時のために原書もあると良いと思う。

2章 Shadowing。Rustでは同じ名前の変数を複数回宣言できて、それにより前の変数はShadowされる(見えなくなる)。他の言語では、ほとんどが二重宣言のエラーになるところだ。便利かもしれないが個人的にはRustの言語仕様の中で、ほぼ唯一疑問を感じるところ。まぁ関数のサイズが小さければ問題ないのかもしれない。

2章 Result型。他の多くの言語が例外を使うのに対し、Rustは正常時と異常時との2つの型を持てるいわゆるEither型を使用する。ユーザのコードがResult型の結果を受けて何もしていないとコンパイラが警告を出してエラー処理を忘れていないかと忠告してくれる。

3章 Rustのセミコロンの扱いは、他の言語(行末のセミコロンを省略しても良いという仕様)から来た人をとまどわせることが多いと思う。値を返したい場合にはセミコロンを書いてはいけないし、文の間は基本はセミコロンで区切らないといけない。

4章 Rustで特徴的なのが所有権の管理。

let s1 = String::from("Hello");
let s2 = s1;

最後の文でs1からs2に文字列の所有権が移転されて、この後はs1が使用できなくなってしまう。これにより二重解放のバグが回避される。

4章 そしてもう1つ象徴的なのが参照の管理。例えばJavaとかScalaでは型によりImmutableかどうかが決定される(Javaはまぁ、Collections.unmodifiableXXX()みたいな型だけじゃ判別できない辛いやつもあるけど)。String型ならImmutableだし、StringBuilderならMutableだ。Rustはここの考え方が根本的に異なり、最初に見た時は衝撃を受けた。コードのあらゆるポイントで以下が満たされていることをコンパイラがチェックしてくれる。

  1. Immutableな参照(&)だけであれば、同時に幾つでも生成できる。
  2. 1つでもMmutablな参照(&mut)があれば、これ以外に別のMutableな参照もImmutableな参照も同時に存在できない。

この条件を満たしさえすれば、何もImmutableなデータのみに拘泥してロジック組まなくてもいいよね、という考え方なのだ。そしてスレッド安全性もコンパイラがチェックできてしまう。

8章 Rustはデフォルトはmoveセマンティクスなので、コレクションにデータを格納すると、所有権が移転されてコレクション側に移る。このあたりも他の言語から来た人を、かなり驚かせる点だろうと思う。

10章 そしてライフタイム。Rustでは、ライフタイム(そのデータがどこまで有効であるか)を指定するのにジェネリクスの記法を用いる。おそらくRustで最初につまずくポイントがここだろう。

13章 Rustではクロージャを利用できる。ただクロージャは環境にアクセスできるので、その環境からキャプチャした変数の扱いにも所有権が絡んでくる。そのためクロージャにも3つの型があり、FnOnceは環境から所有権を奪い、FnMutは可変借用をし、Fnは不変借用をする。

17章 代表的なGoFデザインパターンをRustで実装してみる試みがおもしろい。

19章 関連型。Iteratorは要素の型を型パラメータではなく、関連型で指定する。

pub trait Iterator {
  type Item;
}

Scalaでも抽象型宣言という同じ機能があるのだが、型パラメータを使うのと比べて何が違うのかが良く分からなかった。本書は関連型を使うと何が嬉しいのかが具体的に解説されている。本書の良い点は随所でこのように「なぜそうなっているのか、何が嬉しいのか」かがきちんと説明されている点だ。

以上、個人的に特に面白いなと感じた点を、つまみ食い的に紹介してきたが、本書の内容は網羅的であり、これ1冊理解すればRustの大部分を理解できると思う。

Rustの魅力は、上にも書いたように低レベルの処理をマルチ・スレッド処理も含めて安全に書けることに加え、しがらみが無いため単純で美しい点、エラーメッセージが懇切丁寧である点、また単体テストやビルドツールが標準で用意されている点などが挙げられると思う。最近は採用されるケースも良く聞くようになってきたので、今後ますますの普及に期待したい。

コメントを書き込む
1 / 18