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

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. 作成したリソースはすべて削除しましょう! お金かかっちゃうので!

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

最近、友達と毎週メタプログラミングRubyの読書会をやっています。

というのも、友達が転職して🎉、業務でRubyを使い始めることになり、自分も業務で少し使っているので、お互い知識を共有しつつ学びがあれば良いなと思って始めました。

序盤の4章ぐらいまでは特に難しいとは思わなかったんですが、define_methodやevalなどを使って動的にメソッドを定義したり、クラスマクロを作ったりするところはやはりメタプロ脳が足りないのか、少し難しく感じました。

普段、JavaやScalaを使っているので、ここまで自由に書けるのも面白いけど、業務のコードでメタプログラミングしまくると保守が大変そう...というイメージ。

でも、ライブラリに手を入れたり、少しだけここのコードを書き換えたいみたいなときは便利なんだろうな。

まさに、「大いなる力には大いなる責任が伴う」ですね。

内容について

手書きの図を使って、継承チェーンやメソッド探索の動きを説明してくれていたり、出てくるキーワードは理解しやすい言葉で説明されています。

ところどころクイズもあって、解きながら読み進めると理解も深まるし、本としてはおもしろく読めると思います。

Rubyの言語仕様をざっくり理解したあとに読むと良いんじゃないかな。

www.amazon.co.jp

ノート

2章 月曜日: オブジェクトモデル

  • 共通のクラスを持つオブジェクトは、メソッドも共通している。つまり、メソッドはオブジェクトではなく、クラスに存在する
  • レシーバー
  • 継承チェーン Class -> Module -> Object -> Kernel -> BasicObject
  • ancestors メソッド
  • include, extend, prepend
  • included, extended, prepended
  • Object クラスが Kernelモジュールをインクルードしているので、すべてのオブジェクトの継承チェーンにKernelモジュールが挿入されている
  • https://github.com/awesome-print/awesome_print
  • self
  • クラスやモジュールの定義の内側(メソッドの外側)では、selfの役割はクラスやモジュールそのものになる。
  • Refinements
    • モジュール を書いてから、モジュール定義のなかで refine を呼び出す
    • 変更を反映にするには、using メソッドを使って明示的に有効にする必要がある。
    • Refinements が有効になるのは 2 箇所だけ
      • refine ブロックそのものと
      • using を呼び出し た場所からモジュールの終わりまで(モジュール定義にいる場合)またはファイルの終わりまで(トップレベルにいる場合)た
  • Kernel.private_instance_methods.grep(/^print/) // => [:printf, :print]

3章 火曜日:メソッド

  • メソッドを呼び出すということは、オブジェクトにメッセージを送っていること。
  • sendメソッド
    • どんなメソッドも呼び出せて、privateでさえも呼び出せる
  • define_method
    • 動的にメソッドを定義できる
  • data_source.methods.grep(/^get(.*)info$/) { Computer.define_component $1 }
  • メソッド呼び出しを method_missing に集中させ、ラップしたオブジェクトに転送する
  • respond_to_missing?
    • respond_to? は、respond_to_missing? というメソッドを呼び出している
  • const_missing
  • ゴーストメソッドの名前と、継承した本物のメソッドの名前が衝突すると、後者が勝ってしまう
  • こうした最小限のメソッドし かない状態のクラスをブランクスレートと呼ぶ。Ruby には、最初からこのブランクスレートが用意されている。
  • undef_method
    • メソッド呼び出しへのレスポンスを止める
  • remove_method
    • 現在のクラスからメソッドを削除する
      • 継承元にある場合は呼ばれる

4章 水曜日: ブロック

  • ブロックはメソッドに渡され、メソッドは yield キーワードを使ってブロックをコールバックする。
  • メソッドの内部では、Kernel#block_given? メソッドを使ってブロックの有無を確認できる。
  • スコープゲート
    • def, class, module
    • Class => Class.new
      • 変数を共有できるように => フラットスコープ
  • define_method
def define_methods
  shared = 0
  Kernel.send :define_method, :counter do
    shared
  end
  
  Kernel.send :define_method, :inc do |x|
    shared += x
  end
end

