CSSで画像をトリミングするためにobject-fitを使ってたら痛い目を見ました

久しぶりにHTMLコーディングをすることがありました。

大きい画像をCSSでトリミングして良い感じに表示したいなと思い、object-fit: cover; を使って良い感じに表示できてすごく便利になったものだなと感心してました。

で、あとから画像の比率がおかしいと指摘があり、ChromeやSafari・Firefoxで見ても崩れてないからキャッシュの仕業かなと思ってたんですが違いました。

ええ、コイツです。

f:id:ryskit:20190923231519p:plain

object-fit って便利なんですけど、IEが対応してくれてないのが残念だ。。。。

と思ってたら、JSでなんとか対応してくれるライブラリがありました。

object-fit-images ライブラリ

github.com

1. CSSを記述する

使い方は簡単で、ChromeやSafari・Firefoxはobject-fitを使えるので、その部分のCSSだけ外出しして以下のようにCSSを書いておきます。

.object-fit-img {
    object-fit: cover;
    font-family: 'object-fit: cover;';
}

大事なのは、 font-family: 'object-fit: cover;'; と記述している部分。 ここに書いているものがIEで適用されるので、ここを書き忘れるとIEでobject-fitが動きません。

これを書き忘れて、IEでも動かねーじゃねーかとハマってしまいました。。。ごめんなさい。

2. ofi.min.js ファイルを読み込む&有効にする

object-fit-images をダウンロードすると、 dist ディレクトリの下に、 ofi.min.js が入っているのでそれをheadタグ内で読み込めるように設定してください。

その後、JSファイルやスクリプトタグの中で、以下のように記述してください。

objectFitImages('img.object-fit-img');

これがないと、IEで有効にならないので注意。 img.object-fit-img は class名:  object-fit-img が付けられたimgタグに対して有効にするとしています。 ここはよしなに変更してください。

3. object-fitを使いたいimgタグにクラス名を付ける

1.で記述したクラス名をimgタグのクラス名に追加してください。 これでIEでもobject-fitが使えるようになります!

<img class="object-fit-img" src="...">

最後に

Web制作って久しぶりにやると楽しいけど、各ブラウザへの対応とかどこまで保証するかとか大変だなと思いました。

個人的にサーバー書いてるほうが楽しいな!

MVVMアーキテクチャでAPIからビール情報を取得して表示するコードを書くのに役立った記事をまとめておく

PUNK APIというBREW DOGのビール情報を返してくれるAPIがあり、それを使って簡単なデータ表示アプリをMVVMアーキテクチャで作ったときに役立った記事のリンクを備忘録としてまとめておく。

ライブラリは以下を利用した。

  • HTTP通信周り
    • OkHttp4
    • Retrofit2
  • Serde
    • GSON
  • DI
    • Koin
  • Databinding
  • Coroutine

先輩の記事

前々からプライベートでAndroidアプリを作ることが決まっていて、 Androidアプリ開発自体が初めてなので、イマイチどのライブラリを使えば良いか分からなかったり、そもそも使い方もどうやるんだろうと試行錯誤していて、 同じ会社の先輩に相談したりしていた。

たまたま、TwitterでRetrofit2やmoshiの記事を連投していると、その先輩がこういうので作ってみるといいよーとわざわざサンプルアプリを作って共有してくださってかなり理解も進んだような気がする!ありがとうございます!

そのサンプルアプリについての記事がこれ。

dev.classmethod.jp

Githubのリポジトリはこれ。

github.com

Android開発で書籍に出てくるようなサンプルアプリを作ったあと右往左往している状態なら、ひとまずリポジトリのコードを読んで試してみると良いかも!

Databindingを学ぶならこれ

たぶん技術書典で販売されていたものだと思う。 これは読んでいてすごく勉強になった。感謝!

booth.pm

Databinding + RecyclerViewで悩んだらこれ

記事ではないけど、サンプルアプリのコードを読めばいろいろ理解できるからおすすめ!

このリポジトリは良かった。

github.com

Retrofit2

square.github.io

qiita.com

Koin

Koinは先輩のコード読んだりドキュメント読んだり、記事読んだりしたぐらい。 やっぱり、ドキュメント読みつつ、他人のコードを読むのが一番理解できる。

insert-koin.io

qiita.com

Coroutine

ココらへんを読んだ気がする。まだまだ使いこなせてないから頑張るぞ!

qiita.com

qiita.com

最後に

自分のコードはある程度、書き直してからGithubのリポジトリにアップする予定です。

試して理解 Linuxのしくみの第4章を読んだ

blog.ryskit.com

前回の続き。

この4章で取り扱うのは「プロセススケジューラ」についてです。

この章で大事なのは以下のとおり。

  • 同時に何個のプロセスが実行していても、ある瞬間に論理CPU上で動作できるプロセスは1つだけ
  • 論理CPU上では、複数プロセスが、プロセスを順番に1ずつ動かして、1周したらまた一番目のプロセスから動かすラウンドロビン方式で動作している
  • 各プロセスはおおよそ等しい長さのタイムスライスを持つ
  • プロセス終了までの経過時間は、プロセス数に比例して増加
  • 1つのCPU上で同時に処理するプロセスは1つだけ
  • 複数プロセスが実行可能な場合、個々のプロセスを適当な長さの時間ごと(タイムスライス)にCPU上で順番に処理する
  • マルチコアCPU環境では、複数プロセスを同時に動かさないとスループットが上がらない
  • 単一論理CPUの場合と同様に、プロセス数を論理CPU数より多くしてもスループットは上がらない

実験プログラムを書いてVPS上で試したり、書籍に載っているグラフを見てなんとなく理解した気になっているが、 まだ自分の中に落とし込めていないように思う。

とりあえずわからなくても先に進むのが良いので、2周目読むときに復習兼ねて記事にまとめよう。

ノート

第4章 プロセススケジューラ

  • マルチコアCPUは、Linuxからは、1つのコアが1つのCPUとして認識される
  • プロセスは、ロードバランサ機能によって、システムの負荷に応じて複数の論理CPUをまたいで実行することがある
  • tasksetコマンド
    • コマンドライン引数で指定したプログラムを、「-c」オプションによってい指定した論理CPU上でのみ動作させる
  • 各プロセスは論理CPUを使っている間だけ処理が進捗し、論理CPU上でもう一方のプロセスが動作している間は進捗しない
  • 単位時間あたりの進捗は、プロセス数=1のおよそ半分。プロセス数=1の場合は1ミリ秒ごとに1%程度、プロセス数=2の場合は1ミリ秒ごとに0.5%程度
  • 処理完了までの経過時間は、プロセス数=1の場合のおよそ2倍
  • 同時に何個のプロセスが実行していようとも、ある瞬間に論理CPU上で動作できるプロセスは1つだけ
  • 論理CPU上では、複数プロセスが、プロセスを順番に1ずつ動かして、1周したらまた一番目のプロセスから動かすラウンドロビン方式で動作している
  • 各プロセスはおおよそ等しい長さのタイムスライスを持つ
  • プロセス終了までの経過時間は、プロセス数に比例して増加
  • コンテキストスイッチ
    • 論理CPU上で動作するプロセスが切り替わること
Linuxにおいて、必ずしも、foo()直後にbar()を実行するという保証はない
void foo(void)
{
  ...
  foo()
  bar()
  ...
}
プロセスの状態 意味
実行状態 現在論理CPUを使っている
実行待ち状態 CPU時間が割り当てられるのを待っている
スリープ 何らかのイベントが発生するのを待っている。イベント発生までのCPU時間は使わない
ゾンビ状態 プロセスが終了した後に親プロセスが終了状態を受け取るのを待っている
  • プロセスは生存中に、実行状態、実行可能状態、スリープ状態という複数の状態を行き来する
  • アイドルプロセス
    • 何もしない特殊なプロセス
    • 新たにプロセスが生成されるか、スリープしているプロセスが希少するまで無駄なループをする
    • CPUの特殊な命令を用いて論理CPUを休止状態にし、1つ以上のプロセスが実行可能状態になるまで消費電力を抑えた状態にする
  • 論理CPU上で一度に実行できるプロセスは1つだけ
  • スリープ状態においてはCPU時間を使わない

スループットとレイテンシ

  • スループット: 単位時間あたりの総仕事量。高いほど良い
  • レイテンシ: それぞれの処理の開始から終了までの経過時間。短いほど良い。
  • プロセスが動作中。実行待ちプロセスがないので、理想的な状態。ただしこの状態で次のプロセスが実行可能状態になると、2つのプロエスのレイテンシが両方とも長くなる
  • プロセスが動作中。実行待ちプロセスもあり。スループットは高いが、レイテンシが長くなる傾向にある

  • 経過時間: プロセスが開始してから終了するまでの経過時間。
  • 使用時間: プロセスが実際に論理CPUを使用した時間
  • 1つのCPU上で同時に処理するプロセスは1つだけ
  • 複数プロセスが実行可能な場合、個々のプロセスを適当な長さの時間ごとにCPU上で順番に処理する
  • マルチコアCPU環境では、複数プロセスを同時に動かさないとスループットが上がらない
  • 単一論理CPUの場合と同様に、プロセス数を論理CPU数より多くしてもスループットは上がらない

