2018年12月8日 星期六
rspec test protocal (interface) - part 2 - define our own DSL for rspec
接續上一篇 https://everyday1percent.blogspot.com/2018/05/rspec-test-protocal-interface.html
現在我們要做一個客製化的 rspec matcher,達到的效果如下:
```ruby
RSpec.describe FooService do
describe "interfaces" do
specify do
expect(FooService).to has_implemented_interfaces :call, :call!
end
end
end
```
可以先參考 rspec 的 custom matcher 應該就可以做到了
https://relishapp.com/rspec/rspec-expectations/docs/custom-matchers/define-a-matcher-supporting-block-expectations
如果要做成 gem 的話可以參考 rspec-expectations 開放的 protocal
https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/matcher_protocol.rb
https://makandracards.com/makandra/662-write-custom-rspec-matchers
https://github.com/kucaahbe/rspec-html-matchers
2018年12月7日 星期五
Ruby instance_eval & block
```rb
class Test
def initialize(&block)
@name = "Hello World"
instance_eval(&block) if block_given?
end
def name
@name
end
end
```
這樣是完全可以的,因為在 block 裡面的 `self` 變成了 `Test` instance
```rb
Test.new do
puts name
end
# => Hello World
```
2018年7月22日 星期日
不要在 rake task 裡定義 method
因為 rake task 的 scope 是 global 的,如果在裡面定義 method 等於是為 Object class 定義 private method,污染全部的 code
可行的做法之一是把要跑的 task 寫成 service 執行
另外推薦的做法是使用 `Rake::DSL`
```ruby
# lib/tasks/bicycle.rake
class BicycleTasks
include Rake::DSL
def initialize
namespace :bicycle do
task :assemble do
bicycle = Bicycle.new
# Assemble the bicycle:
attach_wheels(bicycle)
attach_handlebars(bicycle)
attach_brakes(bicycle)
end
end
end
private
def attach_wheels(bicycle)
# ...
end
def attach_handlebars(bicycle)
# ...
end
def attach_brakes(bicycle)
# ...
end
end
# Instantiate the class to define the tasks:
BicycleTasks.new
```
https://supergood.software/dont-step-on-a-rake/
2018年6月10日 星期日
用 Ruby proc 做 success and error handler, like JS Promise
```rb
# A base class for all classes implement calls to API.
class ApiCall
attr_reader :params
def self.call(params)
new(params).call
end
def initialize(params)
@params = params
end
def call
@res = execute
self
end
def on_success
yield @res if @res.success
self
end
def on_error
yield @res unless @res.success
self
end
private
def execute
fail NotImplementedError
end
end
StripeCall.(number: 'valid')
.on_success { |response| puts response.body }
.on_error { |response| puts response.body }
# => ok response
StripeCall.(number: 'invalid')
.on_success { |response| puts response.body }
.on_error { |response| puts response.body }
# => bad response
```
感覺有點借鑑 JS promise 的做法,傳 on_success 和 on_failure 的 proc 進到 method 當作 handler,之前有想過這種做法,剛好在 Ruby weekly 看到有人示範了,的確是減少了 if else 判斷,也省略了 response 這種 hidden context,不過最後為了寫的方便一點用了 method(:handle_success) 就沒有特別喜歡,但還是學習了這個用法,又是 meta programing 流派...
原文: https://railsguides.net/conditional-execution-with-dsl/
另外在 comment 裡看到一個 gem https://github.com/apneadiving/waterfall
也是挺有趣的,真的很有 functional programing 的味道
是不是大家開始慢慢被影響導致越來越多人用 Funtional Ruby 了呢~?最近也常常聽到和看到有人要用 Ruby 實現以及使用 elixir pipe operator XD
標籤:
Design Pattern,
Ruby
2018 Ruby Kaigi 網路上看到的懶人整理
- Matz considering alias yied_self with then
- Rib, an alternative for irb
- Stripe team building Ruby type checker
https://blog.bigbinary.com/2018/05/31/rubykaigi-2018-day-one.html
http.rb replace net::http
作者認為 Net::Http 雖然是 standard lib 之一,但 API 設計很不好,底層實作也有很多覺得不滿的地方,包含 Net::Http 的 Error 封裝方式,讓人不知道怎麼 rescue
有機會可以試用看看
https://github.com/httprb/http
https://twin.github.io/httprb-is-great/
2018年5月30日 星期三
ruby proc 可以傳default 值since ruby 1.9
所以
scope :latest, ->(size=5) { recent.limit(size) }
這樣是可以的
好廢的一篇文
呵呵
scope :latest, ->(size=5) { recent.limit(size) }
這樣是可以的
好廢的一篇文
呵呵
2018年5月21日 星期一
一篇講 OO cohesion 和 coupling 簡單易懂的文章
一直想找個好方式跟同事解釋怎麼 decoupling 和 design better code,這篇文章涵蓋了最重要的兩個概念而且用很簡單易懂的 code 解釋了,很不錯。
cohesion: class 裡的 methods 是不是 share 一樣的 context?
coupling: dependency
http://www.rubyguides.com/2018/05/ruby-cohesion-and-coupling/
cohesion: class 裡的 methods 是不是 share 一樣的 context?
coupling: dependency
http://www.rubyguides.com/2018/05/ruby-cohesion-and-coupling/
標籤:
Design Pattern,
OOP,
Ruby
2018年5月19日 星期六
新的做 soft delete 的 gem - discard
https://github.com/jhawthorn/discard
沒有 default 加上 default scope 我覺得是很棒的決定,這個 gem 留給開發者更多的彈性實作 soft delete,我覺得在大多數的 case 下都是很夠用了,相較之下一樣是 soft delete 的 gem paranoia 就太多 magic 了
2018年5月18日 星期五
檢查 gem verison 的方法
在devise 裡看到的
```rb
def self.rails51? # :nodoc:
Rails.gem_version >= Gem::Version.new("5.1.x")
end
def self.activerecord51? # :nodoc:
defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new("5.1.x")
end
```
2018年5月9日 星期三
rspec test protocal (interface) - part 1 - define what's interface and find it
通常我們會希望繼承於 Base class 底下的 class 會有相同的 interface,for example, 每個 service 都要有一個 #call 的 instance method,應該要有個很簡單的方法來對這些 services 測試是否有 implement 這個 protocal,理想情況如下:
```rb
class BaseService
def call
raise "NotImplemented"
end
end
class FooService < BaseService
def call
puts "called"
end
end
class BarService < BaseService
end
module Callable
def call
puts "called from module"
end
end
class IncludeService
include Callable
end
class PrivateService
private
def call
puts "private call"
end
end
RSpec.describe FooService do
describe "interfaces" do
specify do
expect(FooService).to has_interface :call
expect(FooService).to has_implemented_interface :call
end
end
end
```
it_has_interface => 有 respond_to 這個 method 就行 (個人覺得沒什麼用...)
it_has_implemented_interface => 有自己 implement (排除母 class) 或是 include method 進來
it_has_implemented_interface 希望達到的效果是:
FooService 和 IncludeService 會 passed
BarService 和 PrivateService 會 failed
來看看要怎麼樣做 :D
已知使用 respond_to? 是不行的,因為母 class 定義了 call,所以
```ruby
2.4.4 :013 > f = FooService.new
=> #<FooService:0x00007fed06152098>
2.4.4 :014 > b = BarService.new
=> #<BarService:0x00007fed06148700>
2.4.4 :015 > f.respond_to?(:call)
=> true
2.4.4 :016 > b.respond_to?(:call)
=> true
```
用 method_defined? 當然也不行,因為都是繼承於 BaseService 所以都有 defined,但來試來試一下
```ruby
2.4.4 :017 > FooService.method_defined?(:call)
=> true
2.4.4 :018 > BarService.method_defined?(:call)
=> true
```
我們要找的是一個方法檢測子 class 是否有定義這個 method,不管是不是用 mixin 的方式加進去的,有幾個 methods 可以用
* public_methods
* instance_methods
* public_instance_methods
instance_methods default 還會包含 protected 的 methods,不如我們先來試試看 public_instance_methods
```ruby
2.4.4 :030 > FooService.public_instance_methods.include?(:call)
=> true
2.4.4 :031 > BarService.public_instance_methods.include?(:call)
=> true
2.4.4 :032 > IncludeService.public_instance_methods.include?(:call)
=> true
2.4.4 :033 > PrivateService.public_instance_methods.include?(:call)
=> false
```
看起來還是不行,讓我們換個方法,用 public_methods(false)
```ruby
2.4.4 :068 > FooService.new.public_methods(false).include?(:call)
=> true
2.4.4 :069 > BarService.new.public_methods(false).include?(:call)
=> false
2.4.4 :070 > IncludeService.new.public_methods(false).include?(:call)
=> false
2.4.4 :071 > PrivateService.new.public_methods(false).include?(:call)
=> false
```
結果 public_methods(false) 沒辦法讓 include 進來的 method 包含在裡面,為什麼呢?可以看一下 IncludeService 的 ancessor
```
2.4.4 :072 > IncludeService.ancestors
=> [IncludeService, Callable, Object, Kernel, BasicObject]
```
可以想像成 include 其實就是在 parent class 以及 child class 中間插隊,所以也是算在 ancestors 裡面。
所以看起來沒有任何 built-in 的 method 可以直接做到這個功能,那不如我們就自幹一個看看吧?XD
由於要 support module 裡的 methods 所以想到最原始的方法就是去找 ancestors 裡在 superclass 之下的每個 instance_methods, code example:
```ruby
class Utils
class << self
def get_public_instance_methods_without_parent_class(klass)
countable_ancestors = klass.ancestors[0...klass.ancestors.index(klass.superclass)]
countable_ancestors.flat_map do |ancestor_class|
ancestor_class.public_instance_methods(false)
end.uniq
end
end
end
```
這個版本目前只能對instance method 做檢測,不過對我們來說已經足夠了,以後再想辦法擴充,接下來就是來研究自定義 rspec DSL 的部分。
2018年4月26日 星期四
同時定義 class method 和 instance method 的潮潮寫法
You can delegate method to your `class` since your class is also a object in Ruby, fanny~
```ruby
class Foo
def self.bar
return "Hello"
end
delegate :bar, to: :class
end
```ruby
class Foo
def self.bar
return "Hello"
end
delegate :bar, to: :class
end
```
```ruby
Foo.bar
# => "Hello"
Foo.new.bar
# => "Hello"
```
Awesome.
為子 class 寫 class method 在 class 裡調用
老生常談的小技巧了,剛好看到 kickstarer 的一段 code,貼過來回溫一下
```ruby
# Define attributes to be serialize in the `data` column.
# It generates setters and getters for those.
#
# Example:
#
# class MyEvent < Events::BaseEvent
# belongs_to :post
#
# data_attributes :title, :description
# end
#
# MyEvent.create!(post: post, title: "Hello", description: "", metadata: {})
def self.data_attributes(*attrs)
@data_attributes ||= []
attrs.map(&:to_s).each do |attr|
@data_attributes << attr unless @data_attributes.include?(attr)
define_method attr do
self.data ||= {}
self.data[attr]
end
define_method "#{attr}=" do |arg|
self.data ||= {}
self.data[attr] = arg
end
end
@data_attributes
end
```
https://gist.github.com/pcreux/d094affd957a336af4f59b85f6ec0e6d
little funny learn from ruby private
I usually make my private methods like this:
```
class Foo
private
def private_method
end
end
```
However, there is inline-style private method like this:
```
class Foo
private def private_method
end
def puiblic_method_yo!
end
end
```
Additionally, attr_reader is also available for inline-style private method:
```
class Foo
private attr_reader :bar
end
```
So fancy, isn't it? lol
```
class Foo
private
def private_method
end
end
```
However, there is inline-style private method like this:
```
class Foo
private def private_method
end
def puiblic_method_yo!
end
end
```
Additionally, attr_reader is also available for inline-style private method:
```
class Foo
private attr_reader :bar
end
```
So fancy, isn't it? lol
kickstarter 的 event sourcing approach
也解釋了一些 event sourcing 相關的東西,還不錯的深入淺出好文,而且還有 gif 圖加分XD
這張就很棒的解釋了他們所謂的 Aggregation (綠色, state), Reactor (黃色), Calculator (藍色箭頭), Event (藍色方塊)
https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224
這張就很棒的解釋了他們所謂的 Aggregation (綠色, state), Reactor (黃色), Calculator (藍色箭頭), Event (藍色方塊)
https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224
標籤:
Design Pattern,
Ruby
2018年4月22日 星期日
Rspec aggregate_failures syntax
意思就是在 aggregate_failures block 底下任一個的 expectation 有 error 時會針對每個 expectation 顯示 error 的 message
example
```ruby
it "returns false if any of balance, locked, saving is negative" do
negative_balance_account = build(:account, balance: -1)
negative_locked_account = build(:account, locked: -1)
negative_saving_account = build(:account, saving: -1)
aggregate_failures "testing all senarios" do
expect(negative_balance_account.has_negative_asset?).to be true
expect(negative_locked_account.has_negative_asset?).to be true
expect(negative_saving_account.has_negative_asset?).to be true
end
end
```
假設這個 test failed 了,那 console 的訊息會是像這樣:
```
1) Account#has_negative_asset? returns false if any of balance, locked, saving is negative
Got 2 failures from failure aggregation block "testing all senarios".
# ./spec/models/account_spec.rb:15:in `block (3 levels) in <top (required)>'
1.1) Failure/Error: expect(negative_locked_account.has_negative_asset?).to be true
expected true
got false
# ./spec/models/account_spec.rb:17:in `block (4 levels) in <top (required)>'
1.2) Failure/Error: expect(negative_saving_account.has_negative_asset?).to be true
expected true
got false
# ./spec/models/account_spec.rb:18:in `block (4 levels) in <top (required)>'
```
會顯示哪幾個 test failed 了
當有很多 test case 很相近其實可以寫在一個senario 裡時用 aggregate_failures 會讓 test 變得更簡潔,但要小心不要亂用,該分開的 test 還是用 it 分開比較好。
https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures
2018年4月3日 星期二
PaperTrail whodunnit find object (ex: find User object)
PaperTrail::Verison object 的 methods 可以去 PaperTrail::VersionConcern 裡面查
查了一下沒有可以找到 whodunnit object 的方法,只好自己加一個:
```rb
#initializers/paper_trail.rb
PaperTrail::Version.class_eval do
def whodunnit_object
User.find_by(id: whodunnit)
end
end
```
http://blog.nrowegt.com/papertrail-polymorphic-whodunnit/
2018年2月24日 星期六
看到一個不錯的 mock example (RSpec)
之前一篇文章提到 RSpec 盡量不用 let, 那要用啥?可以用 method。
另外看到一個不錯的 mock example, 用 tap 來 return double 但是 yield Authentictor
```rb
def stub_authenticator(auth_hash)
double('authenticator').tap do |authenticator|
Authenticator.stub(:new).with(authenticator).and_return(authenticator)
end
end
def stub_find_user(attributes = {})
build_stubbed(:user, attributes).tap do |user|
User.stub(:find).with(user.to_param).and_return(user)
end
end
```
https://apidock.com/ruby/Object/tap
另外看到一個不錯的 mock example, 用 tap 來 return double 但是 yield Authentictor
```rb
def stub_authenticator(auth_hash)
double('authenticator').tap do |authenticator|
Authenticator.stub(:new).with(authenticator).and_return(authenticator)
end
end
def stub_find_user(attributes = {})
build_stubbed(:user, attributes).tap do |user|
User.stub(:find).with(user.to_param).and_return(user)
end
end
```
https://apidock.com/ruby/Object/tap
from https://thoughtbot.com/upcase/videos/rspec-best-practices
Mutation in Ruby, it tells why more and more people like pure functional (immutable function)
Some mind blown examples:
```rb
[1] pry(main)> list = Array.new(5, "foo")
# => ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> list[4].clear
# => ""
[3] pry(main)> list
# => ["", "", "", "", ""]
```
因為每個 default foo 都是同一個 object
以及
```rb
[1] pry(main)> master_list = Hash.new([])
# => {}
[2] pry(main)> master_list[:evens] << 2
# => [2]
[3] pry(main)> master_list[:odds] << 3
# => [2, 3]
[4] pry(main)> master_list
# => {}
[5] pry(main)> master_list[:evens] <<= 4
# => [2, 3, 4]
[6] pry(main)> master_list
# => {:evens=>[2, 3, 4]}
[7] pry(main)> master_list[:odds] <<= 5
# => [2, 3, 4, 5]
[8] pry(main)> master_list
# => {:evens=>[2, 3, 4, 5], :odds=>[2, 3, 4, 5]}
```
一樣的原理,每個 default array 都是同一個 object
一個避免這樣的做法是用 proc 確保每個 default 值是不同 object,例如:
```rb
Array.new(5) { "foo" }
Hash.new { [] }
```
ex:
```rb
[1] pry(main)> Array.new(5) { "foo" }
=> ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> a = Array.new(5) { "foo" }
=> ["foo", "foo", "foo", "foo", "foo"]
[3] pry(main)> a[2].clear
=> ""
[4] pry(main)> a
=> ["foo", "foo", "", "foo", "foo"]
[5] pry(main)>
```
另外還有更有趣的「以你為 freeze 了是吧?Boom~!」
```rb
[1] pry(main)> CONSTANT = ["foo"]
# => ["foo"]
[2] pry(main)> CONSTANT << "bar"
# => ["foo", "bar"]
[3] pry(main)> CONSTANT.freeze
# => ["foo", "bar"]
[4] pry(main)> CONSTANT << "baz"
# RuntimeError: can't modify frozen Array
# from (pry):4:in `__pry__'
[5] pry(main)> CONSTANT[1] << "baz"
# => "barbaz"
[6] pry(main)> CONSTANT
# => ["foo", "barbaz"]
```
這是因為 freeze 是 freeze CONSTANT 這個 Array 的 object,沒有 freeze 裡面的 string,所以 string 還是 mutable
(Ice Nine gem for deep freezing)
為了拯救 developers 於水深火熱的加班 debug 地獄中,不需要 mutate 的時候就不要 mutate,寫每個 function 都想一下「這真的有必要 mutate 嗎?」
```rb
[1] pry(main)> list = Array.new(5, "foo")
# => ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> list[4].clear
# => ""
[3] pry(main)> list
# => ["", "", "", "", ""]
```
因為每個 default foo 都是同一個 object
以及
```rb
[1] pry(main)> master_list = Hash.new([])
# => {}
[2] pry(main)> master_list[:evens] << 2
# => [2]
[3] pry(main)> master_list[:odds] << 3
# => [2, 3]
[4] pry(main)> master_list
# => {}
[5] pry(main)> master_list[:evens] <<= 4
# => [2, 3, 4]
[6] pry(main)> master_list
# => {:evens=>[2, 3, 4]}
[7] pry(main)> master_list[:odds] <<= 5
# => [2, 3, 4, 5]
[8] pry(main)> master_list
# => {:evens=>[2, 3, 4, 5], :odds=>[2, 3, 4, 5]}
```
一樣的原理,每個 default array 都是同一個 object
一個避免這樣的做法是用 proc 確保每個 default 值是不同 object,例如:
```rb
Array.new(5) { "foo" }
Hash.new { [] }
```
ex:
```rb
[1] pry(main)> Array.new(5) { "foo" }
=> ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> a = Array.new(5) { "foo" }
=> ["foo", "foo", "foo", "foo", "foo"]
[3] pry(main)> a[2].clear
=> ""
[4] pry(main)> a
=> ["foo", "foo", "", "foo", "foo"]
[5] pry(main)>
```
另外還有更有趣的「以你為 freeze 了是吧?Boom~!」
```rb
[1] pry(main)> CONSTANT = ["foo"]
# => ["foo"]
[2] pry(main)> CONSTANT << "bar"
# => ["foo", "bar"]
[3] pry(main)> CONSTANT.freeze
# => ["foo", "bar"]
[4] pry(main)> CONSTANT << "baz"
# RuntimeError: can't modify frozen Array
# from (pry):4:in `__pry__'
[5] pry(main)> CONSTANT[1] << "baz"
# => "barbaz"
[6] pry(main)> CONSTANT
# => ["foo", "barbaz"]
```
這是因為 freeze 是 freeze CONSTANT 這個 Array 的 object,沒有 freeze 裡面的 string,所以 string 還是 mutable
(Ice Nine gem for deep freezing)
為了拯救 developers 於水深火熱的加班 debug 地獄中,不需要 mutate 的時候就不要 mutate,寫每個 function 都想一下「這真的有必要 mutate 嗎?」
2018年1月30日 星期二
Ruby Array(params[:ids])
关于 Array(params[:ids] 这个用法,如果是 Array([1,2,3]) 会等同于 [1,2,3] 没变,但是 Array[nil] 会变成 [] 空数组,这可以让 .each 方法不会因为 nil.each 而爆错。如果不这样处理,在没有勾选任何 offer 就送出的情况,就会爆出 NoMethodError 错误。除非你额外检查 params[:id] 如果是 nil 就返回,但不如用 Array 来的精巧。
来自 ihower 大大在全栈营教材中的 Pro tips
訂閱:
文章 (Atom)
