『オブジェクト指向設計実践ガイド』 実践編
はじめに
この記事は『オブジェクト指向設計実践ガイド』の内容をもとにテストコード書いてたらよくわかんなくなったので助けてくれ、という内容です。だれか助けてください。コメントとかRe:記事とかのリアクションあると嬉しいです!
本の内容の抜粋
詳細は買って読んでね、ということでざっくりと。
- 「Dependency Injectionでテストしやすくしよう」
- 依存オブジェクトの注入*1ってやつだよ
- サンプルコード参照
- 「送信コマンドメッセージはテストダブルを使うとよい」
- コマンド: 副作用がある
- 送信メッセージ: テスト対象のメソッドから呼ばれてる他のメソッド
- 要は副作用があるなら副作用を呼び出せてるかはチェックすべきってこと
サンプルコード
サンプルコードの数値リテラルは適当な値なのでどんなギアだよって感じになってます。すみません。
# DependencyInjection 適用前 class Wheel attr_reader :rim, :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim * (tire * 2) end end class Gear attr_reader :ratio, :rim, :tire def initialize(ratio: nil, rim: nil, tire: nil) @ratio = ratio @rim = rim @tire = tire end def gear_inches ratio * Wheel.new(rim, tire).diameter end end Gear.new(ratio: 1, rim: 3, tire: 2) # 値はてけとー
# DependencyInjection 適用後 class Gear attr_reader :ratio, :wheel def initialize(ratio: nil, wheel: nil) @ratio = ratio @wheel = wheel end def gear_inches ratio * wheel.diameter end end Gear.new(ratio: 1, wheel: Wheel.new(3, 2)) # ここでDependency = Wheelを注入!
んで、これをやっておくとテストで Wheel
クラスを参照しなくて済みます。『参照しなくて済む』というのは『GearのテストがWheelに依存しな』くなるということです。
# 旧 class GearTest < Minitest::Test # 受信メッセージのテスト def test_calculates_gear_iniches gear = Gear.new(ratio: 1, rim: 3, tire: 2) assert_equal(12, gear.gear_inches) # gear_inchesの中でWheel#diameterに依存 end end # 新 class DiameterDouble # テストダブルの導入 def diameter 6 # 固定値で返すとバグらなくてあんしん end end class GearTest < Minitest::Test def test_calculates_gear_iniches gear = Gear.new(ratio: 1, wheel: DiameterDouble.new) # WheelじゃなくてDouble使う assert_equal(12, gear.gear_inches) # 実行してもWheelはどこにも必要とされてない = 依存していない ので変更につよい end end
ほんとはこれ以降もダブルをどう扱うかとかが続くんですけどこのへんで。
実践してみた
タイトルが実践入門ですからね、実践していきます。
雰囲気を出すためにそれっぽいコードを書きます。記事をファイルに保存するような処理を考えましょう。
もらった文字列の1行目をタイトルとし、他を本文としてファイルに書き出す処理です。
vs クラスメソッドへの依存
DI前のコードがこちら。
module FileCreator def self.create(title, text) # いろんな処理とか File.write(title, text) end end class Article attr_reader :title, :body def initialize(body) @title, @body = body.split("\n", 2) end def save FileCreator.create(title, body) end end
Article#save
は明らかに FileCreator.craete
に依存していますね。DIしましょう*2。
DIする
DI版にしてみたのがこちら。
class Article attr_reader :title, :body, :creator def initialize(body, creator: FileCreator) @title, @body = body.split("\n", 2) @creator = creator end def save creator.create(title, body) end end
でもこのコード、2点気になるところが。
- creatorの値をほぼ変える予定がない場合にオプション引数取れる実装は、今後の変更に備えすぎてるように見える
- KISSとかYAGNIとかそういう感じで
- 変更する予定ないくらいに密結合になってるならテストでもそのままでいいような気がする
- でも副作用は気になる……
- 拡張性じゃなくてテスタビリティのための実装だと思えば仕方ない気もする……。
- クラスをインスタンス変数にとるの、結構キモくないですか?
DIはしないけど継承 + オーバーライドでなんとかしてみる
あるいはこういう感じにメソッドにしてみるとか。
class Article attr_reader :title, :body def initialize(body) @title, @body = body.split("\n", 2) end def save creator.create(title, body) end def creator FileCreator end end # テスト時にはオーバーライドしてどうにかする class ArticleDouble < Article def creator Class.new { def self.create; end } end end
うーん、まあ、ギリギリ……?テスト用のクラスができちゃうの、あんまり好きじゃないけど……。
スタブ使う
違うアプローチとして、アプリケーションコードをDIでよくするんじゃなくてスタブ使ってみるとか。
class ArticleTest < Minitest::Test def test_save article = Article.new(<<~EOS) たいとるでーす 本文でーす 2行目でーす EOS expected_args = [ 'たいとるでーす', "本文でーす\n2行目でーす\n" ] creator = MiniTest::Mock.new.expect(:call, nil, expected_args) FileCreator.stub(:create, creator) do article.save end assert(creator.verify) end end
これがすっきりしてていいけど、テスト側に思いっきり FileCreator
って書かれてるのがうーん……。Dependency減らしたいんじゃなかったっけ?
まとまらないまとめ
- クラス同士の依存が明らかな場合、スタブがよさそう
- つまりFileCreator以外のcreatorが存在しそうにないとき
- テストコードにクラス名がおもいっきり書かれるけど増えるかわかんないんだしいいんじゃないの
- creatorが多数存在するとき
- DIにするのがよさそう……なのか?
- クラスオブジェクトをインスタンス変数にもちたくない問題がー
- BaseArticleとFileArticleとDatabaseArticleと、みたいにして
#creator
をそれぞれ実装しちゃうのがよさそう- テストはArticleDoubleを作ってそこにスタブいれちゃう
- オーバーライド版 + スタブ版って感じ
- テスト用のクラスができちゃうのは仕方ない
- DIにするのがよさそう……なのか?
あるいはなんかいい方法があるんでしょうか?実際のアプリケーションの成熟具合とも関連すると思いますがご意見お待ちしております 🙋
2017/02/06 04:24追記
クラスオブジェクトをインスタンス変数にもちたくない問題がー
これたぶん基本的には問題なくてArticleのインスタンスはFileCreatorよりも寿命が短いことが想定されるから。
DIにするのもそんなに高コストではないのでDI化しておいていいんじゃないのってのがいまの結論です
2017年1月振り返り
はやいものでもう2017年の1/12が終了したんですね*1。2016年振り返りで書いたように、定量的な指標を導入してみたのでそれを使って振り返りをやろうと思います。
定量的な指標というのはwakatimeで計測した時間です。wakatimeはコーディングしているときの時間を計測してくれるサービスです。
自分が最近何やってるかな、というのの振り返りにはもってこいですね。
プロジェクト名を書いても伝わらないので言語別の指標での振り返りを毎月やっていくことにします。
記録
2017年1月
コーディング時間: 3724分
言語比率 1〜5位
YAML 601 Ruby 388 ERB 254 Go 233 JavaScript 201
total: 8135 minutes
訂正版 Rank Name Minutes 1 Ruby 1658 2 YAML 1511 3 Go 1018 4 JavaScript 326 5 ERB 321
2017-03-15追記: 普通に計算ミスってました。 -> 懺悔 - Smoky God Express
雑感
記録から
えーと、アホみたいにYAMLをいじってますがこれはインフラをやっていたせいです。最近はAWSでDockerを走らせるには、というのをいろいろ試してます。CloudFormationの設定ファイルを書き、hakoの設定ファイルを書き、docker-compose.ymlを書き、circle.ymlを書き。試行錯誤をひたすら繰り返していたらこうなりました。得意言語はYAMLです(キリッ*2。ERBもひたすら多いですがこれも設定ファイルのテンプレートとしてERB使ってたからですね。
Rubyは、まあ普通にアプリケーション書いてたので妥当。
GoとJavaScriptは振り返り用のツールでGo + Vue.jsのwebアプリを作ったのでそれの影響ですね。
総コーディング時間が3724分 = 60時間ちょいなのは、なんか少ない気がする。
AWSの画面でイベント見つめたり、ドキュメント読んだり、どうするかミーティングしたりというのは平常より多めだった気がする。
あと日別で見ると完全にやる気を失っている日が何回かあって、なるほどという感じ。
こういうのをなくしていきたいのでアラートとかいれたほうがいいのかな。
「昨日n分しかコード書いてませんけど大丈夫ですか?」的な。
やったこと
ざっと思い出していきます
- 振り返り用webアプリ書いた
- google slides apiについて調べてある程度使えるようになった
- cloudformationに詳しくなった
- awsでのNW構築ができるようになった
- hako + ECSあるいはEBを使ったデプロイができるようになった
- hakoは割と難しいと思うのでなんか記事書きたい
こんなかなー。
やったことはほんとに忘れていくので週次くらいでサクサク書いてったほうがいいかもしれない。
『ゼロから作るDeep Learning』を読んだ
年末にやるかと思って買って、買った直後に3日ほど寝込んで結局昨日までかかってしまった。
といっても正月明けてから読んだのはたぶんトータル2時間程度で完全にサボってただけです。
内容は、ゼロから作るとあるだけあって、ほんとにゼロから作ったし、内容も初歩の初歩からでかなり助かった。大学入試以来数学から遠ざかっていたので懐かしさがある。ちなみにどうでもいいが大学入試の数学はセンターも二次もコケた記憶があるのであんまり思い出したくない。
と書いていたらそういえば経済学部では統計もミクロ経済も数式書いてたことを思いだしてきた。ウッ頭が……
1章のPython入門では、お決まりの環境構築から。
個人的にPythonには苦手意識があって、virtualenvだのなんだのというのがよくわかってない。
ただpyenv + condaでの環境構築はかなりスムーズにいって助かった。
numpyも初めて存在を知ったがこれは便利だなーという感じ。
本書に出てきた使い方以外にもたくさんありそうなので別途調べたほうがよさそう。
2〜6章は機械学習部分を作っていくぜーという感じ。
全結合レイヤのみで畳み込みは7章へ。
特に面白かったのはlearning rateの調整の話かな。
このへんはノートで板書をとっていたのだけど、途中でDropbox paperに変えた。
Tex記法で数式書けたりして楽しい。
8章はざっといろんな事例紹介という感じで読みとばしておしまい。
実際になにかやる段になったらもう一度読んだほうがいいかもしれない。
読み終わったので「よっしゃ、じゃあ俺も機械学習やってみっか」という気持ちではあるがMNISTのような使いやすい形になっているデータが世の中にどれくらいあるのだろうと思ってつらさを感じている。 まだCNNの感覚とかつかめてない気がするしChainerとかtensorflowとか聞いたことあるぜってやつらのチュートリアルに手をだすのがいいのかなー。
Slackのスレッド機能をどう使うか
待望のSlackのThread機能きましたね。みなさんつかってますか。
Slackはチャットツールとして手軽でヨサがあったもののフロー型の情報でありその運用過程にはいろいろ問題がありました。
スレッド機能の登場は僕らの頭を悩ませていたそれらの課題をいくらか解決してくれるものと思われました。
- 同時に複数の話題が流れたりするとどれへのリプなのかわからない
- ちょっとタイミング逃すと返信しにくい
- 議論が発散しがち*1
しかし、実際つかってみるとどーもなんか違う感じがあります……。
いや、デフォでスレッド内だけに発言されても……開くのめんどいし……。スレッドの開始位置どこだっけってなるし……。
なんかどうやってつかえばいいのかなあという感じでなかなか難しいなと個人的には思ってました。
そんなスレッド機能ですが今日試しに個人の作業ログとして残してみたらいい感じになりました。
こんな感じ(さっきやっつけで作ったイメージ図*2
いままで時間のログとか、やることリスト、やったことリストとかをSlackの分報内で実現しようとして流れて発散しがちだったんですが、これなら続くかも?と思ってます*3
スレッド機能をいい感じに運用できてるチームがありましたらどう使ってるか是非共有してほしいですね。
冒頭にも書きましたけど、みなさんつかってます?どうやって使うといい感じですか?
RubyでEnumerableを条件Xを満たすものと満たさないものに排他的に分けたいんだけど
どうするのがいいんでしょうか。
メモリとかパフォーマンスとかそういうのはあんまり気にしない前提です。
以下のサンプルコードでは対象のEnumerableは変数 arr
に代入されているものとし、条件Xを満たすかどうかのメソッドは foo?(x)
という名前であるとします*1。
普通に書く
a = arr.select { |e| foo?(e) } b = arr.select { |e| !foo?(e) }
排他的に分けたいのにブロックが2回書かれている。
条件に変更があった場合の変更漏れ、うっかり !
つけ忘れるなどのミスをやりそう。微妙。
selectとrejectする
a = arr.select { |e| foo?(e) } b = arr.reject { |e| foo?(e) }
おんなじブロックを使うからまだミスが減る気はする。が、微差だろう。
引き算する
a = arr.select { |e| foo?(e) }
b = arr - a
排他的っぽい。割とよい
まとめてみる
tmp = arr.group_by { |e| foo?(e) } a = tmp[true] b = tmp[false]
hash[true]
がそこはかとなくキモい気がする……。
調べてみる
a, b = arr.partition { |e| foo?(e) }
普通にあった*2
参考: https://docs.ruby-lang.org/ja/latest/class/Enumerable.html#I_PARTITION
結論
リファレンス見ろ
--pathつきでbundleしたときのGemfile内でrequireすると指定したpathからロードしようとしてくる
背景
やんごとなき事情によりGemfile内で色々することになり*1そのためのgemを作って使うことになった。 ローカルではうまくいったけどCI環境にいれてみるとどーもうまくいかない。
事象その1
状況
# Gemfile source "https://rubygems.org" require 'awesome_gem'
$ bundle # 詳細わすれたけどそんなgemねえ系のエラー
解決策
先にグローバルにgem installしておけばいいんじゃね?
$ gem install awsome_gem $ bundle # => OK
事象その2
状況
Gemfileは同じ。
$ gem install awsome_gem $ bundle install --path vendor/bundle # 詳細わすれたけどそんなgemねえ系のエラー
--path
を指定するとまたもrequireでコケる
これはなぜかというと、--path
オプションを指定した場合、Gemfileを読み込む時点でrequireでロードするパスがオプションの値になってるっぽい。
だからグローバルにインストールしておいてもそれをrequireしてくることができない
解決策
$ mkdir tmp $ cd tmp $ echo <<EOS > Gemfile source "https://rubygems.org" gem 'awesome_gem' EOS $ bundle install --path ../vendor/bundle $ cd ../ $ rm -rf tmp $ bundle install --path vendor/bundle
要は別Gemfileでvendor/bundleに突っ込んでおけばよい。
こんなことでハマる人、たぶんそうそういないと思うけどメモ。
*1:いいプラクティスではない気がしている
自宅の無線LAN環境について
最初に言い訳しておくとNW全然詳しくないので嘘を書いているかもしれませんし、これは日記なので解決したとかそういう話ではないです。
ウチの無線LAN環境には不満がある。長く繋いでると、ときどき「新しいコネクションが張れなくなる」気がする。
いま言っている事象は具体的には、skypeなどの通話は途切れずそのまま続行できるが、その間に新しくwebページを開くとかgit pushするとかの操作をするとNWにつながりません的なエラーを吐く。ブラウザの場合、大体DNSでの解決に失敗しましたと出るがDNSの問題ではなさそうだ。
macの上のバーにある無線LAN状況を示すやつだと、バリ4*1で、ちゃんと接続していることになっている。しかし上述の状態になる。謎い。
電器屋のおねーさんに相談したら「それ老朽化っすよ老朽化。無線LANルータ側の受信部が老朽化してそういうことになります。新しいのどっすか」という返答だった。ふーん、ありがとうございますと思いながらその日は帰って、ラズパイを新しい無線LANルータにしてみることで様子をみてみた。
結果、ラズパイのNWは非常に安定していて、何の問題もなく使えたので確かにこれはどうも無線LANルータ本体の問題なのかもしんないっすねえという気持ちが高まっていた。
というわけで家人に買い替えどうすかと聞いてみたところたぶん変えてから1年経ってないとのこと。うーん、じゃあ老朽化じゃないんか?というかそもそも俺と同じ事象起きてる?と聞くと全然起きてないっていう話に。えー、みんな我慢強いなって思ってたら俺だけかよ。受信部の老朽化なら全員に影響ありそうなんだけどそこんところどーなんすか。
うーん、まあとりあえず今のがどんくらい古いのか調べてみますか、と思って型番検索して某ねだん.com見てたら、クチコミでファームウェア品評会してる人が。はー、熱心な方ですね。中の人か?
と、ここでファームウェアアップデート、全然してなくね?と気づく。
調べてみたら現行は1.xでどーも2.x系が出てるっぽい。よっしゃこれはあげてみるしかありませんねと思ってあげてみたのがついさっき。うまくいくといいなあ。
そういえば無線LANの設定画面が見たいからIP調べたくて、 arp -a
したIP全部調べてみたけど出てこなかった。
BUFFALO製品はStationRadarなるスマホアプリでLAN内の製品調べられるみたいだからこれつかってみたらさっきの一覧にはないIPで出てきた。ふーん。
そんでファームウェアアップデートした後にもっかい arp -a
叩いたら出てくるようになった。なんでやねん。
*1:たぶんこれ白黒で電波の強さを示してると思っているけど合ってますか?