試して理解 Linuxのしくみの第3章を読んだ

blog.ryskit.com

の続き。

今日は第3章のプロセス管理を読んだ。

カーネルによるプロセス生成と削除の機能についてだが、仮想記憶の理解がないと詳しい理解が得られないということで、仮想記憶がない単純な場合をもとに話が進んだ。仮想記憶については5章で述べられるようだ。

プロセスに関しては、以前に「なるほどUnixプロセス」を読んでいたため、理解が早かった。

tatsu-zine.com

ただ、プログラムがどのようにメモリ上にマップされるかまでは把握してなかったので、 解説の図を見たり、readelfコマンド、/proc/[pid]/maps ファイルを実際に確認したので、 ざっくりこんなもんかと知れたのは良かった。

ノート

第3章 プロセス管理

  • Linuxにおいて、プロセス生成は2つの目的がある
    • 同じプログラムの処理を複数のプロセスに分けて処理する
      • fork()
    • 全く別のプログラムを生成する
      • execve()
  • fork() 関数
    • 発行したプロセスをもとに、新たにプロセスを1つ生成する
      • 親プロセス - 子プロセス
    • 流れ
      1. 子プロセス用のメモリ領域を作成して、親プロセスのメモリをコピーする
      2. 親プロセスと子プロセスは違うコードを時刻するように分岐する
      3. form()関数の戻り値が、親プロセスと子プロセスの間で異なることを利用する
  • execve() 関数
    • 流れ
      1. 実行ファイルを読み出して、プロセスのメモリマップに必要な情報を読み出す
      2. コードを含むデータ領域のファイル上オフセット、サイズ、およびメモリマップ開始アドレス
      3. コード以外の変数などデータ領域ついての上記と同じ情報
      4. 最初に実行する命令のメモリアドレス(エントリポイント)
      5. 現在のプロセスのメモリを新しいプロセスのデータで上書きする
      6. 新しいプロセスの最初の命令から実行開始する
  • https://qiita.com/zacky1972/items/ef4486e8a6d95edb68fd
    • CPUは レジスタ (register, processor register) と呼ばれる小規模な記憶装置を持っている
    • レジスターはメモリと比べて記憶容量が小さいが,その代わりとても高速に読み書きすることができる
  • Linuxの実行ファイルは Executable Linkable Format(ELF)というフォーマットを使用する
    • readelfコマンドで確認できる
      • 開始アドレスを得る場合は「-h」オプション
      • コードとデータのファイル内オフセット、サイズ、開始アドレスを得る場合は「-S」オプション
      • 「.text」なのがコード領域の情報、「.data」なのがデータ領域の情報
  • プログラム実行時に作成されたプロセスのメモリマップは、/proc/[pid]/mapsというファイルで得られる
cat /proc/29537/maps

55e16a5e9000-55e16a5f0000 r-xp 00000000 fc:02 1048708                    /bin/sleep
55e16a7f0000-55e16a7f1000 r--p 00007000 fc:02 1048708                    /bin/sleep
55e16a7f1000-55e16a7f2000 rw-p 00008000 fc:02 1048708                    /bin/sleep
55e16ac85000-55e16aca6000 rw-p 00000000 00:00 0                          [heap]
7f9cccce9000-7f9ccce5c000 r--p 00000000 fc:02 131771                     /usr/lib/locale/C.UTF-8/LC_COLLATE
7f9ccce5c000-7f9ccd043000 r-xp 00000000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd043000-7f9ccd243000 ---p 001e7000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd243000-7f9ccd247000 r--p 001e7000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd247000-7f9ccd249000 rw-p 001eb000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd249000-7f9ccd24d000 rw-p 00000000 00:00 0
7f9ccd24d000-7f9ccd274000 r-xp 00000000 fc:02 1185270                    /lib/x86_64-linux-gnu/ld-2.27.so
7f9ccd295000-7f9ccd2c6000 r--p 00000000 fc:02 131773                     /usr/lib/locale/C.UTF-8/LC_CTYPE
7f9ccd2c6000-7f9ccd2c7000 r--p 00000000 fc:02 131778                     /usr/lib/locale/C.UTF-8/LC_NUMERIC
7f9ccd2c7000-7f9ccd2c8000 r--p 00000000 fc:02 131781                     /usr/lib/locale/C.UTF-8/LC_TIME
7f9ccd2c8000-7f9ccd2c9000 r--p 00000000 fc:02 131776                     /usr/lib/locale/C.UTF-8/LC_MONETARY
7f9ccd2c9000-7f9ccd2ca000 r--p 00000000 fc:02 131782                     /usr/lib/locale/C.UTF-8/LC_MESSAGES/SYS_LC_MESSAGES
7f9ccd2ca000-7f9ccd2cb000 r--p 00000000 fc:02 131779                     /usr/lib/locale/C.UTF-8/LC_PAPER
7f9ccd2cb000-7f9ccd2cc000 r--p 00000000 fc:02 131777                     /usr/lib/locale/C.UTF-8/LC_NAME
7f9ccd2cc000-7f9ccd2cd000 r--p 00000000 fc:02 131770                     /usr/lib/locale/C.UTF-8/LC_ADDRESS
7f9ccd2cd000-7f9ccd2ce000 r--p 00000000 fc:02 131780                     /usr/lib/locale/C.UTF-8/LC_TELEPHONE
7f9ccd2ce000-7f9ccd2cf000 r--p 00000000 fc:02 131775                     /usr/lib/locale/C.UTF-8/LC_MEASUREMENT
7f9ccd2cf000-7f9ccd46a000 r--p 00000000 fc:02 145331                     /usr/lib/locale/locale-archive
7f9ccd46a000-7f9ccd46c000 rw-p 00000000 00:00 0
7f9ccd46c000-7f9ccd473000 r--s 00000000 fc:02 137960                     /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f9ccd473000-7f9ccd474000 r--p 00000000 fc:02 131774                     /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
7f9ccd474000-7f9ccd475000 r--p 00027000 fc:02 1185270                    /lib/x86_64-linux-gnu/ld-2.27.so
7f9ccd475000-7f9ccd476000 rw-p 00028000 fc:02 1185270                    /lib/x86_64-linux-gnu/ld-2.27.so
7f9ccd476000-7f9ccd477000 rw-p 00000000 00:00 0
7ffd81948000-7ffd81969000 rw-p 00000000 00:00 0                          [stack]
7ffd819ad000-7ffd819b0000 r--p 00000000 00:00 0                          [vvar]
7ffd819b0000-7ffd819b2000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
  • プログラム終了には「_exit()」関数(内部的にはexit_group()システムコール)を呼ぶ
    • プロセスに割り当てていたメモリをすべて回収する

試して理解 Linuxのしくみの第2章を読んだ

blog.ryskit.com

の続き。

第2章では、CPUはユーザモードとカーネルモードを切り替えて処理を実行しているということを学んだ。

CPUのモード遷移の図や実験で試したプログラムのCPUのモードの遷移図は分かりやすい。

試したコマンドは、strace, sar

簡単なプログラムを書いて、「ユーザモードで動く場合」「カーネルモードで動く場合」のプロセスのモードの割合を確認した。

まだ難しくない内容なので、どんどん進めていこう。

ノート

第2章 ユーザモードで実現する機能

  • 各種プロセスは、プロセス生成、ハードウェアの操作など、カーネルの助けが必要なときにはシステムコールという手段によってカーネルに処理を依頼する
  • システムコール
    • プロセス生成、削除
    • メモリ確保、解放
    • プロセス間通信
    • ネットワーク
    • ファイルシステム
    • ファイル操作(デバイスアクセス)
  • システムコールを発行すると、CPUにおいて割込みというイベントが発生する
    • これにより、CPUではユーザモードからカーネルモードに依頼し、内容に応じてカーネルの処理が動く
    • 処理が終われば、ユーザモードに戻りプロセスの動作を継続する
  • straceの出力は、1つのシステムコール発行が1行に対応している
  • sarコマンドでプロセスがユーザモードとカーネルモードのどちらで実行しているかの割合を取得できる
  • sar -P ALL 1
  • allになっている行は全CPUの平均値
  • ユーザモードでプロセスを実行している割合は、%userと%niceの合計
  • CPUコアが、カーネルモードでシステムコールなどの処理を実行している時間の割合は、「%system」によって得られる
  • %idleはCPUコア上でプロセスもカーネルも動いていない状態