define_methods
counter # => 0 inc(4)
counter # => 4
  • instance_eval
  • instance_eval に渡したブロックは、レシーバを self にしてから評価されるので、レシーバの private メソッドや @v などのインスタンス変数にもアクセスできる。また、他のブロックと同じよ うに、instance_eval を定義したときの束縛も見える。
  • ブロックを評価するためだけにオブジェクトを生成することもある。このようなオブジェクトは、クリーンルームと呼ばれる。
    • クリーンルームにはメソッドもインスタンス変数もあまり増やさないほうがいい。ブロックに伴う環境のメソッドやインスタンス変数と名前が衝突する可能性があるからだ。クリーンルームとして使うには、BasicObject のインスタンスが最適である。
  • コードを保管できるところは少なくとも3つある
    • Proc <- ブロックオブジェクト
    • lambda <- Procの変形
    • メソッドのなか

Procオブジェクト

inc = Proc.new {|x| x + 1}
inc.call(2)

この技法は、あとで評価と呼ばれるもの。
  • ブロックをProcに変換する2つのカーネルメソッド
    • lambda
    • proc
dec = lambda {|x| x - 1}
dec.class # => Proc
dec.call(2) # => 1
p = ->(x){ x - 1} # => lambda {|x| x - 1} と同じ

「矢印ラムダ」演算子で lambda を生成する方法
  • yieldでは足りないケース
    • 他のメソッドにブロックを渡したいとき
    • ブロックをProcに変換したいとき
  • &をつけると「メソッドに渡されたブロックを受け取って、それをProcに変換したい」という意味になる
    • &をつけなければ、Prcoのままになる
  • Proc をブロックに戻したいときも & を使う
  • Proc と lambda には 2 つの違いがある。
    • ひとつは、return キーワードに関すること
    • もうひとつは、引数のチェックに関すること

return キーワードの違い

def double(callable_object)
  callable_object.call * 2
end

l = lambda { return 10 }

double(l) # => 20
def another_double
  p = Proc.new { return 10 }
  result = p.call
  return result * 2 # ここまで来ない!
end

another_double # => 10

引数のチェックの違い

  • 一般的に lambda のほうが Proc(や普通のブロック)よりも引数の扱いに厳しい。違った項数で lambda を呼び出すと、ArgumentError になる。Procは引数列を期待に合わせてくれる。
p = Proc.new {|a, b| [a, b]}
p.call(1, 2, 3) # => [1, 2]
p.call(1) # => [1, nil]

UnboundMethod

  • UnboundMethod は、元のクラスやモジュールから引き離されたメソッドのようなものである。Method を UnboundMethod に変換するには、Method#unbind を呼び出す。

5章 木曜日: クラス定義

  • 特異クラス(別名: シングルトンクラス)
  • Rubyのプログラムは、常にカレントオブジェクトselfを持っている。それと同様に、常にカレントクラス(あるいはカレントモジュール)を持っている
  • カレントクラスを変更するには、classキーワード以外の方法が必要
  • Module#class_eval メソッド
  • Module#class_evalBasicObject#instance_eval とはまったく別物
    • instance_eval はselfを変更するだけだが、class_evalはselfとカレントクラスを変更する
  • クラスインスタンス変数
    • アクセスできるのはクラスだけであり、クラスのインスタンスやサブクラスからはアクセスできない
class MyClass
  @my_var
end
  • クラス変数
    • サブクラスや通常のインスタンスメソッドからもアクセスできる
class Loan
  def initialize(book)
    @book = book
    @time = Loan.time_class.now
  end
  
  def self.time_class
    @time || Time
  end
  
  def to_s
    ...
  end
end

特異メソッド

  • 単一のオブジェクトに特化したメソッドのことを 特異メソッド という
  • クラスメソッドはクラスの特異メソッド
def object.method
  # メソッドの中身
end

上記のobjectの部分は、オブジェクトの参照、クラス名の定数、selfのいずれかが使える

クラスマクロ

  • attr_*族メソッドはModuleクラスで定義されているので、selfがモジュールであっても、クラスであっても使える。
  • attr_accessorのようなメソッドはクラスマクロと呼ぶ
class Book
  def title #...
  def subtitle #...
  def lend_to(user)
    puts "Lending to  #{user}"
    # ...
  end
  
  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end
  
  deprecate :GetTitle, :title
  deprecate :LEND_TO_USER, :lend_to
  deprecate :title2, :subtitle

特異クラス

  • あなたが見ているクラスとは別に、オブジェクトは裏に特別なクラスを持っている
    • それが特異クラスと呼ばれるもの
class << an_object
  # << あなたのコードをここに