各行が1つのCPUに対応している
サーバーは1CPUなので、一つ

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all      0.00      0.00      0.00      0.00      0.00    100.00
Average:          0      0.00      0.00      0.00      0.00      0.00    100.00
loop処理実行後

Linux 4.15.0-50-generic (133-130-123-136)   06/10/19    _x86_64_    (1 CPU)

21:51:46        CPU     %user     %nice   %system   %iowait    %steal     %idle
21:51:47        all    100.00      0.00      0.00      0.00      0.00      0.00
21:51:47          0    100.00      0.00      0.00      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all    100.00      0.00      0.00      0.00      0.00      0.00
Average:          0    100.00      0.00      0.00      0.00      0.00      0.00
親プロセスのプロセスIDを得るシステムコールのループ処理実行後

sar -P ALL 1 1
Linux 4.15.0-50-generic (133-130-123-136)   06/10/19    _x86_64_    (1 CPU)

21:55:02        CPU     %user     %nice   %system   %iowait    %steal     %idle
21:55:03        all     46.46      0.00     53.54      0.00      0.00      0.00
21:55:03          0     46.46      0.00     53.54      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     46.46      0.00     53.54      0.00      0.00      0.00
Average:          0     46.46      0.00     53.54      0.00      0.00      0.00
  • %systemが数十のような大きな値に似合っている場合は、むやみにシステムコールを発行しているか、システム負荷が高すぎる場合が多い
  • straceに「-T」オプションをつけると、各種システムコールの処理にかかった時間をマイクロ秒の精度で採取できる
  • システムコールは、通常の関数呼び出しとは違い、直接呼び出せない
    • アーキテクチャ依存のアセンブリコードを使って呼び出す必要がある
    • この問題を解決するために、OSは内部的にシステムコールを呼び出すだけの、システムコールラッパーと呼ばれる一連の関数を提供している
    • https://twitter.com/_ryskit/status/1138069450014330881
  • glibcは、システムコールのラッパー関数を含む
    • ldd /bin/echo
    • ldd ppidloop
    • ldd /usr/bin/python3

試して理解 Linuxのしくみの第1章を読んだ

f:id:ryskit:20190609202759j:plain

「Linuxのしくみ」はOSやハードウェアについて、実際に手を動かして挙動を確認しながら学べる本です。

仕事で一緒に働いている方々が低レイヤに詳しいので、感化されてこの本を手に取った。 理由は、試して理解と書いてあったから。

手を動かさずに理解するのは得意じゃなく、手を動かして、どういった動きをするのか自分の目で確認しつつ学びたかったので、この本は僕にはちょうど良かった。

全部で8章で、1章の概要はすでに読んだからあと4週間もあれば試して少しは理解できる状態になっているのではないかと期待している。

実験プログラムの環境はUbuntu 16.04/x86_64環境なので、とりあえずConohaで月640円のVPS借りて試してみることにする。 ローカルで立ててもいいのだけれど、会社のPCしか持ち合わせていないことも考えて、いつでもログインできるようにしておきたかったからだ。

パラッと全体に目を通してみた感じだと図が多く、分かりやすそうといった印象。 とにかく、手を動かすしかない。

ノート

第1章 コンピュータシステムの概要

  • OSは各種プログラムをプロセス単位で実行する
  • 各プログラムは1つないし複数のプロセスから構成される
  • Linuxの重要な役割として、外部デバイスの操作がある
    • LinuxのようなOSがないと、各プロセスが独自にデバイスを操作するコードを書く必要がある
    • LInuxではデバイスを操作する処理をデバイスドライバというプログラムにまとめており、プロセスからデバイスには、このデバイスドライバを介してアクセスする
  • Linuxはハードウェアの助けを借りて、プロセスからデバイスに直セスアクセスできないようにしている
    • CPUには2つのモードがある
      • カーネルモード
        • カーネルモードの場合に、デバイスにアクセスできるようにする
        • デバイスドライバはカーネルモードで動作する
        • プロセス管理システム
        • プロセススケジューラ
        • メモリ管理システム
      • ユーザーモード
        • プロセスはユーザーモードで動作する
  • カーネルモードで動作する、OSの核となる処理をまとめたプログラムを「カーネル」と呼ぶ
  • プロセスは、カーネルが提供する機能を使いたければ、システムコールという特殊な処理を介してカーネルに依頼する

direnvの使い方をいつも忘れるからメモしておく

direnvの使い方をいつも忘れるので備忘録として書いておく。

direnvとは

特定のディレクトリに移動したときに設定ファイルに記載された環境変数を読み込んで有効化するツール。 とりあえず、まだインストールしてないのであればインストールするべし!便利なので!

インストール

Macの方はHomebrewで入れればいいと思います。

$ brew install direnv

上記以外でインストールする方はリポジトリのREADMEを読んでください。

github.com

使い方

特定のプロジェクトのディレクトリ配下で .envrc に環境変数を書き込む

$ echo "export AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx" >> .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ echo "export AWS_SECRET_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx" >> .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ echo "export AWS_DEFAULT_REGION=ap-northeast-1" >> .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

.envrcの内容を許可する

このままだと .envrc はブロックされたままなので以下のコマンドで許可してあげる。

$ direnv allow .
direnv: loading .envrc
direnv: export +AWS_ACCESS_KEY_ID +AWS_DEFAULT_REGION +AWS_SECRET_ACCESS_KEY

これで、環境変数がロードされたはず。 ためしに、環境変数を出力してみよう。

$ echo $AWS_DEFAULT_REGION
ap-northeast-1

はい、ちゃんと出力されてます。

特定のディレクトリから抜けると環境変数もアンロードされる

今のディレクトリから一つ上の階層に移動してみよう。

$ cd ..
direnv: unloading

.envrcに書き込んだ環境変数がアンロードされました。 確認するために出力してみます。

$ echo $AWS_DEFAULT_REGION

はい、何も出力されなくなりました。

これでいちいち環境変数を読み込んだりする作業が楽になりますね!

注意

.envrc はGithub等にコミットするのはやめましょう。 リポジトリが公開されていると、AWSのシークレットキーなどが盗まれて悪用されちゃうので。

メタプログラミングRuby第2版 Ⅱ部を読んだ

メタプログラミングRuby第2版 Ⅰ部を読んだの続き

メタプログラミングRuby第2版 Ⅱ部を読んだので、簡単な内容と感想を書いていく。 最後に、読んでメモしたものを追記しておく。

I部ではRubyのメタプログラミングの考え方や継承チェーンの仕組み、メソッドやイディオムの紹介が主な内容となっていた。

Ⅱ部では、Ruby on Rails に含まれるライブラリである Active Record や Active Support などがメタプログラミングの技術を用いてどうやって実装されているかを学ぶ。

個人的に面白かったのは、第10章のActive SupportのConcernモジュールで、includeとextendメソッドを使ったトリックのメリットとデメリットをあげ、Concernモジュール内ではそのデメリットをどう解決しているかを実際のコードを読みながら学ぶことができる部分だ。

といっても、実際に取り上げられるモジュールのコードはほんの1ファイルの一部だけなので、Railsの学習も兼ねて、ActiveSupportやActiveModelあたりのモジュールはざっくりコードリーディングしていきたいところ。

あと、付録のCが「魔術書」となっていて、メタプログラミングのトリックやイディオムが33個書かれている。

ここを読むだけでも、この本の価値はあるんじゃないかな。

メタプログラミングRuby第2版は全体的に内容も面白いので、少しでも興味があるなら読むことをおすすめします!

第9章 Active Recordの設計

  • オートローディング
    • ActiveSupport::Autoloadモジュール
    • モジュール名を最初に使ったときに、自動的にモジュール(やクラス)のソースコードを探して、requireするという命名規約が使われている
    • Active Record は ActiveSupport::Autoload をエクステンドしているので、autoload は ActiveRecord モジュールのクラスメソッドになる(わからない場合は、クラス拡張(p.135)を読む)
    • run_load_hooks を呼び出している行がある。これは、オートロードされたモジュールが設定用のコードを呼び出せるようにするもの
  • Validationsモジュール
    • validateメソッドは、ActiveModel::Validationsにある
      • ActiveRecord::Validationsがインクルードしたモジュール
    • クラスがモジュールをインクルードすると、通常はインスタンスメソッドが手に入るが、validate は ActiveRecord::Base のクラスメソッドである

第10章 Active SupportのConcernモジュール

  • Kernel#autoload
  • includeとextendのトリックは便利ではあるが、問題もある
    • クラスメソッドを定義するあらゆるモジュールが、インクルーダーを拡張するincludedというフックメソッドを定義する必要が出てくる
  • includeの連鎖の問題
    • インクルードするモジュールが、また別のモジュールをインクルードしている場合
module SecondLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  def second_level_instance_method; 'ok'; end

  module ClassMethods
    def second_level_class_method; 'ok'; end
  end
end

module FirstLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  def first_level_instance_method; 'ok'; end

  module ClassMethods
    def first_level_class_method; 'ok'; end
  end
  
  include SecondLevelModule
end

class BaseClass
  include FirstLevelModule
end

BaseClass.new.first_level_instance_method # => "ok"
BaseClass.new.second_level_instance_method # => "ok"
BaseClass.first_level_class_method # => "ok"
BaseClass.second_level_class_method # => NoMethodError
  • Rails2だと上記のコードの問題を以下のように解決している
module FirstLevelModule
  def self.included(base)
    base.extend ClassMethods
    base.send :include, SecondLevelModule
  end
  ...
  end
  • ActiveSupport::Concernは、 includeとextendのトリック をカプセル化して、includeの連鎖の問題を解決している
    • この機能を手に入れるには、モジュールでConcernをextendして自身のClassMethodsモジュールを定義する
require 'active_support'

module MyConcern
  extend ActiveSupport::Concern
  def an_instance_method; " インスタンスメソッド "; end
  
  module ClassMethods
    def a_class_method; " クラスメソッド "; end
  end
end
  • モジュールがConcernをextendすると、extendedを呼び出す。エクステンダーにクラスインスタンス変数(p.114)である@_dependenciesを定義する
  • Module#append_features は、Ruby のコアのメソッド
  • includedとappend_featuresの違い
    • includedはフックメソッドなため、通常はメソッドの中身がなく、オーバーライドして使う
    • append_featuresは実際にインクルードするもの
      • インクルードされたモジュールがインクルーダーの継承チェーンに含まれているかどうかを確認して、含まれていなければ継承チェーンにモジュールを追加する
      • append_features をオーバーライドすると、モジュールが一切インクルードされなくなる。
  • 本章では、「ActiveSupport::Concern をエクステンドしたモジュール」を表すときに、小文字 の「concern」を使うことにする。上記のコードでは、MyConcern が concern だ。今の Rails では、ActiveRecord::Validations や ActiveModel::Validationsも含めて、ほとんどのモジュールがconcernである。
rails/activesupport/lib/active_support/concern.rb

module Concern
  class MultipleIncludedBlocks < StandardError #:nodoc:
    def initialize
      super "Cannot define multiple 'included' blocks for a Concern"
    end
  end

  def self.extended(base) #:nodoc:
    base.instance_variable_set(:@_dependencies, [])
  end

  def append_features(base) #:nodoc:
    if base.instance_variable_defined?(:@_dependencies)
      base.instance_variable_get(:@_dependencies) << self
      false
    else
      return false if base < self
      @_dependencies.each { |dep| base.include(dep) }
      super
      base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
      base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
    end
  end

  # Evaluate given block in context of base class,
  # so that you can write class macros here.
  # When you define more than one +included+ block, it raises an exception.
  def included(base = nil, &block)
    if base.nil?
      if instance_variable_defined?(:@_included_block)
        if @_included_block.source_location != block.source_location
          raise MultipleIncludedBlocks
        end
      else
        @_included_block = block
      end
    else
      super
    end
  end

  # Define class methods from given block.
  # You can define private class methods as well.
  #
  #   module Example
  #     extend ActiveSupport::Concern
  #
  #     class_methods do
  #       def foo; puts 'foo'; end
  #
  #       private
  #         def bar; puts 'bar'; end
  #     end
  #   end
  #
  #   class Buzz
  #     include Example
  #   end
  #
  #   Buzz.foo # => "foo"
  #   Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError)
  def class_methods(&class_methods_module_definition)
    mod = const_defined?(:ClassMethods, false) ?
      const_get(:ClassMethods) :
      const_set(:ClassMethods, Module.new)

    mod.module_eval(&class_methods_module_definition)
  end
end
  • Concernの基本的な考え方。ActiveSupport::Concern をエクステンドしたモジュールの中で、ActiveSupport::Concernをエクステンドしたモジュールをインクルードしない
  • ActiveSupport::Concernをエクステンドしたモジュールでないモジュールに別のActiveSupport::Concern をエクステンドしたモジュールがインクルードされたら、すべての依存関係をインクルーダーに一気に流し込む。
  • このスコープのなかでは、self は ActiveSupport::Concern をエクステンドしたモジュール である。base はインクルードしているモジュール。
    • concern かもしれないし、そうではないかもしれない。
  • append_features 内では、インクルーダーがActiveSupport::Concern をエクステンドしたモジュールかどうかを確認したい。クラス変数 @_dependencies があれば、それが ActiveSupport::Concern をエクステンドしたモジュール だとわかる。
  • インクルーダーが ActiveSupport::Concern をエクステンドしたモジュール ではない場合(たとえば、ActiveRecord::Validations が ActiveRecord::Base にインクルードされた場合)は、何が起きるのだろうか? ここでは、他の ActiveSupport::Concern をエクステンドしたモジュール がインクルードされるなどして、すでにインクルーダーの継承チェーンに自身が追加され たかどうかを確認している(base < selfが意味するのはそういうことだ)
    • 継承チェーンに追加されていない場合
      • インクルーダーに依存関係を再帰的にインクルードしていく。この最小主義の依存管理システムが「10.1.2 include の連鎖の問題」で触れた問題 を解決する。
      • super で Module.append_features を呼び出して、継承チェーンに自分自身 を追加している
      • ClassMethods の参照は Kernel#const_get を使って取得する必要がある。 コードが物理的に配置されている Concern モジュールではなく、self のスコープで定数を読み込まなければいけないから。

11章 alias_method_chain の盛衰

  • 名声を極めたalias_method_chain メソッドが不評 を招き、最終的に Rails のコードベースから姿を消した話
  • alias_method_chain はエイリアスの重複を取り除くことができるが、それ自身に問題がある。 alias_method_chain はアラウンドエイリアス(p.140)をカプセル化したもの
  • Rails のなかでメソッドのリネームとシャッフルを繰り返したことで、実際に呼び出して いるメソッドがどのバージョンかを追跡するのが難しくなってしまった

12章 アトリビュートメソッドの進化

  • アクセサを動的に定義せず、ゴーストメソッドだけを使う
  • オブジェクトを生成したときに initialize メソッドでアクセサを定義する
  • アクセスされたアトリビュートのアクセサだけを定義する
  • 派生フィールドも含めて、オブジェクトのすべてのアクセサを常に定義する
  • コード文字列ではなく、 define_method でアクセサを定義する

「新装版リファクタリング - 既存のコードを安全に改善する」を読んだ

f:id:ryskit:20190604221313j:plain

今、仕事で参加しているプロジェクトのJavaのコードは数年前のもので、昔に書かれた部分は、メソッド名が分かりづらかったり、処理を一つのメソッドに詰め込み過ぎていて可読性が低い状況である。(テストは結構書かれているから修正できる)

また、最初にアプリケーションを書いたエンジニアはすでにプロジェクトからは抜けており、どうしてそのコードを書いたのか意味を読み取れないこともあったりする。

僕自身、恥ずかしながら前職で汚いコードを書いてきた方だと思うし、今もキレイなコードを書けているかと言われればそうではないだろう(キレイに書く努力はしているつもりで、先輩方にPRで指摘もいただけるので助かっている)

そんな僕でさえ、ウッとなるコードがところどころ見かける。

この読みづらい・修正しづらいコードをリファクタリングしたいと思っていて、今後、プロジェクトの機能開発や修正スピードを上げたり、新しくプロジェクトに入ってくるメンバーが気持ちよくプロジェクトに参加してもらうためには、率先して僕がリファクタリングをしていくべきだと考えている。

そのリファクタリングの作業を良いものにしたくて、この本を手に取った。

最初の5章までは、リファクタリングについての説明から始まり、原則であったり、どういったコードが不吉な臭いを発しているのかなど、細かく説明している。

また、リファクタリングにはテストコードが不可欠であるということも、まるまる1章割いて説明している。

6章からはリファクタリングのカタログを眺めるように読むことができる。どういったときに、どのようなリファクタリングを行うのが良いのか、UMLの図を見て作業手順を読みながら学ぶことができる。

400ページ以上ある本ではあるが、サクッと読めるし、もしリファクタリングの際にヒントが欲しい場合はなんとなく目を通してからリファクタリングするのも良いかもしれない。

すぐに効き目が出るような本ではないと思うが、まだ読んだことがない人はぜひ一度読んでみて、リファクタリングのパターンを頭に入れておくのは良いと思う。

ノート