end

特異クラスの参照を取得したければ、スコープの外にselfを返せばよい

obj = Object.new
singleton_class = class << obj
  self
end

singleton_class.class # => Class

"abc".singleton_class # => #<Class::#<String:0x331df0>
  • インスタンスを一つしか持てない(だから、シングルトンクラスと呼ばれる)
  • 継承ができない
  • オブジェクトが特異メソッドを持っていれば、特異クラスのメソッドから探索をはじめる

Rubyオブジェクトモデルの7つのルール

  1. オブジェクトは 1 種類しかない。それが通常のオブジェクトかモジュールになる。
  2. モジュールは 1 種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
  3. メソッドは 1 種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
  4. すべてのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
  5. すべてのクラスは(BasicObject を除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスが BasicObject に向かって 1 本の継承チェーンを持っている。
  6. オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう。もっと早く。図 5-5「特異クラスと継承」を見返せば、納得できるはずだ)。
  7. メソッドを呼び出すときは、Ruby はレシーバの本物のクラスに向かって「右へ」進み、継承チェーンを「上へ」進む。Ruby のメソッド探索について知るべきことは以上だ。

クラス拡張

  • Object#extend
    • レシーバの特異クラスにモジュールをインクルードするためのショートカット
module MyModule
  def my_method; 'hello'; end
end

obj = Object.new
obj.extend MyModule
obj.my_method # => 'hello'

class MyClass
  entend MyModule
end

MyClass.my_method # => 'hello'

メソッドラッパー

  • エイリアス
    • alias_method :m, :my_method
    • 新しいメソッド名を先に、古いメソッドをあとに書く
  • メソッドの再定義
    • 元のメソッドを変更することではない。新しいメソッドを定義して、元のメソッドの名前をつけること
class Integer
  alias_method :old_plus, :+
  
  def +(value) self.old_plus(value).old_plus(1)
end end

第6章 金曜日: コードを記述するコード

  • instance_eval
  • class_eval
  • Kernel#eval
  • Bindingオブジェクト
    • Bindingオブジェクトにはスコープは含まれているが、コードは含まれていないため、ブロックよりも「純粋」なクロージャーと考えることができる
    • 取得したスコープで評価するには、evalの引数にBindingを渡せばいい
class MyClass
  def my_method
    @x = 1
    binding
  end
end

b = MyClass.new.my_method
eval "@x", b # => 1
  • 定数: TOPLEVEL_BINDING
    • トップレベルのスコープのBinding
    • これを使えば、トップレベルのスコープにプログラムのどこからでもアクセスできる
class AnotherClass
  def my_method
    eval "self", TOPLEVEL_BINDING
  end
end

AnothierClass.new.my_method ## => main
  • gem: Pry
    • Bindingをうまく活用している
    • binding.pryを呼び出すと、現在のbindingでRubyのインタプリタが開かれる
      • つまり、実行中のプロセスの中
  • instance_eval, class_evalはコード文字列またはブロックのいずれかを
  • evalでコードインジェクションに気をつける
    • 自分で書いた文字列のみevalを使うように制限すれば良い
  • オブジェクトが汚染されているか
    • tainted? を使う
  • Rubyにはファイル名を受け取り、そのファイルのコードを実行するメソッドが用意されている
    • Kernel#load
    • Kernel#require

docs.ruby-lang.org

クイズ: アトリビュートのチェック(手順2)

  • クラスメソッドを定義するには、クラスのスコープに入る必要がある
    • クラスのスコープに入るには、class_evalを使えば良い

クイズ: アトリビュートのチェック(手順4)

  • attr_checkedをあらゆるクラス定義で使うには、Class, またはModuleのインスタンスメソッドにすればよい
  • self.inherited
  • include
  • prepended
  • Module#extendedをオーバーライドすれば、モジュールがオブジェクトを拡張したときにコードを実行できる
    • 以下のフックは、オブジェクトのクラスに住むインスタンスメソッドにしか使えない
      • Module#method_added
      • Module#method_removed
      • Module#method_undefined
    • 特異メソッドのイベントをキャッチするには以下を使う
      • Kernel#singleton_method_added
      • Kernel#singleton_method_removed
      • Kernel#singleton_method_undefined

クイズ: アトリビュートのチェック(手順5)

module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end
  
  module ClassMethods
    def attr_checked(attribute, &validation)
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_varialbe_set("@#{attribute}", value)
      end
      
      define_method attribute do
        instance_variable_get "@#{attribute}"
      end
    end
  end
end

MySQLのJSON型の値をGenerated Columnsを使ってカラムにデータを追加してみる

MySQLのGenerated Columnsを使う必要があり、動作が気になったので検証してみました。

Generated Columns(生成カラム)とは

Values of a generated column are computed from an expression included in the column definition. [翻訳] 生成された列の値は、列定義に含まれている式から計算されます。

つまり、Generated Columnsの値には、そのカラムを定義したときに定義した関数や式の計算結果の値が入るということ。

MySQL :: MySQL 5.7 Reference Manual :: 13.1.18.8 CREATE TABLE and Generated Columns

なるほど、もう少し手を動かして掴んでみましょう。

作業

MySQLに接続するまで

試すには、Generated Columnsが追加されたバージョン 5.7.6 以上のMySQLをインストールします。

Dockerでサクッとやる。

docker pull mysql

これで最新のMySQLのイメージを取得。

docker run --name mysql -e MYSQL_ROOT_PASSWORD=mysql -d -p 3306:3306 mysql

これでMySQLを起動。 rootのパスワードはmysql

次は、MySQLのコンテナに入る。

docker exec -it `CONTAINER ID` /bin/bash

MySQLに接続する。 パスワードはさっき書いたものを入力する。

mysql -u root -p

データベースを作成する

以下のコマンドでデータベースを作成する。

create database gc_db;

テーブルを作成する

以下のコマンドでテーブルを作成する。

create table gc_test(id int unsigned auto_increment not null primary key, json_data json);

カラムは id , json_data のみでひとまず作成する。

データを追加する

以下のコマンドでデータを追加する。

insert into gc_test(json_data) values ('{"demo": {"id": 1}}'), ('{"demo": {"id": 2}}'), ('{"demo": {"id": 3}}');

これでJSONデータを含むテーブルができました。

mysql> select * from gc_test;
+----+---------------------+
| id | json_data           |
+----+---------------------+
|  1 | {"demo": {"id": 1}} |
|  2 | {"demo": {"id": 2}} |
|  3 | {"demo": {"id": 3}} |
+----+---------------------+

次は、Generated Columnsを定義します。

Generated Columnsを定義する

今回、Generated Columnsを定義するとき、 json_data カラムの demo オブジェクト中の id を取り出すように定義します。

次のコマンドでテーブルにGenerated Columnsを追加します。

alter table gc_test add column demo_id varchar(255) GENERATED ALWAYS AS (json_extract(`json_data`, '$.demo.id')) STORED;

STORED というキーワードを書いているが、他にもVIRTUALが存在します。 デフォルトでは、VIRTUALが選択されます。

このキーワードで、生成された値をストレージに格納するかを決めることができます。

VIRTUAL

VIRTUAL の場合は、ストレージには保存されず、BEFOREトリガーの後に行が読まれたときに評価されます。

VIRTUAL: Column values are not stored, but are evaluated when rows are read, immediately after any BEFORE triggers. A virtual column takes no storage.

引用 MySQL :: MySQL 5.7 Reference Manual :: 13.1.18.8 CREATE TABLE and Generated Columns

行が読み込まれたときに評価だと、Generated Columnsの値で検索を絞り込むときにパフォーマンスは悪いのかな? そこらへんはデータを大量に用意して後日試してみたい。

STORED

STOREDの場合は、行がINSERTまたはUPDATEされたときに評価されて値が格納されます。

STORED: Column values are evaluated and stored when rows are inserted or updated. A stored column does require storage space and can be indexed.

引用 MySQL :: MySQL 5.7 Reference Manual :: 13.1.18.8 CREATE TABLE and Generated Columns

Generated Columnsを定義した後はこんな感じになります。

mysql> select * from gc_test;
+----+---------------------+---------+
| id | json_data           | demo_id |
+----+---------------------+---------+
|  1 | {"demo": {"id": 1}} | 1       |
|  2 | {"demo": {"id": 2}} | 2       |
|  3 | {"demo": {"id": 3}} | 3       |
+----+---------------------+---------+

json_dataのdemoオブジェクトの中のidが、demo_id として取り出されてカラムに格納されているのが分かりますね。

検索のパフォーマンスがどうなるのかがモヤモヤしてきたので、 そこらへんはドキュメント読むなり実際に試してみて計測したほうが良さそうだな。