リファクタリングとは

  • リファクタリングとは、ソフトウェアの外部の振る舞いを保ったままで、内部の構造を改善していく作業を指す
  • 実装したあとで、設計を改善する
  • リファクタリングによって、仕事の作業配分が変わってくる
    • 設計の作業が、最初の工程で集中的に発生するのではなく、全行程を通じて継続して行われるようになる

第1章 リファクタリング - 最初の例

  • 構造的に機能を付け加えにくいプログラムに新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと
  • リファクタする前にテストを用意せよ
  • コード内の論理的な固まりを見つけ出して「メソッドの抽出(p.110)」リファクタリングを適用する
  • リファクタリングをするときには、失敗するとどうなるかを常に把握しておく必要がある
    • 最初に、抜き出そうとする部分でローカルなスコープを持つ変数に着目し、それらが新規に作られるメソッドの一時変数かパラメータにならないか検討する
    • 変更されない変数については、パラメータとして渡すことができる
    • 変更される変数にはもっと注意を払う必要がある
  • リファクタリングでは小さなステップでプログラムを変更していく。そのため誤ったことをしても、バグを見つけるのは簡単である。
  • 名前変更の後には、何以下おかしなことを指定内科、コンパイルしてテストをして確認する
  • コンパイラが理解できるコードは誰にでも書ける。優れたプログラマは、人間にとってわかりやすいコードを書く
  • 仕様するデータを持つオブジェクトにメソッドは定義されるもの
  • 古いメソッドを、新しいメソッドへの委譲として残しておくこともある。これはメソッドがpublicで、’他のオブジェクトに対するインターフェースを変えたくないと金役立つ
  • 一時変数をできる限り取り除くようにする
    • 一時変数は、不必要なパラメータをたくさん受け渡してしまう原因になりがち
    • パラメータ数が多いと、なんのためのものなのかすぐにわからなくなる
  • switch分は他のオブジェクトの属性を調べるのではなく、自分自身について行うべき
  • Stateパターン
  • まとめ
    • メソッドの抽出
    • メソッドの移動
    • ポリモーフィズムによる条件記述の置き換え

第2章 リファクタリングの原則

  • リファクタリング(名詞)は、外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること
  • リファクタリングする(動詞)は、一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること
  • リファクタリングの定義として強調する点
    • あくまでソフトウェアを理解しやすく、変更を用意にするために行う
    • 外的振る舞いを保ったまま、ソフトウェアに多くの変更を加えることは可能
    • 対照的なのは、パフォーマンスの最適化
      • リファクタリング同様に、大抵は振る舞いの仕方を返ることはなく、内部構造が書き換えられる
      • パフォーマンスチューニングでは、コードは理解しにくくなるのが普通
      • 必要なパフォーマンスを得るためには、そうせざるを得ない
    • ソフトウェアの外的振る舞いを保つ
  • ソフトウェア開発でリファクタリングを行うときには、2つの活動に区分すべき
    • 機能追加
      • 機能追加を行うときには、既存のコードを変更してはいけない
      • 機能拡張に専念する
      • 作業の進度は、テストの追加とそれが正常に終了したことによって測ることが可能
    • リファクタリング
      • リファクタリングをしているときには、機能追加は行わないようにする
      • コードの再構築のみ
      • 原則として、テストの追加をしてはいけない(機能追加の段階で漏れていた場合は例外)
  • 3度目の法則
    • 最初は単純に作業する
    • 2度目に以前と似たようなことをしていると気がついているときがついたときは、重複や無駄を意識しつつ作業を続行。
    • 3度目に同じことをしていると気づいたなら、そこでリファクタリング
  • 機能追加のときにリファクタリングを行う
  • バグフィックスのときにリファクタリングを行う
  • コードレビューのときにリファクタリングを行う
  • 何がプログラムを難しくするか
    • 読みにくいプログラムが変更しにくい
    • ロジックが重複しているプログラムは変更しにくい
    • 機能追加に伴い、既存のコード修正が必要になるプログラムは変更しにくい
    • 複雑な条件分岐の多いプログラムは変更しにくい
  • 間接層(indirection)
    • ロジックの共有を可能にする
    • 意図と実装を独立して説明できる
    • 変更を分離できる
    • 条件分岐をポリモーフィズムで表現する
  • オブジェクト指向の利点
    • ソフトウェアの実装とインターフェースを独立して変更できる
    • インターフェースを守ることは重要で、変更すれば何らかの影響が及ぶことになる
  • あまり早急にインターフェースを公布しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要
  • リファクタリングには、設計を補完する役割がある
  • 速いソフトウェアを作る、一般的な3つの方法
    • 最も厳しい要求を実現するのが、時間分割を行うやり方
      • ハードリアルタイムシステムでよく使われる
      • 時間とメモリ使用量というリソース観点から、設計を細かなコンポーネントに分割する
        • 各コンポーネントは予め与えられたリソースの消費量を超えてはならない
        • 与えられた時間を交換するメカニズムが許されていても。
        • 心臓のペースメーカーなどのように、タイミングが遅れたらデータが役に立たないシステムにおいては不可欠な考え方
    • パフォーマンスを常に意識しておくやり方
      • すべてのプログラマがパフォーマンスを常に意識しておくというやり方
      • パフォーマンスで興味深いのは、プログラムを解析してみると、ほとんどの時間がごく一部の処理で集中的に消費されていること
    • パフォーマンスチューニングに、前述の90%の法則を使う
      • このやり方では、まずプログラムを整理された形で作り上げる
      • チューニングの段階で、一定の手順に従い、最適化を行う

第3章 コードの不吉な臭い

重複したコード

  • 同じようなコードが2箇所以上見られたら、1箇所にまとめることを考えると良い
  • 同一クラス内の複数メソッドに同じ式がある場合
    • 「メソッドの抽出」を行い、「メソッドの引き上げ」を適用すれば解決する
  • コードが完全に同じではなく似ている場合
    • 「メソッドの抽出」を使い、共通で使える部分とそうでない部分を分離する
    • 「Template Methodの形成(p.345)」に進んでいけることもある
  • 複数のメソッドが同じ処理を異なるアルゴリズムで実装している場合
    • 「アルゴリズムの取り替え(p.139)」を適用する
  • まったく関係ない2つのクラス間で重複したコードが見られるとき
    • 一方のクラスに対して「クラスの抽出」を行い、もう一方のクラスから新しいクラスへ処理を委譲するようにする
    • または、重複したコードを持つメソッドは、一方のクラスだけが本来持つべきであって、もう一方のクラスからはそれを呼び出すようにすべきかもしれない

長すぎるメソッド

  • メソッド名をわかりやすくする
  • コメントの必要を感じたとき、そうする代わりにわかりやすい名前をつけたメソッドに分割する
  • メソッド名には、内部でどのように処理しているかでなく、そのコードが何をするのかという意図を示すこと
  • 重要なのは、メソッド名の長さを切り詰めるのではなく、メソッド名とその実装との距離を埋めること
  • 「メソッドの抽出」は、メソッド名を短くするのに常に役に立つ
  • パラメータや一時変数が多すぎるメソッドは、メソッドの抽出を妨げる要因となる
  • 「問い合わせによる一時変数の置き換え(p.120)」を組み合わせて、一時変数を減らす必要がある
  • 長いパラメータには「パラメータオブジェクトの導入(p.295)」「オブジェクトそのものの受け渡し(p.288)」を使いスリム化すること
  • 「メソッドオブジェクトによるメソッドの置き換え」を試す

巨大なクラス

  • 1つのクラスが大きい場合、インスタンス変数を持ちすぎになっている
  • クラスがインスタンス変数すべて使っていない場合、「クラスの抽出」「サブクラスの抽出」の適用を何回か試す
  • コード量が多すぎるクラスは「重複したコード」の温床
    • クラスの重複部分を排除する

長すぎるパラメータリスト

  • メソッドの実行の必要なデータを、すべてパラメータで渡したりはしない
    • オブジェクトをそのまま渡し、メソッドがそこからさまざまなデータを取り出せば良い
  • パラメータの数が多いと、1つひとつが何を意味しているのか理解しづらくなる
    • パラメータの一貫性がなくなり、使いにくくなる

変更の偏り

  • 1つのクラスが別々の理由で何度も変更される状況では、「変更の偏り」が起こっている

変更の分散

  • 「変更の分散」は、「変更の偏り」に似ているが異なるもの
    • 変更を行うたびに、あちこちのクラスがすこしずつ書き換わるような場合、不吉な臭いと受け取った方が良い
    • 「メソッドの移動」や「フィールドの移動」を行い、変更部分を一つのクラスにまとめあげるようにする