以上です。

[Android] [Kotlin] Android開発でRoomとKoinの設定

今、UdemyのAndroid開発コースをやっていて、そのコースではRoomとToothPickというライブラリを使って開発が進むのですが、個人的にKoinを使いたくてドキュメント見ながらやると見事に設定の記述漏れでハマったので、備忘録として書いておきます。

Kotlinのバージョンは、1.3.20 を使用。

Module: appbuild.gradle に以下の記述を追加する。

// Room
def room_version = "2.1.0-alpha05"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// Koin
def koin_version = "1.0.2"
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-scope:$koin_version"
implementation "org.koin:koin-android-viewmodel:$koin_version"

今回、以下の設定が抜けていたためビルドは通る?けど、アプリが起動しないため原因が最初どこかわからなくて1時間ぐらい時間溶かしてしまいました。

// Room
kapt "androidx.room:room-compiler:$room_version"

ちなみに、kapt を利用するには以下の記述が必要です。

apply plugin: 'kotlin-kapt'

kaptは Kotlin Annotation Processing Tool(であってる?)で、 Java6から導入された Pluggable Annotation Processing API を Kotlinからも利用できるようにしたものです。

Koinは Applicationクラスを継承したAppクラスに設定を記述しました。

class App : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin(this,
            listOf(AppModule.module())
        )
    }

    companion object {
        lateinit var instance: Application
    }

    init {
        instance = this
    }
}

AppModulemodule() 関数内にDIの設定を記述しました。

object AppModule {
    fun module() = module {
        single<ITaskModel> { TaskLocalModel() }
        single<INoteModel> { NoteLocalModel() }
        single {
            RoomDatabaseClient.getInstance(App.instance.applicationContext)
        }
    }
}

これで合っているのかよく分からないので、間違っていたらコメントとかもらえると嬉しいです。

以上です。

UdemyのKotlin for Android O Developmentというコースが良かった!

普段はWeb開発しかしていないのだけれど、モバイルアプリを開発する必要性が出てきたので最近いろいろサンプルアプリを作ったり、ライブラリを試しに動かしたりしている。

そのいった中で、Udemyの「Kotlin for Android O Development」というコースが個人的に総合して良いと思ったので、良かった点・気になった点に分けて感想をまとめてみる。

良かった点

Kotlinを使ったAndroidの開発コースであること

このコースはKotlinを使って1からサンプルアプリを開発していくため、KotlinでAndroidアプリを開発していきたいと思っていた僕にはちょうどよかった。

Kotlinで書かれた書籍はいつくかあるもののまだまだJavaで説明されているものも多いので、Udemyのような動画で学ぶことには意味がある。

サンプル数が多い

なにか新しい言語や仕組みを学ぶときは本を読むのも良いが、実際にものを作って動かしてから書籍を読んだ方が個人的には理解力が高まると思っているので、とにかくサンプルアプリを作りまくりたかった。

このコースでは、10個以上のサンプルアプリを作ることが可能なので、とりあえず手を動かして覚えたい人におすすめしたい。

DBを使ったサンプルがある

モバイルアプリを開発するにあたってDBを使うことは多々あるのではないかと思う。

このコースでは簡単ではあるがSQLiteを使ってアプリを作ることもできるので良かった。

Firebaseを使ったアプリも作れる

コースの総まとめとして、今まで作ってきたサンプルアプリの機能を駆使して、そしてFirebaseも利用してアプリを作ることができる。

実際に使ってみると分かるが、Firebase簡単すぎっ!!ってなる。

少し気になったのは、コースで使っているFirebaseのライブラリのバージョンが古いので、 deprecated になっているメソッドを呼び出していたりしていた。

僕がアプリを作ったときは最新バージョンを使うようにしていたので、値がうまく取れないなと思ったら上記のような理由が原因だった。Firebaseドキュメントや他の方の記事を参考にすれば解決できるはずだ。

Android でファイルをアップロードする  |  Firebase

気になった点

ListViewのみを使っていた

おそらくBeginner向けのコースであるからだと思うが、RecyclerViewもコースに盛り込めばいいのにと思った。 おそらく普段Androidアプリを開発されている方はListViewとか使うのか?と思ったりしたのでここらへんは気になった。

Fragmentは扱われていない