特性の横恋慕

  • オブジェクト指向には、処理および処理に必要なデータを1つにまとめてしまうという重要な考え方がある
  • 通常、データとそのデータを扱う処理の変更の影響はいっしょに受ける
  • 例外としては、振る舞いのみの変更に対処するために外に出したほうが良い場合もある
    • Strategy、Visitorパターンを使うと振る舞いのみを簡単に変更していけるようになる

データの群れ

  • データの集まりから、ある要素を除外した場合を考えてみてください。
    • 残ったデータの集まりは意味をなすでしょうか。
    • 意味をなさない場合には、もとの集まりが一人前のオブジェクトの候補であったことを示しています。

怠け者クラス

  • 一度作成したクラスは、理解や保守のためのコストがかかる。
    • 十分な仕事をせず、その見返りに合わないようなクラスは排除するべき

仲介人

  • オブジェクト指向の特徴にカプセル化がある
    • 内部の詳細を外部から見えないようにする
  • カプセル化は権限委譲をもたらす

第4章 テストの構築

  • リファクタリングに限らず、よいテストを書くとプログラミングが加速する
  • テストを完全に自動化して、その結果もテストにチェックさせること
  • テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。
    • これによって、バグの発見にかかる時間は削減される
  • リファクタリングにはテストは必須
    • リファクタリングするにはテストを書かなければならない
  • テストフィクスチャ(test fixture)
    • テストデータとなるオブジェクトのこと
  • テストを頻繁に実行せよ。コンパイル時にはテストを局所化して、1日に最低一度はすべてのテストを実行せよ
  • テストしたくないという誘惑に屈してはいけない
    • 報いは必ず訪れる
  • テストを書く際は、はじめは失敗するようにしておく
    • 既存のコードであれば、失敗するように変更するか、予想される値として正しくないものを表明メソッドに与える
    • 本当にテストが実行され、期待通りのテストをしているか確認するため
  • バグレポートを受け取ったら、まずそのバグを明らかにする単体テストを書け
  • 実行されない完全なテストよりも、実行される不完全なテストの方がましである
  • 失敗すると予想されるときに、例外が上がることをテストし忘れないこと

第6章 メソッドの構成

  • メソッドを適切にパッケージ化されたコードとして構成すること
  • 問題を起こすのは、ほとんどの場合、長すぎるメソッドである
  • 「メソッドの抽出」では、ローカル変数の扱いが最大の問題であり、一時変数がこの問題の主たる発生源
    • あるメソッドについて作業するときは、「問い合わせによる一時変数の置き換え」によって取り除ける一時変数はすべて取り除いておきたいところ
  • 一時変数があまりにも絡み合っていて置き換えられない場合、「メソッドオブジェクトによるメソッドの置き換え」が必要

メソッドの抽出

  • コードの断片をメソッドにして、それに目的を表す名前を付ける
  • 長過ぎるメソッドやコメントがなければその木亭が理解できないメソッドを目にしたときは、その断片をメソッドにする

メソッドのインライン化

  • メソッドの本体が名前をつけて呼ぶまでもなく明らかである
  • わざわざ名前をつけて呼ぶほどでもないようなメソッドにリファクタリングしてしまうこともある
    • メソッドを取り除いてしまう
  • 手順
    • そのメソッドがポリモーフィックでないことを確認する
      • サブクラスでオーバーライドしているメソッドをインライン化しないこと。メソッドがなくなるとオーバーライドできなくなる
    • そのメソッドの呼び出しをすべて検索する
    • 各メソッド呼び出しをメソッド本体で置き換える
    • コンパイルしてテストする
    • メソッド定義を取り除く

一時変数の分離

  • 「パラメータに代入する」とは、パラメータとして渡ってきたオブジェクトを別のオブジェクトを参照させるように変更すること
    • 渡されたオブジェクトになにかをすることは問題ない
    • まるごと別のオブジェクトを参照させるように変更するのはだめ
  • 明確さの欠如および「値渡し」と「参照渡し」の間の混乱がこれを嫌う
  • 本質的に、オブジェクトへの参照は値渡し

メソッドオブジェクトによるメソッドの置き換え

  • 大きなメソッドから小さなメソッドを抽出することで、ものごとが把握しやすくなる

第7章 オブジェクト間での特性の移動

  • オブジェクトの設計において根幹をなすのは、唯一とまではいかなくとも、責務をどこに配置するかについての判断である
    • Martin Fowlerでさへ、10年以上オブジェクトを生業としてきたが、責務をはじめから正しいところに配置できない
  • 責務が多すぎてクラスが膨張することもある
    • 「クラスの抽出(p.149)」を適用して、それらの責務を分離する
  • クラスの責務が希薄すぎる場合は、「クラスのインライン化(p.154)」を適用して、他のクラスに併合する
  • あるクラスが他のクラスを使っているとき、「委譲の隠蔽(p.157)」によって、そのことを隠すのが有用なことがある

メソッドの移動

  • あるクラスでメソッドが定義されているが、現在または将来において、そのクラスの特性よりも他クラスの特性の方が、そのメソッドを使ったり、そのメソッドから使われることが多い
  • あるクラスのメソッドをすべて眺めて、それが定義されているオブジェクトよりも、それ以外のオブジェクトを参照することが多いメソッドを探す
    • 特にフィールドを移動したあとは、これを行うほうが良い

フィールドの移動

  • あるクラスに定義されているフィールドが、現在または将来において、定義しているクラスよりも、他のクラスから使われることの方が多い
  • クラス間で状態や振る舞いを移動するのは、リファクタリングの本質
  • フィールドを移動すると考えるのは、そのクラスのメソッドよりも別クラスメソッドの方がそのフィールドを多く使っているとわかったとき

クラスの抽出

  • 2つのクラスでなされるべき作業を1つのクラスで行っている
    • クラスを新たに作って、適切なフィールドとメソッドを元クラスからそのクラスに移動する
  • 「クラスの抽出」は、並行プログラミングの実行性を向上するための一般的な技法

委譲の隠蔽(p.157)

  • クライアントがあるオブジェクトの委譲クラスを呼び出している
    • サーバにメソッドを作って委譲を隠蔽する
  • カプセル化は、オブジェクト指向技術の鍵となる
  • 委譲オブジェクトが変更されると、クライアントの変更も余儀なくされる
    • この依存関係を取り除くには、委譲を隠蔽するための単純なメソッドを配置する

仲介人の除去

  • クラスがやっているのは単純な委譲だけである場合
    • クライアントに委譲オブジェクトを直接呼ばせるように修正する

外部メソッドの導入

  • 利用中のサーバクラスにメソッドを追加する必要があるが、そのクラスを変更できない
    • クライアントクラスに、サーバクラスのインスタンスを第1引数に取るメソッドを作る

局所的拡張の導入

  • 利用中のサーバクラスにメソッドをいくつか追加する必要があるが、クラスを変更できない場合
    • それらの追加されるメソッドを備えた新たなクラスを作る。この拡張クラスは、元のクラスのサブクラスまたはラッパーである
  • メソッドを追加できるのであれば最善であるが、変更できない場合は、サブクラスやラッパークラスを作る
  • サブクラスかラッパーにするかは、martin fowlerはサブクラスを好む
    • 作業が少なくて済むから
  • ラッパーの場合は委譲を使う

データの再編成

自己カプセル化フィールド(p.171)

  • フィールドを直接アクセスしているが、そのフィールドとの結合関係が煩わしい場合
    • そのフィールドに対するgetメソッドとsetメソッドを作って、それだけを使ってアクセスするように変更する
  • 「自己カプセル化フィールド」を行う重要なタイミングは、スーパークラスのフィールドをアクセスしていて、その変数アクセスをサブクラス内の計算値で置き換えたいと思ったとき

オブジェクトによるデータ値の置き換え

  • 追加のデータや振る舞いが必要なデータ項目がある場合
    • そのデータ項目をオブジェクトに変える
  • コードが重複してきたり、特性の横恋慕が臭ってきた場合には、データ値をオブジェクトに変更する

値から参照への変更

  • 同じインスタンスが多数存在するクラスがある。それらを1つのオブジェクトに置き換えたい場合
    • そのオブジェクトを参照オブジェクトに変える
  • 参照オブジェクトと値オブジェクトを分けて考えることが役立つ
  • 参照オブジェクト
    • 顧客や勘定といったもので、実世界における1個のオブジェクトを表す
      • それらが同じかどうかはオブジェクト識別が用いられる
  • 値オブジェクト
    • 日付やお金のようなもの
    • それ自身のデータ値によって定義される
      • コピーはいくらあっても構わない

参照から値への変更

  • 小さくて、不変で、コントロールが煩わしい参照オブジェクトがある場合
    • 値オブジェクトに変える
  • 参照オブジェクトを使った処理が煩わしくなってきたら、それは参照を値に変更するきっかけ
  • 値オブジェクトは、特に分散並行処理システムで有効
  • 値オブジェクトの重要な性質
    • 不変であること
    • そのオブジェクトについて問い合わせを行ったとき、常に同じ値が返されるべき