普通はAcitivityのみを使って開発は行わないと思うのでFragmentについてのチャプターもあってもいいんじゃないかと思った。 このコースではFragmentは学べないため、記事を見るなり本を読むなりして学ぶのが良いだろう。

最後に

Kotlin for Android O Development: From Beginner to Advanced

このコースは講師の方が英語で説明するため、英語字幕を出しながら動画を視聴していた。話すスピードはゆっくりで難しいことは話さないので、そんなに英語が得意でない僕でも理解できたのでおすすめである。

RecyclerViewやCardViewはAndroid Developersの記事を読むのが良さげ。

developer.android.com

まだまだ知らないといけない部分があるので、このまま継続してAndroid開発の学習・開発を進めていく。

おすすめの記事やこれだけはやっておけみたいなのがあれば、 ブログのコメントかTwitterに投げつけてくださると嬉しいです!

では!

AWS CloudFrontのキャッシュを削除する方法

静的ページをCloudFront + S3でホストしていて画像を更新したけど、キャッシュのせいで反映されないということがありそうです。

そんなときにキャッシュの有効期限まで待つのではなく、自分でキャッシュを削除して反映させたいはず。

この記事では、AWSのCloudFrontでどうやってキャッシュを削除するかを知ることができます。

では、いってみましょう。

ファイルを無効化

CloudFrontにはInvalidationというものがあり、これをCreateすることでキャッシュを削除することができます。 Edgeサーバーにあるファイルを無効化するからInvalidation。

手順

1. コンソールで [CloudFront Distributes]を開く。

f:id:ryskit:20190209171501j:plain

2. [ディストリビューション] を選択する。

選択したディストリビューションの概要が表示されます。

f:id:ryskit:20190209171840j:plain

3. [Invalidations] タブを選択する。

f:id:ryskit:20190209171915j:plain

4. [Create Invalidation] ボタンをクリックする。

f:id:ryskit:20190209172213j:plain

ボックスの中にキャッシュを削除したいファイルのパスを入力してください。

画像のように記述すると、 /images/xxxx.jpg のキャッシュを削除することになります。

まとめてキャッシュを削除したいときは、 /* のような指定をするとすべてキャッシュを削除できます。

5. [Invalidate] ボタンをクリックする。

選択すると、CloudFrontのEdgeサーバーのキャッシュ削除処理が開始されます。 Status が In Progress の場合はまだ削除処理中で、 Complete になれば完了です。

こちらにAWSの公式ドキュメントがあるので、詳細を知りたい場合は参考にしてみてください。

Databindingを利用したアプリをビルドするとUnresolved reference: BR エラーになった

Androidアプリの開発を始めて、Databindingが便利そうというかMVVMなどのアーキテクチャで実装しようとすると必須?みたいなので、とりあえず簡単なサンプルアプリを実装して、どんなものか簡単に実装を理解しようとした。

アプリを書き終わったのでビルドしようとすると Unresolved reference: BR というエラーに出くわしてしまった。

結論から言うと、

(Module: app) build.gradle に apply plugin: 'kotlin-kapt' の記載を書いたら解決した。

経緯

Googleのドキュメントを見る限り、 (Module: app) build.gradleに、

android {
    ....
    dataBinding {
        enabled = true
    }
}

を書くだけでデータバインディングの準備はOKなのかなと思っていた。

そのため、以下のようにViewHolderクラスで ViewDataBinding に変数をセットするコードを書いてビルドしてみるとエラーになった。

class WeatherViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {

    fun bind(data: Any) {
        binding.setVariable(BR.data, data) // ここで kotlin compiler にUnresolved reference: BR と表示された
        binding.executePendingBindings()     
    }
}

どうやら、BRクラスファイルの参照解決ができてないということで、 うーんとうなりながらぐぐってみると以下の記事に行き着きました。

medium.com

どうやらこいつが必要らしい。

apply plugin: 'kotlin-kapt'

ということで、build.gradleに記載してビルドしてみるとすんなりエラーなくビルドできました。

いろいろ昔の記事を見ていると、

kapt 'com.android.databinding:compiler:x.x.x'

を書いてる記事とかあるんだけど、これはもう書かなくて良いっぽい。

最後に

不慣れなことをすると、すぐバグ踏んだり躓いたりしやすいから、早く慣れていきたい。 友達とサービス作るときに、キダくんAndroidアプリ開発で参加してよ!って声がいっぱいかかるぐらいにはスキルアップしていくぞ!