オブジェクトによる配列の置き換え

  • 配列の各要素がそれぞれ違う意味を持っている場合
    • その配列を、要素ごとに対応したフィールドを持つオブジェクトに置き換える

コレクションのカプセル化

  • メソッドがコレクションを返している
    • 読み取り専用のビューを返して、追加と削除のメソッドを提供する
  • getメソッドはコレクションオブジェクトそのものを返すべきではない
    • コレクションを所有するクラス側で、何が行われているのか知らないうちに、クライアントがそのコレクションの内容を操作できてしまう
  • 振る舞いをクラスに移動する

条件記述の分解

  • 複雑な条件記述(if-then-else)がある
    • その条件記述部とthen部およびelse部から、メソッドを抽出する
  • どんなに長いコードブロックでも、それを分解することで、そしてそのブロックにふさわしい名前を持ったメソッドを呼び出しで、一群のコードを置き換えることで、プログラマの意図を明確化できる

条件記述の統合

  • 同じ結果を持つ一連の条件判定がある場合
    • それらを1つの条件記述にまとめて抽出する
  • 一連の条件判定が全て異なっているのに、結果のアクションが同じである場合がときどきある
    • この場合は、andやorを使って結合して、同じ結果を持つ1つの条件判定にする

重複した条件記断片の統合

  • 条件式のすべての分岐に同じコードの断片がある場合
    • それを式の外側に移動する

ガード節による入れ子条件記述の置き換え

  • メソッド内に正常ルートが不明確な条件つき振る舞いがある場合
    • 特殊ケースすべてに対してガード節を使う

ポリモーフィズムによる条件記述の置き換え

  • オブジェクトのタイプによって異なる振る舞いを選択する条件記述がある場合
    • 条件記述の各アクション部をサブクラスでオーバーライドするメソッドに移動する。元のメソッドはabstractにする
  • ポリモーフィズムの真髄は、オブジェクトの振る舞いがその型によって変わるとき、明示的な条件記述を書かなくても良いようにする

第10章 メソッド呼び出しの単純化

  • 理解が容易で、使いやすいインターフェースを提供すること
  • 状態を更新するメソッドと、状態を問い合わせるメソッドは明確に分離すること
  • 良いインターフェースは、何をすべきかだけを示し、それ以上は何も語らない
    • 内部の詳細を隠蔽することで、インターフェースは改善される

メソッド名の変更

  • メソッドの名前がその目的を正しく表現できていない場合
    • メソッド名を変更する
  • 複雑な処理を小さく分解して、小さなメソッドの集まりにする
  • あなたの書くコードは、第一に人間のためのものであり、コンピューターはその次であることを忘れてはならない

パラメータの削除

  • あるパラメータが、もはやメソッド本体から使われていない場合
    • パラメータを削除する
  • パラメータは必要な情報を指示するもの
  • 値が異なれば、結果も変わるべき
  • 呼び出し側は、そのパラメータにどんな値を渡すのかを考える必要がある
    • パラメータを削除しなければ、そのメソッドを使うすべての人に余計な仕事をさせることになる

問い合わせと更新の分離

  • 1つのメソッドが値を返すと同時にオブジェクトの状態を変更している場合
    • 問い合わせ用と更新用の2つのメソッドをそれぞれ作成する
  • 値を返すメソッドはすべて、副作用を持たないと決めることが大切
  • 値を返すと同時に、副作用も伴うようなメソッドは分離するように努める

メソッドのパラメタライズ

  • 複数のメソッドがよく似た振る舞いをしているが、それはメソッド内部にもつ異なる値に基づいている場合
    • その異なる値をパラメータとして受け取るメソッドを一つ作成する

オブジェクトそのものの受け渡し

  • あるオブジェクトから複数の値を取得し、それらの値をメソッド呼び出しのパラメータとして渡している場合
    • 代わりにオブジェクトそのものを渡す
  • 依存関係の構造を乱すのであれば、「オブジェクトそのものの受け渡し」を適用すべきではない

メソッドによるパラメータの置き換え

  • あるオブジェクトがメソッドを呼び出し、その戻り値を別のオブジェクトのパラメータとして渡している。そのメソッドは受信側でも呼び出すことができる場合
    • パラメータを削除し、受信側にそのメソッドを呼び出させる
  • もしメソッドが、パラメータで渡される値を別の方法で取得できるならば、そのように修正すべき

パラメータオブジェクトの導入

  • 本来まとめて扱うべきひとかたまりのパラメータがある場合
    • それらをオブジェクトに置き換える
  • 開始日・終了日、上限値・下限値といった、範囲を示す一組の値は、Rangeパターン[Fowler AP]といったものを代わりに使うようにする

Factory Methodによるコンストラクタの置き換え

  • オブジェクトを生成する際に、単純な生成以上のことをしたい場合
    • ファクトリメソッドを使って、コンストラクタを置き換える
  • 「Factory Method によるコンストラクタの置き換え」に着手する最もわかりやすい動機は、 サブクラス化することによってタイプコードを置き換えたい場合

ダウンキャストのカプセル化

  • メソッドが返すオブジェクトを、呼び出し側でダウンキャストする必要がある場合
    • ダウンキャスト処理をメソッド内に移動する
  • ダウンキャストはやむを得ないかもしれないが、できる限り少なくするべき

例外によるエラーコードの置き換え

  • エラーを示す特別なコードをメソッドがリターンしている場合
    • 代わりに例外を発生させる

条件判定による例外の置き換え(p.315)

  • 例外を発生させているが、本来は呼び出し側が先にチェックすべきである場合
    • 最初に条件判定をするように呼び出し側を修正する

継承の取り扱い

フィールドの引き上げ

  • 2つのサブクラスが同じフィールドを持っている
    • そのフィールドをスーパークラスに移動する
  • 重複しているか調べる方法は、フィールドを調べて、他のメソッドからどのように使われているか理解すること
    • 同じように使われていれば、汎化できる

コンストラクタ本体の引き上げ

  • 複数のサブクラスに内容がほとんど同一のコンストラクタがある場合
    • スーパークラスのコンストラクタを作成して、サブクラスから呼び出す
  • このリファクタリングが複雑になる場合は、Factory Methodによるコンストラクタの置き換え(p.304)を検討する

サブクラスの抽出

  • あるクラスの特定のインスタンスだけに必要な特性がある場合
    • その一部の特性を持つサブクラスを作成する
  • サブクラスの抽出の有力な代替案としては、「クラスの抽出」がある
    • これは委譲と継承のどちらを使うかの選択
  • 一般的には「サブクラスの抽出」の方がより簡単で すが、これには制􏰂があります。オブジェクトがいったん生成されたあとは、クラスベース訳注(class-based)の振る舞いを変えることはできません。「クラスの抽出(p.149)」を行えば、異 なるコンポーネントを差し替えるだけで、クラスベースの振る舞いを簡単に変更できます。ま たサブクラスを使う場合は、1 種類のバリエーションだけしか表現できません。複数の異なる 方法でクラスを変化させたい場合には、1 つを除いた残りすべてに対して委譲を使う必要があります。

スーパークラスの抽出

  • 似通った特性を持つ2つのクラスがある場合
    • スーパークラスを作成して、共通の特性を移動する
  • 代替案として「クラスの抽出(p.149)」もあります。これは本質的には、継承と委譲の選択 です。継承は、2 つのクラスがインタフェースに加えて振る舞いも共有している場合に簡単な 選択となります。間違った選択をした場合には、あとで「委譲による継承の置き換え(p.352)」 を適用できます。

インターフェースの抽出

  • 複数のクライアントが、あるクラスのひとかたまりのインターフェースを使っている。または2つのクラス間でインターフェースの一部が共通である場合
    • その共通部分をインターフェースとして抽出する
  • 「インタフェースの抽出(p.341)」は共通のインタフェースを抽出しますが、共通の コードは抽出しません。そのため「インタフェースの抽出(p.341)」は、「重複したコード」の 臭いを引き起こしがちです。この問題は、「クラスの抽出(p.149)」を適用し、振る舞いをコン ポーネント化して委譲することで緩和できます。もし共通の振る舞いがかなりの量になるなら ば「スーパークラスの抽出(p.336)」の方が簡単ですが、スーパークラスは 1 つだけしか宣言 できません。

階層の平坦化

  • スーパークラスとサブクラスにさほど大きな違いがない場合
    • それらを合わせてまとめる

Template Methodの形成(p.345)

  • 異なるサブクラスの2つのメソッドが、類似の処理を同じ順序で実行しているが、各処理は異なっている場合
    • 元のメソッドが同一になるように、各処理を同じシグネチャのメソッドにする。そしてそれらを引き上げる

委譲による継承の置き換え

  • サブクラスがスーパークラスの一部のインターフェースだけを使っている、あるいはデータを継承したくない場合
    • スーパークラス用のフィールドを作成して、メソッドをスーパークラスに委譲するように変更した上で、継承をやめる。

継承による委譲の置き換え

  • 委譲を使っていて、すべてのインターフェースに対する単純な委譲をたくさん書いている場合
    • 委譲元のクラスを、委譲先のクラスのサブクラスにする
  • まず、委譲先の クラスのメソッドをすべて使っているわけではない場合、「継承による委譲の置き換え」を適用すべきでない
    • サブクラスはスーパークラスのすべてのインタフェースには常に従うべきだから
  • 「仲介人の除去(p.160)」を適用して、クライアント自身に委譲呼び出しを行わせることもできる
  • また「スーパークラスの抽出(p.336)」を適用して、共通のイン タフェースを分離し、その新しいクラスから継承する方法もあります。同様に「インタフェー スの抽出(p.341)」も適用できる

第12章 大きなリファクタリング

継承の切り離し

  • 1つの継承階層で、2つの仕事をまとめて行っている場合
    • 2つの継承階層を作成し、委譲を使って一方から他方を呼び出す
  • 2つの仕事をしている1つの継承階層を特定することは容易
    • もし階層の特定レベルで、すべてのサブクラスが同じ形容詞から始まる名前のサブクラスを持っていたら、1つの階層で2つの仕事をしている可能性が高い
  • 一度に1ステップずつリファクタリングをするほうが、10ステップ先のすでに単純化されたデザインに飛びつくよりも安全

第15章 部品から全体へ

  • リファクタリングは学習可能な技法
    • 目標の選別に慣れる
      • 不吉な臭いの元を断ち、問題を取り除いて、目標に進む
      • プログラムを分かりやすくするもの
    • 自信がなくなったらやめる
      • 目標に進んでいくと、自分のしていることがプログラムのセマンティックを保っていると、自分や他人にきちんと説明できなくなる
        • そこがやめ時
    • 引き返す
      • 構成が正しいことがわかっている最新のところまで戻る
      • 再度、1つずつ変更し、変更が終わるたびにテストを行う
    • デュエットで
      • 誰かと組んでやってみる

EC2+Squidでプロキシサーバを立てて、複数IPでアクセス元を分散させる

スクレイピングをする場合、同じアクセス元だとBanされたりすることがあるようで、なるべくコストを抑えつつプロキシサーバを運用したいと相談されたので、AWSで試してみることにしました。

前提

VPCとサブネットがすでに作成されている前提で書いていきます。 もし、この2つを作成していない場合は作成してください。

手順

EC2

まず、EC2作成します。AMIは Amazon Linux 2 AMI (HVM), SSD Volume Type - ami-00d101850e971728dで、インスタンスタイプは t2.medium を使いました。

理由は、インスタンスタイプによって、インターフェースあたりのIPv4アドレスを追加できる数に制限があるためです。 t2.mediumだと6つ追加できるようです。

※ 自動割り当てパブリックIPを有効にしてください

Elastic Network Interface

ネットワークインターフェースを作成します。 サブネットには既存のサブネットを指定し、セキュリティグループも指定します。作成していなければ、作成してください。

作成したネットワークインターフェースを先ほど作成したEC2にアタッチします。

eth1に新しいIPを割り当てる

またインスタンスの一覧に戻って、作成したインスタンスを指定した後、 アクション > ネットワーキング > IPアドレスの管理を開きます。

f:id:ryskit:20190601211900j:plain

そのあと、eth1側に新しいIPを5つ追加します。

f:id:ryskit:20190601212311j:plain

追加したら更新します。

Elastic IPを割り当てる

Elastic IPを5つ割り当てます。 スコープはVPCで構いません。

Elastic IPを関連付ける

先ほど、eth1に新しいIPを割り当てたかと思いますが、 そのIPに対してElastic IPを関連づけていきます。

Elastic IPを選択し、アクション > アドレスの関連付け をクリックします。

f:id:ryskit:20190601220307j:plain

リソースタイプはネットワークインターフェースを選択し、 ネットワークインターフェースは、作成したものを選択します。

プライベートIPは、eth1で新しく割り当てたセカンダリプライベートIPを選んで、関連付けを5つ行ってください。

f:id:ryskit:20190601221518j:plain

セキュリティグループのインバウンドのルールを追加する

Squid(プロキシサーバー)にアクセスする際のポートをインバウンドのルールに追加しておきます。 今回、タイプはカスタムTCP、ポートは 4578、ソースは カスタム 0.0.0.0/0 を設定しました。

Squidをインストールする

EC2にSSHで接続して以下のコマンドでSquidをインストールします。

sudo yum install -y squid

これでSquidがインストールされます。

Squidの設定ファイルを書き換える

アクセス元を分散するように設定ファイルを書き換えます。

sudo vim /etc/squid/squid.conf

以下のように書き換えました。

#
# Recommended minimum configuration:
#

# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 10.0.0.0/8 # RFC1918 possible internal network
acl localnet src 172.16.0.0/12  # RFC1918 possible internal network
acl localnet src 192.168.0.0/16 # RFC1918 possible internal network
acl localnet src fc00::/7       # RFC 4193 local private network range
acl localnet src fe80::/10      # RFC 4291 link-local (directly plugged) machines

acl SSL_ports port 443
acl Safe_ports port 80      # http
acl Safe_ports port 21      # ftp
acl Safe_ports port 443     # https
acl Safe_ports port 70      # gopher
acl Safe_ports port 210     # wais
acl Safe_ports port 1025-65535  # unregistered ports
acl Safe_ports port 280     # http-mgmt
acl Safe_ports port 488     # gss-http
acl Safe_ports port 591     # filemaker
acl Safe_ports port 777     # multiling http
acl CONNECT method CONNECT

#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access deny !Safe_ports

# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports

# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access deny manager

# We strongly recommend the following be uncommented to protect innocent
# web applications running on the proxy server who think the only
# one who can access services on "localhost" is a local user
#http_access deny to_localhost

#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#

# Example rule allowing access from your local networks.
# Adapt localnet in the ACL section to list your (internal) IP networks
# from where browsing should be allowed
http_access allow localnet
http_access allow localhost

# And finally deny all other access to this proxy
#http_access deny all

# Squid normally listens to port 3128
#http_port 3128

# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /var/spool/squid 100 16 256

# Leave coredumps in the first cache dir
coredump_dir /var/spool/squid

#
# Add any of your own refresh_pattern entries above these.
#
refresh_pattern ^ftp:       1440    20% 10080
refresh_pattern ^gopher:    1440    0%  1440
refresh_pattern -i (/cgi-bin/|\?) 0 0%  0
refresh_pattern .       0   20% 4320

http_access allow all

client_persistent_connections off
server_persistent_connections off

acl balance random 1/5
balance_on_multiple_ip on

http_port 4578

visible_hostname unknown
forwarded_for off
request_header_access X-Forwarded-For deny all
request_header_access Via deny all
request_header_access Cache-Control deny all
reply_header_access X-Forwarded-For deny all
reply_header_access Via deny all
reply_header_access Cache-Control deny all

max_filedesc 65535

tcp_outgoing_address 192.168.xx.xxx balance
tcp_outgoing_address 192.168.xx.xxx balance
tcp_outgoing_address 192.168.xx.xxx balance
tcp_outgoing_address 192.168.xx.xxx balance
tcp_outgoing_address 192.168.xx.xxx balance

tcp_outgoing_address 192.168.xx.xxx balance で書いているIP部分は、EC2のセカンダリプライベートIPの5つ書いてください。

f:id:ryskit:20190601214628j:plain

※注意: http_access allow all でアクセスを許可するクライアントをすべて許可しているため、ここはよしなに変更してください。

Squidを起動する

以下のコマンドで起動します。

sudo systemctl start squid

自動起動の設定もするのであれば、以下も実行してください。

sudo systemctl enable squid

試してみる

実行するたびに あなたのIPアドレス(IPv4) の部分がランダムに切り替わっていたら成功です🎉

curl --proxy http://EC2の自動割り当てパブリックIP:4578 http://www.ugtop.com/spill.shtml

最後に

いかがでしたでしょうか?

これらの作業を自動化すれば、必要なときにプロキシーサーバーを立てて、 必要ないときはすべてのリソースを解放しておけばお金もかからないので、 VPSでプロキシサーバーを立てて置いとくより安くなるかもしれません。

自動化までやったら、またこれ関連の記事でも書こうと思います。

P.S. 作成したリソースはすべて削除しましょう! お金かかっちゃうので!