The worst Ruby codes
I’ve seen in my life
RubyKaigi 2015
@Prodis
Fernando Hamasaki de Amorim
@Prodis
• Developing web applications for 15 years
• Writing Ruby code since 2009
• Working at...
WOP
WOP
Workaround Oriented Programming
WOP
Workaround Oriented Programming is an advance
technique of software development that uses of any
kind of workaround, p...
The worst Ruby codes
I’ve seen in my life
"The names have been changed to protect the innocent."
A first example of WOP:
masking credit card
numbers
describe '#mask_credit_card' do
let(:number) { '5464193830403276' }
it 'returns masked credit card number' do
masked = mas...
def mask_credit_card(number)
limit = number.length - 4
“#{'*' * limit}#{number[limit..-1]}”
end
def mask_credit_card_wop(number)
(number.length - 4).times do |i|
number[i] = '*'
end
number
end
describe '#mask_credit_card_wop' do
let(:number) { '5464193830403276' }
it 'returns masked credit card number' do
masked =...
#mask_credit_card_wop
returns masked credit card number
does not change number variable (FAILED - 1)
Failures:
1) #mask_cr...
def mask_credit_card_wop(number)
(number.length - 4).times do |i|
number[i] = '*'
end
number
end
Unclear flow
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
if...
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
if...
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
if...
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
if...
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
if...
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
if...
How to fix it?
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
un...
class Support::DomainsController < Support::BaseController
def create
site = Site.find_by(name: params[:domain][:site])
un...
No Ruby way
class PaymentGatewayWOP
def initialize(options = {})
raise ArgumentError if options[:email].to_s.strip.empty?
raise Argume...
class PaymentGatewayWOP
def initialize(options = {})
raise ArgumentError if options[:email].to_s.strip.empty?
raise Argume...
class PaymentGatewayWOP
def initialize(options = {})
raise ArgumentError if options[:email].to_s.strip.empty?
raise Argume...
class PaymentGateway
attr_reader :email, :token
def initialize(options = {})
@email = options.fetch(:email)
@token = optio...
class PaymentGateway
attr_reader :email, :token
def initialize(options = {})
@email = options.fetch(:email)
@token = optio...
class PaymentGateway
attr_reader :email, :token
def initialize(options = {})
@email = options.fetch(:email)
@token = optio...
class PaymentGateway
attr_reader :email, :token
# constructor omitted
[:identification, :billing_type, :billing_status, :m...
class PaymentGateway
attr_reader :email, :token
# other methods omitted
def exists?
message =~ /Account found/
end
def act...
class PaymentGatewayWOP
def initialize(options = {})
raise ArgumentError if options[:email].to_s.strip.empty?
raise Argume...
class PaymentGateway
attr_reader :email, :token
def initialize(options = {})
@email = options.fetch(:email)
@token = optio...
Naming issues
class ImageWidgetImporter < WidgetImporter
def import(img_element, row_number, position)
return if img_element.blank? || i...
class ImageWidgetImporter < WidgetImporter
def import(img_element, row_number, position)
return if img_element.blank? || i...
class ImageWidgetImporter < WidgetImporter
def import(img_element, row_number, position)
return if img_element.blank? || i...
class ImageWidgetImporter < WidgetImporter
def import(img_element, row_number, position)
return if img_element.blank? || i...
class ImageWidgetImporter < WidgetImporter
# other public methods omitted
private
def create_image_widget(img_element, row...
class ImageWidgetImporter < WidgetImporter
# other public methods omitted
private
def create_image_widget(img_element, row...
class ImageWidgetImporter < WidgetImporter
# other public methods omitted
private
def create_image_widget(img_element, row...
OOP
OOP
Inheritance for the purpose
of code reuse
class Installation::FromFeed < Installation::FromBase
def install(args)
# implementation omitted
end
end
class Installatio...
class Installation::FromBase
include Rails::LabeledLog::Logging
attr_writer :customers_api, :installer, :mailer
def instal...
How to fix it?
module Installation::Infra
include Rails::LabeledLog::Logging
attr_writer :customers_api, :installer, :mailer
def customer...
class Installation::FromFeed
include Installation::Infra
def install(args)
# implementation omitted
end
end
class Installa...
OOP
Inheritance mistake
DNS
A quick introduction
class WsDns
attr_reader :host, :user, :timeout
def initialize(options)
@host = options[:host]
@user = options[:user]
@time...
class CnameWsDns
attr_reader :ws_dns, :zone, :content
def initialize(options)
@ws_dns = WsDns.new(options)
@zone = options...
class AWsDns < CnameWsDns
protected
def type
'A'
end
end
class TxtWsDns < CnameWsDns
protected
def type
'TXT'
end
end
How to fix it?
OOP
Parent knowing your children
class TransactionResponseParser
attr_reader :xml
def initialize(xml)
@xml = xml
end
def parse
# omitted implementation
end...
class ResponseParser
attr_reader :xml
def initialize(xml)
@xml = xml
end
def parse
# omitted implementation
end
# omitted ...
class TransactionResponseParser < ResponseParser
private
# specific transaction methods omitted
end
class AccountResponseP...
class ResponseParser
def self.transaction?(xml)
xml.include?('<transaction>')
end
def self.get_parser(xml)
ResponseParser....
How to fix it?
module ResponseParserFactory
def self.build(xml)
if xml.include?('<transaction>')
TransactionResponseParser.new(xml)
else
...
The worst class
class DomainChecker
extend Memoist
DOMAIN_REGEXP = /^[a-z0-9]+(-[a-z0-9]+)*(.[a-z0-9]+(-[a-z0-9]+)*)+$/
attr_accessor :dom...
Business scenario for
DomainChecker class
class DomainChecker
# ...
def check_new
# omitted implementation
end
def status
# omitted implementation
end
memoize :stat...
class DomainChecker
extend Memoist
attr_accessor :domain
def initialize(args = {})
@domain = args[:domain]
end
# ...
end
class DomainChecker
extend Memoist
# ...
def check_new
check_existing
end
def check_existing
return external_check if exte...
class DomainChecker
extend Memoist
attr_accessor :domain
def initialize(args = {})
@domain = args[:domain]
end
# ...
end
def status
if dns_adapter.ns_locaweb?
a_entry_locaweb = dns_adapter.a_entry_locaweb
if a_entry_locaweb == AppConfig.ip_lvs...
def dns_adapter
DnsAdapter.new(domain: CGI.escape(domain))
end
memoize :dns_adapter
def domain_checker_result
domain_check...
def get_token
WsAuthentication.new(AppConfig.wsauthentication.url).authenticate(AppConfig.
wsauthentication.user, AppConfi...
def internal_check
url = "#{AppConfig.registro_service_url}/domain_availability/
#{domain}/internal_check"
JSON(http_get(u...
class DomainChecker
extend Memoist
attr_accessor :domain
def initialize(args = {})
@domain = args[:domain]
end
# ...
end
def status
if dns_adapter.ns_locaweb?
a_entry_locaweb = dns_adapter.a_entry_locaweb
if a_entry_locaweb == AppConfig.ip_lvs...
def available_domain_by_user(user)
if domain.blank?
return {valid: false, notice: :invalid, message: :blank}
end
if !domai...
def available_domain_by_user(user)
if domain.blank?
return {valid: false, notice: :invalid, message: :blank}
end
if !domai...
def available_domain_by_user(user)
if domain.blank?
return {valid: false, notice: :invalid, message: :blank}
end
if !domai...
def available_domain_by_user(user)
if domain.blank?
return {valid: false, notice: :invalid, message: :blank}
end
if !domai...
def available_domain_by_user(user)
# …
if domain_checker_result["customer_login"].downcase == user.username.downcase
Rails...
def available_domain_by_user(user)
# …
if !domain_checker_result["valid"] &&
domain_checker_result["error"] != "unsupporte...
class DomainChecker
# ...
def check_new
check_existing
end
def check_existing
return external_check if external_check["err...
class DomainChecker
# ...
def check_new
check_existing
end
def check_existing
return external_check if external_check["err...
def external_check
url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/external_check"
begin
JSON(http_...
class DomainChecker
# ...
def http_get(url, headers = {})
Rails.logger.info "chamando GET #{url}, headers: #{headers}"
res...
class DomainChecker
# ...
def check_new
check_existing
end
def check_existing
return external_check if external_check["err...
def external_check
url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/external_check"
begin
JSON(http_...
class DomainChecker
# ...
def check_new
check_existing
end
def check_existing
return external_check if external_check["err...
class DomainChecker
# ...
def details
{
entry_a: dns_adapter.a_value,
entry_ns: dns_adapter.ns_value,
entry_cname: dns_ada...
class DomainChecker
# ...
private
def get_token
WsAuthentication.new(AppConfig.wsauthentication.url)
.authenticate(AppConf...
DomainChecker class
problems
Long class
DomainChecker class problems
Constructor with hash
parameter, but only use
one key of the hash.
DomainChecker class problems
A method implementation
that only call a private
method without parameters
DomainChecker class problems
Memoize (hell) dependency:
memoized methods used
like variables.
DomainChecker class problems
A lot ifs: ifs with ifs with
else with if with else
(it’s hard until to explain)
DomainChecker class problems
Code hard
to understand
(internal x external checks)
DomainChecker class problems
An unused
private method
DomainChecker class problems
Instance method
creates another
instance of itself
DomainChecker class problems
• Many responsibilities:
✴ Validate domain format
✴ Validate domain logic
✴ Format return to use in view
✴ Do HTTP request...
DomainChecker class
problems introduces new
patterns and principles
Write-only code
Once write no one can read
DomainChecker class introduces

new patterns and principles
Close Closed Principle
Closed for modification,
more closed for extension.
DomainChecker class introduces

new patterns and...
Inception Pattern
Where an instance of a class creates
a new instance of itself and aggregates

the new instance state to ...
DomainChecker class probably
is the worst Ruby class
I've seen in my life
How did we fix it?
Why is WOP applied?
Lack of knowledge
Causes of WOP
Immaturity in software
development
Causes of WOP
No collaborative
environment
Causes of WOP
No coaching
Causes of WOP
Tight deadlines
Causes of WOP
Why simplify if you
can complicate?
Causes of WOP
To use "cool things"
because they are cool, even
if they are not a solution.
Causes of WOP
Some mysteries
of the human mind
(that we can't explain)
Causes of WOP
How to avoid WOP?
Read a lot
How to avoid WOP?
How to avoid WOP?
But do not learn
only Ruby
How to avoid WOP?
How to avoid WOP?
Use code review
How to avoid WOP?
Read code from
others programmers
How to avoid WOP?
Write code yourself
can read in the future
How to avoid WOP?
Participate of open source
projects: contributing,
discussing, reading.
How to avoid WOP?
Do coaching of less
experienced developers
(teaching is a good way to learn too)
How to avoid WOP?
Do not write code for you:
write it to the application,
to the team.
How to avoid WOP?
Exchange
experiences, ask.
How to avoid WOP?
Use pair programming
(not 100% IMHO)
How to avoid WOP?
Learn from your
mistakes and others
How to avoid WOP?
Face bad code as an
opportunity to get better
How to avoid WOP?
Do not face bad code
with complaints or
making fun of the authors
How to avoid WOP?
This funny code causes
waste of time, resources
and money
How to avoid WOP?
Instead, show the authors
of bad code the right way
How to avoid WOP?
Show them
the Ruby way
How to avoid WOP?
Thank you!
I hope you enjoyed
@Prodis
The worst Ruby codes
I’ve seen in my life
RubyKaigi 2015
@Prodis
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
Próximos SlideShares
Carregando em…5
×

The worst Ruby codes I’ve seen in my life - RubyKaigi 2015

14.678 visualizações

Publicada em

Video presentation: https://www.youtube.com/watch?v=jLAFXQ1Av50

Most applications written in Ruby are great, but also exists evil code applying WOP techniques. There are many workarounds in several programming languages, but in Ruby, when it happens, the proportion is bigger. It's very easy to write Ruby code with collateral damage.

You will see a collection of bad Ruby codes, with a description of how these codes affected negatively their applications and the solutions to fix and avoid them. Long classes, coupling, misapplication of OO, illegible code, tangled flows, naming issues and other things you can ever imagine are examples what you'll get.

Publicada em: Software

The worst Ruby codes I’ve seen in my life - RubyKaigi 2015

  1. 1. The worst Ruby codes I’ve seen in my life RubyKaigi 2015 @Prodis
  2. 2. Fernando Hamasaki de Amorim @Prodis • Developing web applications for 15 years • Writing Ruby code since 2009 • Working at Locaweb, the biggest hosting company in Brazil • Playing basketball at the free time
  3. 3. WOP
  4. 4. WOP Workaround Oriented Programming
  5. 5. WOP Workaround Oriented Programming is an advance technique of software development that uses of any kind of workaround, patch e all of evil a code can have. WOP is based on code duplication, redundant flows, unnecessary tasks and wheels reinvention.
  6. 6. The worst Ruby codes I’ve seen in my life "The names have been changed to protect the innocent."
  7. 7. A first example of WOP: masking credit card numbers
  8. 8. describe '#mask_credit_card' do let(:number) { '5464193830403276' } it 'returns masked credit card number' do masked = mask_credit_card(number) expect(masked).to eq '************3276' end end
  9. 9. def mask_credit_card(number) limit = number.length - 4 “#{'*' * limit}#{number[limit..-1]}” end
  10. 10. def mask_credit_card_wop(number) (number.length - 4).times do |i| number[i] = '*' end number end
  11. 11. describe '#mask_credit_card_wop' do let(:number) { '5464193830403276' } it 'returns masked credit card number' do masked = mask_credit_card_wop(number) expect(masked).to eq '************3276' end it 'does not change number variable' do mask_credit_card_wop(number) expect(number).to eq '5464193830403276' end end
  12. 12. #mask_credit_card_wop returns masked credit card number does not change number variable (FAILED - 1) Failures: 1) #mask_credit_card_wop does not change number variable Failure/Error: expect(number).to eq '5464193830403276' expected: "5464193830403276" got: "************3276" (compared using ==) # ./spec/mask_credit_card/mask_credit_card_spec.rb: 23:in `block (2 levels) in <top (required)>' Finished in 0.0202 seconds (files took 0.17324 seconds to load) 2 examples, 1 failure
  13. 13. def mask_credit_card_wop(number) (number.length - 4).times do |i| number[i] = '*' end number end
  14. 14. Unclear flow
  15. 15. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) if site.blank? flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return else domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true if domain.save flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path return else flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end end end end
  16. 16. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) if site.blank? flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return else domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true if domain.save flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path return else flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end end end end
  17. 17. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) if site.blank? flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return else domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true if domain.save flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path return else flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end end end end
  18. 18. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) if site.blank? flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return else domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true if domain.save flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path return else flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end end end end
  19. 19. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) if site.blank? flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return else domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true if domain.save flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path return else flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end end end end
  20. 20. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) if site.blank? flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return else domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true if domain.save flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path return else flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end end end end
  21. 21. How to fix it?
  22. 22. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) unless site flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return end domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true unless domain.save flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path end end
  23. 23. class Support::DomainsController < Support::BaseController def create site = Site.find_by(name: params[:domain][:site]) unless site flash[:alert] = I18n.t('support.domains.errors.without_site') redirect_to new_support_domain_path return end domain = site.domains.build(address: params[:domain][:address]) domain.support_create = true unless domain.save flash[:alert] = I18n.t('support.domains.errors.invalid') redirect_to new_support_domain_path return end flash[:success] = I18n.t('support.domains.success') redirect_to support_domains_path end end
  24. 24. No Ruby way
  25. 25. class PaymentGatewayWOP def initialize(options = {}) raise ArgumentError if options[:email].to_s.strip.empty? raise ArgumentError if options[:token].to_s.strip.empty? @options = options end def email @options[:email] end def token @options[:token] end def identification @options[:identification] end def billing_type @options[:billing_type] end def billing_status @options[:billing_status] end def message @options[:message] end def exists? @options[:message] =~ /Account found/ end def is_active? @options[:billing_status] == 'active' end def is_seller? @options[:billing_type] == 'seller' || @options[:billing_type] == 'company' end # other methods omitted end
  26. 26. class PaymentGatewayWOP def initialize(options = {}) raise ArgumentError if options[:email].to_s.strip.empty? raise ArgumentError if options[:token].to_s.strip.empty? @options = options end def email @options[:email] end def token @options[:token] end def identification @options[:identification] end def billing_type @options[:billing_type] end # other methods omitted end
  27. 27. class PaymentGatewayWOP def initialize(options = {}) raise ArgumentError if options[:email].to_s.strip.empty? raise ArgumentError if options[:token].to_s.strip.empty? @options = options end def email @options[:email] end def token @options[:token] end def identification @options[:identification] end def billing_type @options[:billing_type] end # other methods omitted end
  28. 28. class PaymentGateway attr_reader :email, :token def initialize(options = {}) @email = options.fetch(:email) @token = options.fetch(:token) @options = options end def identification options[:identification] end def billing_type options[:billing_type] end # other public methods omitted private attr_reader :options # other methods omitted end
  29. 29. class PaymentGateway attr_reader :email, :token def initialize(options = {}) @email = options.fetch(:email) @token = options.fetch(:token) @options = options end def identification options[:identification] end def billing_type options[:billing_type] end # other public methods omitted private attr_reader :options # other methods omitted end
  30. 30. class PaymentGateway attr_reader :email, :token def initialize(options = {}) @email = options.fetch(:email) @token = options.fetch(:token) @options = options end [:identification,:billing_type, :billing_status, :message].each do |method| define_method(method) do options[method] end end # other public methods omitted private attr_reader :options # other methods omitted end
  31. 31. class PaymentGateway attr_reader :email, :token # constructor omitted [:identification, :billing_type, :billing_status, :message].each do |method| define_method(method) do options[method] end end def exists? message =~ /Account found/ end def is_active? billing_status == 'active' end def is_seller? billing_type == 'seller' || billing_type == 'company' end private attr_reader :options # other methods omitted end
  32. 32. class PaymentGateway attr_reader :email, :token # other methods omitted def exists? message =~ /Account found/ end def active? billing_status == 'active' end def seller? billing_type == 'seller' || billing_type == 'company' end private attr_reader :options # other methods omitted end
  33. 33. class PaymentGatewayWOP def initialize(options = {}) raise ArgumentError if options[:email].to_s.strip.empty? raise ArgumentError if options[:token].to_s.strip.empty? @options = options end def email @options[:email] end def token @options[:token] end def identification @options[:identification] end def billing_type @options[:billing_type] end def billing_status @options[:billing_status] end def message @options[:message] end def exists? @options[:message] =~ /Account found/ end def is_active? @options[:billing_status] == 'active' end def is_seller? @options[:billing_type] == 'seller' || @options[:billing_type] == 'company' end # other methods omitted end
  34. 34. class PaymentGateway attr_reader :email, :token def initialize(options = {}) @email = options.fetch(:email) @token = options.fetch(:token) @options = options end [:identification, :billing_type, :billing_status, :message].each do |method| define_method(method) do options[method] end end def exists? message =~ /Account found/ end def active? billing_status == 'active' end def seller? billing_type == 'seller' || billing_type == 'company' end private attr_reader :options # other methods end
  35. 35. Naming issues
  36. 36. class ImageWidgetImporter < WidgetImporter def import(img_element, row_number, position) return if img_element.blank? || img_element['src'].blank? create_image_widget(img_element, row_number, position) end def import! @page.widgets.where(kind: 'text').each do |widget| content = Nokogiri::HTML(widget.content, nil, 'UTF-8') next unless has_internal_image?(content) images = content.css('img').select do |image| internal_image?(image) end images.each { |image| download_and_change_image_src(image) } widget.update_attribute(:content, content.inner_html) end end private def kind 'image' end def create_image_widget(img_element, row_number, position) widget = create(row_number: row_number, position: position, remote_image_url: img_element['src']) source = (AppConfig.assets_host + widget.image.url) widget.content = @template_adapter.render_widget_content('image', alt: '', src: source) widget.save! widget end # Create widget_image to Text Widget def create_widget_image(url) widget_image = WidgetImage.new remote_image_url: url widget_image.site_id = @page.site.id widget_image.save! widget_image end # other methods omitted end
  37. 37. class ImageWidgetImporter < WidgetImporter def import(img_element, row_number, position) return if img_element.blank? || img_element['src'].blank? create_image_widget(img_element, row_number, position) end def import! @page.widgets.where(kind: 'text').each do |widget| content = Nokogiri::HTML(widget.content, nil, 'UTF-8') next unless has_internal_image?(content) images = content.css('img').select do |image| internal_image?(image) end images.each { |image| download_and_change_image_src(image) } widget.update_attribute(:content, content.inner_html) end end private def kind 'image' end # other methods omitted end
  38. 38. class ImageWidgetImporter < WidgetImporter def import(img_element, row_number, position) return if img_element.blank? || img_element['src'].blank? create_image_widget(img_element, row_number, position) end def import! @page.widgets.where(kind: 'text').each do |widget| content = Nokogiri::HTML(widget.content, nil, 'UTF-8') next unless has_internal_image?(content) images = content.css('img').select do |image| internal_image?(image) end images.each { |image| download_and_change_image_src(image) } widget.update_attribute(:content, content.inner_html) end end private def kind 'image' end # other methods omitted end
  39. 39. class ImageWidgetImporter < WidgetImporter def import(img_element, row_number, position) return if img_element.blank? || img_element['src'].blank? create_image_widget(img_element, row_number, position) end def import_from_text_widget @page.widgets.where(kind: 'text').each do |widget| content = Nokogiri::HTML(widget.content, nil, 'UTF-8') next unless has_internal_image?(content) images = content.css('img').select do |image| internal_image?(image) end images.each { |image| download_and_change_image_src(image) } widget.update_attribute(:content, content.inner_html) end end private def kind 'image' end # other methods omitted end
  40. 40. class ImageWidgetImporter < WidgetImporter # other public methods omitted private def create_image_widget(img_element, row_number, position) widget = create(row_number: row_number, position: position, remote_image_url: img_element['src']) source = (AppConfig.assets_host + widget.image.url) widget.content = @template_adapter.render_widget_content('image', alt: '', src: source) widget.save! widget end # Create image widget to text widget def create_widget_image(url) widget_image = WidgetImage.new remote_image_url: url widget_image.site_id = @page.site.id widget_image.save! widget_image end # other methods omitted end
  41. 41. class ImageWidgetImporter < WidgetImporter # other public methods omitted private def create_image_widget(img_element, row_number, position) widget = create(row_number: row_number, position: position, remote_image_url: img_element['src']) source = (AppConfig.assets_host + widget.image.url) widget.content = @template_adapter.render_widget_content('image', alt: '', src: source) widget.save! widget end # Create image widget to text widget def create_widget_image(url) widget_image = WidgetImage.new remote_image_url: url widget_image.site_id = @page.site.id widget_image.save! widget_image end # other methods omitted end
  42. 42. class ImageWidgetImporter < WidgetImporter # other public methods omitted private def create_image_widget(img_element, row_number, position) widget = create(row_number: row_number, position: position, remote_image_url: img_element['src']) source = (AppConfig.assets_host + widget.image.url) widget.content = @template_adapter.render_widget_content('image', alt: '', src: source) widget.save! widget end def create_image_widget_to_text_widget(url) widget_image = WidgetImage.new remote_image_url: url widget_image.site_id = @page.site.id widget_image.save! widget_image end # other methods omitted end
  43. 43. OOP
  44. 44. OOP Inheritance for the purpose of code reuse
  45. 45. class Installation::FromFeed < Installation::FromBase def install(args) # implementation omitted end end class Installation::FromHosting < Installation::FromBase def install(args) # implementation omitted end end class Installation::FromMigration < Installation::FromBase def install(args) # implementation omitted end end
  46. 46. class Installation::FromBase include Rails::LabeledLog::Logging attr_writer :customers_api, :installer, :mailer def install(args) raise NotImplementedError end def customers_api @customers_api ||= CustomersApi.new end def installer @installer ||= Installation::Installer.new end def mailer @mailer ||= Installation::Mailer.new end end
  47. 47. How to fix it?
  48. 48. module Installation::Infra include Rails::LabeledLog::Logging attr_writer :customers_api, :installer, :mailer def customers_api @customers_api ||= CustomersApi.new end def installer @installer ||= Provisioner::Installation::Installer.new end def mailer @mailer ||= Provisioner::Installation::Mailer.new end end
  49. 49. class Installation::FromFeed include Installation::Infra def install(args) # implementation omitted end end class Installation::FromHosting include Installation::Infra def install(args) # implementation omitted end end class Installation::FromMigration include Installation::Infra def install(args) # implementation omitted end end
  50. 50. OOP Inheritance mistake
  51. 51. DNS A quick introduction
  52. 52. class WsDns attr_reader :host, :user, :timeout def initialize(options) @host = options[:host] @user = options[:user] @timeout = options.fetch(:timeout, 5) end def create_entry(options) # implementation omitted end def delete_entry(options) # implementation omitted end def get_entry(options) # implementation omitted end def has_entry?(options) # implementation omitted end # other methods to DNS zone end
  53. 53. class CnameWsDns attr_reader :ws_dns, :zone, :content def initialize(options) @ws_dns = WsDns.new(options) @zone = options[:zone] @content = options.fetch(:content, zone) end def create_entry(subdomain) ws_dns.create_entry(type: type, content: content, name: subdomain, zone: zone) end def delete_entry(subdomain) ws_dns.delete_entry(type: type, content: content, name: subdomain, zone: zone) end def has_entry?(subdomain) ws_dns.has_entry?(type: type, name: subdomain, zone: zone) end protected def type 'CNAME' end end
  54. 54. class AWsDns < CnameWsDns protected def type 'A' end end
  55. 55. class TxtWsDns < CnameWsDns protected def type 'TXT' end end
  56. 56. How to fix it?
  57. 57. OOP Parent knowing your children
  58. 58. class TransactionResponseParser attr_reader :xml def initialize(xml) @xml = xml end def parse # omitted implementation end private # specific transaction methods omitted end
  59. 59. class ResponseParser attr_reader :xml def initialize(xml) @xml = xml end def parse # omitted implementation end # omitted protected methods end
  60. 60. class TransactionResponseParser < ResponseParser private # specific transaction methods omitted end class AccountResponseParser < ResponseParser private # specific account methods omitted end
  61. 61. class ResponseParser def self.transaction?(xml) xml.include?('<transaction>') end def self.get_parser(xml) ResponseParser.transaction?(xml) ? TransactionResponseParser.new(xml) : AccountResponseParser.new(xml) end def initialize(xml) @xml = xml end def parse # omitted implementation end end
  62. 62. How to fix it?
  63. 63. module ResponseParserFactory def self.build(xml) if xml.include?('<transaction>') TransactionResponseParser.new(xml) else AccountResponseParser.new(xml) end end end
  64. 64. The worst class
  65. 65. class DomainChecker extend Memoist DOMAIN_REGEXP = /^[a-z0-9]+(-[a-z0-9]+)*(.[a-z0-9]+(-[a-z0-9]+)*)+$/ attr_accessor :domain def initialize(args = {}) @domain = args[:domain] end def check_new check_existing end def status if dns_adapter.ns_locaweb? a_entry_locaweb = dns_adapter.a_entry_locaweb if a_entry_locaweb == AppConfig.ip_lvs_criador_de_sites return :ok elsif a_entry_locaweb == false return :unavailable else return :already_using end end if domain_checker_result["error"] == "generic" return :generic_error end if domain_checker_result["error"] == "unsupported_tld" return :unsupported_tld end if domain_checker_result["available"] return :register end if dns_adapter.a_value == AppConfig.ip_lvs_criador_de_sites return :ok else return :config_dns end end memoize :status def available_domain_by_user(user) if domain.blank? return {valid: false, notice: :invalid, message: :blank} end if !domain.match(DOMAIN_REGEXP) return {valid: false, notice: :invalid, message: :invalid} end if forbidden_domain? return {valid: false, notice: :invalid, message: :forbidden_domain} end if Domain.where(address: domain).count > 0 current_domain = Domain.where(address: domain).first if (current_domain.site.account.users.include?(user) rescue false) return {valid: false, notice: :invalid, message: :already_using} else return {valid: false, notice: :invalid, message: :already_exists} end end if !domain_checker_result["valid"] && domain_checker_result["error"] != "unsupported_tld" return {valid: false, notice: :invalid, message: :invalid} end if domain_checker_result["error"] == "unsupported_tld" return {valid: true, notice: :unsupported_tld} end if domain_checker_result["available"] return {valid: true, notice: :register} end if domain_checker_result["customer_login"].blank? return {valid: true, notice: :config_dns} end if domain_checker_result["customer_login"].downcase == user.username.downcase Rails.logger.info "user owner domain" if dns_adapter.a_entry_locaweb? if dns_adapter.a_entry_locaweb == AppConfig.ip_lvs_criador_de_sites_old return {valid: true, notice: :old_csit} else return {valid: true, notice: :already_using} end else Rails.logger.info "Without entry A" return {valid: true, notice: :owner_domain} end else Rails.logger.info "user does not owner domain" return {valid: false, notice: :not_owner} end end def details { entry_a: dns_adapter.a_value, entry_ns: dns_adapter.ns_value, entry_cname: dns_adapter.cname_value }.merge(domain_checker_result) end def check_existing return external_check if external_check["error"] == "generic" return external_check if external_check["error"] == "invalid_domain" return external_check if external_check["error"] == "unsupported_tld" return external_check if external_check["available"] return external_check if internal_check["available"] internal_check end private def dns_adapter DnsAdapter.new(domain: CGI.escape(domain)) end memoize :dns_adapter def domain_checker_result domain_checker = DomainChecker.new(domain: CGI.escape(domain)) domain_checker_result = domain_checker.check_new end memoize :domain_checker_result def get_token WsAuthentication.new(AppConfig.wsauthentication.url).authenticate(AppConfig.wsauthentication.user, AppConfig.wsauthent end memoize :get_token def external_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/external_check" begin JSON(http_get(url)) rescue RestClient::NotImplemented return { "valid" => false, "available" => false, "error" => 'unsupported_tld' } rescue RestClient::InternalServerError => exception Rails.logger.error "[ERROR] GET #{url}: #{exception.message}n" "Response: #{exception.http_body}" return { "valid" => false, "available" => false, "error" => 'generic' } rescue => exception Rails.logger.error exception.print raise exception end end memoize :external_check def internal_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/internal_check" JSON(http_get(url)) end memoize :internal_check def forbidden_domain? uri = "#{AppConfig.registro_domain_url}/domain/#{domain}/check" begin response = JSON(CasSaas::CasRestClient.new.get(uri)) !response["valid"] rescue => e Rails.logger.info e.message true end end memoize :forbidden_domain? def http_get(url, headers = {}) Rails.logger.info "chamando GET #{url}, headers: #{headers}" response = RestClient.get url, headers Rails.logger.info "response #{response}" response end end
  66. 66. Business scenario for DomainChecker class
  67. 67. class DomainChecker # ... def check_new # omitted implementation end def status # omitted implementation end memoize :status def available_domain_by_user(user) # omitted implementation end def details # omitted implementation end def check_existing # omitted implementation end # ... end
  68. 68. class DomainChecker extend Memoist attr_accessor :domain def initialize(args = {}) @domain = args[:domain] end # ... end
  69. 69. class DomainChecker extend Memoist # ... def check_new check_existing end def check_existing return external_check if external_check["error"] == "generic" return external_check if external_check["error"] == "invalid_domain" return external_check if external_check["error"] == "unsupported_tld" return external_check if external_check["available"] return external_check if internal_check["available"] internal_check end # ... end
  70. 70. class DomainChecker extend Memoist attr_accessor :domain def initialize(args = {}) @domain = args[:domain] end # ... end
  71. 71. def status if dns_adapter.ns_locaweb? a_entry_locaweb = dns_adapter.a_entry_locaweb if a_entry_locaweb == AppConfig.ip_lvs_criador_de_sites return :ok elsif a_entry_locaweb == false return :unavailable else return :already_using end end if domain_checker_result["error"] == "generic" return :generic_error end if domain_checker_result["error"] == "unsupported_tld" return :unsupported_tld end if domain_checker_result["available"] return :register end if dns_adapter.a_value == AppConfig.ip_lvs_criador_de_sites return :ok else return :config_dns end end memoize :status
  72. 72. def dns_adapter DnsAdapter.new(domain: CGI.escape(domain)) end memoize :dns_adapter def domain_checker_result domain_checker = DomainChecker.new(domain: CGI.escape(domain)) domain_checker_result = domain_checker.check_new end memoize :domain_checker_result
  73. 73. def get_token WsAuthentication.new(AppConfig.wsauthentication.url).authenticate(AppConfig. wsauthentication.user, AppConfig.wsauthentication.pass) end memoize :get_token def external_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/ external_check" begin JSON(http_get(url)) rescue RestClient::NotImplemented return { "valid" => false, "available" => false, "error" => 'unsupported_tld' } rescue RestClient::InternalServerError => exception Rails.logger.error "[ERROR] GET #{url}: #{exception.message}n" "Response: #{exception.http_body}" return { "valid" => false, "available" => false, "error" => 'generic' } rescue => exception Rails.logger.error exception.print raise exception end end memoize :external_check
  74. 74. def internal_check url = "#{AppConfig.registro_service_url}/domain_availability/ #{domain}/internal_check" JSON(http_get(url)) end memoize :internal_check def forbidden_domain? uri = "#{AppConfig.registro_domain_url}/domain/#{domain}/check" begin response = JSON(CasSaas::CasRestClient.new.get(uri)) !response["valid"] rescue => e Rails.logger.info e.message true end end memoize :forbidden_domain?
  75. 75. class DomainChecker extend Memoist attr_accessor :domain def initialize(args = {}) @domain = args[:domain] end # ... end
  76. 76. def status if dns_adapter.ns_locaweb? a_entry_locaweb = dns_adapter.a_entry_locaweb if a_entry_locaweb == AppConfig.ip_lvs_criador_de_sites return :ok elsif a_entry_locaweb == false return :unavailable else return :already_using end end if domain_checker_result["error"] == "generic" return :generic_error end if domain_checker_result["error"] == "unsupported_tld" return :unsupported_tld end if domain_checker_result["available"] return :register end if dns_adapter.a_value == AppConfig.ip_lvs_criador_de_sites return :ok else return :config_dns end end memoize :status
  77. 77. def available_domain_by_user(user) if domain.blank? return {valid: false, notice: :invalid, message: :blank} end if !domain.match(DOMAIN_REGEXP) return {valid: false, notice: :invalid, message: :invalid} end if forbidden_domain? return {valid: false, notice: :invalid, message: :forbidden_domain} end if Domain.where(address: domain).count > 0 current_domain = Domain.where(address: domain).first if (current_domain.site.account.users.include?(user) rescue false) return {valid: false, notice: :invalid, message: :already_using} else return {valid: false, notice: :invalid, message: :already_exists} end end if !domain_checker_result["valid"] && domain_checker_result["error"] != "unsupported_tld" return {valid: false, notice: :invalid, message: :invalid} end if domain_checker_result["error"] == "unsupported_tld" return {valid: true, notice: :unsupported_tld} end if domain_checker_result["available"] return {valid: true, notice: :register} end if domain_checker_result["customer_login"].blank? return {valid: true, notice: :config_dns} end if domain_checker_result["customer_login"].downcase == user.username.downcase Rails.logger.info "user owner domain" if dns_adapter.a_entry_locaweb? if dns_adapter.a_entry_locaweb == AppConfig.ip_lvs_criador_de_sites_old return {valid: true, notice: :old_csit} else return {valid: true, notice: :already_using} end else Rails.logger.info "Without entry A" return {valid: true, notice: :owner_domain} end else Rails.logger.info "user does not owner domain" return {valid: false, notice: :not_owner} end end
  78. 78. def available_domain_by_user(user) if domain.blank? return {valid: false, notice: :invalid, message: :blank} end if !domain.match(DOMAIN_REGEXP) return {valid: false, notice: :invalid, message: :invalid} end if forbidden_domain? return {valid: false, notice: :invalid, message: :forbidden_domain} end if Domain.where(address: domain).count > 0 current_domain = Domain.where(address: domain).first if (current_domain.site.account.users.include?(user) rescue false) return {valid: false, notice: :invalid, message: :already_using} else return {valid: false, notice: :invalid, message: :already_exists} end end # ... end
  79. 79. def available_domain_by_user(user) if domain.blank? return {valid: false, notice: :invalid, message: :blank} end if !domain.match(DOMAIN_REGEXP) return {valid: false, notice: :invalid, message: :invalid} end if forbidden_domain? return {valid: false, notice: :invalid, message: :forbidden_domain} end if Domain.where(address: domain).count > 0 current_domain = Domain.where(address: domain).first if (current_domain.site.account.users.include?(user) rescue false) return {valid: false, notice: :invalid, message: :already_using} else return {valid: false, notice: :invalid, message: :already_exists} end end # ... end
  80. 80. def available_domain_by_user(user) if domain.blank? return {valid: false, notice: :invalid, message: :blank} end if !domain.match(DOMAIN_REGEXP) return {valid: false, notice: :invalid, message: :invalid} end if forbidden_domain? return {valid: false, notice: :invalid, message: :forbidden_domain} end if Domain.where(address: domain).count > 0 current_domain = Domain.where(address: domain).first if (current_domain.site.account.users.include?(user) rescue false) return {valid: false, notice: :invalid, message: :already_using} else return {valid: false, notice: :invalid, message: :already_exists} end end # ... end
  81. 81. def available_domain_by_user(user) # … if domain_checker_result["customer_login"].downcase == user.username.downcase Rails.logger.info "user owner domain" if dns_adapter.a_entry_locaweb? if dns_adapter.a_entry_locaweb == AppConfig.ip_lvs_criador_de_sites_old return {valid: true, notice: :old_csit} else return {valid: true, notice: :already_using} end else Rails.logger.info "Without entry A" return {valid: true, notice: :owner_domain} end else Rails.logger.info "user does not owner domain" return {valid: false, notice: :not_owner} end end
  82. 82. def available_domain_by_user(user) # … if !domain_checker_result["valid"] && domain_checker_result["error"] != "unsupported_tld" return {valid: false, notice: :invalid, message: :invalid} end if domain_checker_result["error"] == "unsupported_tld" return {valid: true, notice: :unsupported_tld} end if domain_checker_result["available"] return {valid: true, notice: :register} end if domain_checker_result["customer_login"].blank? return {valid: true, notice: :config_dns} end # … end
  83. 83. class DomainChecker # ... def check_new check_existing end def check_existing return external_check if external_check["error"] == "generic" return external_check if external_check["error"] == "invalid_domain" return external_check if external_check["error"] == "unsupported_tld" return external_check if external_check["available"] return external_check if internal_check["available"] internal_check end private def domain_checker_result domain_checker = DomainChecker.new(domain: CGI.escape(domain)) domain_checker_result = domain_checker.check_new end memoize :domain_checker_result # ... end
  84. 84. class DomainChecker # ... def check_new check_existing end def check_existing return external_check if external_check["error"] == "generic" return external_check if external_check["error"] == "invalid_domain" return external_check if external_check["error"] == "unsupported_tld" return external_check if external_check["available"] return external_check if internal_check["available"] internal_check end private def domain_checker_result domain_checker = DomainChecker.new(domain: CGI.escape(domain)) domain_checker_result = domain_checker.check_new end memoize :domain_checker_result # ... end
  85. 85. def external_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/external_check" begin JSON(http_get(url)) rescue RestClient::NotImplemented return { "valid" => false, "available" => false, "error" => 'unsupported_tld' } rescue RestClient::InternalServerError => exception Rails.logger.error "[ERROR] GET #{url}: #{exception.message}n" "Response: #{exception.http_body}" return { "valid" => false, "available" => false, "error" => 'generic' } rescue => exception Rails.logger.error exception.print raise exception end end memoize :external_check def internal_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/internal_check" JSON(http_get(url)) end memoize :internal_check
  86. 86. class DomainChecker # ... def http_get(url, headers = {}) Rails.logger.info "chamando GET #{url}, headers: #{headers}" response = RestClient.get url, headers Rails.logger.info "response #{response}" response end end
  87. 87. class DomainChecker # ... def check_new check_existing end def check_existing return external_check if external_check["error"] == "generic" return external_check if external_check["error"] == "invalid_domain" return external_check if external_check["error"] == "unsupported_tld" return external_check if external_check["available"] return external_check if internal_check["available"] internal_check end private def domain_checker_result domain_checker = DomainChecker.new(domain: CGI.escape(domain)) domain_checker_result = domain_checker.check_new end memoize :domain_checker_result # ... end
  88. 88. def external_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/external_check" begin JSON(http_get(url)) rescue RestClient::NotImplemented return { "valid" => false, "available" => false, "error" => 'unsupported_tld' } rescue RestClient::InternalServerError => exception Rails.logger.error "[ERROR] GET #{url}: #{exception.message}n" "Response: #{exception.http_body}" return { "valid" => false, "available" => false, "error" => 'generic' } rescue => exception Rails.logger.error exception.print raise exception end end memoize :external_check def internal_check url = "#{AppConfig.registro_service_url}/domain_availability/#{domain}/internal_check" JSON(http_get(url)) end memoize :internal_check
  89. 89. class DomainChecker # ... def check_new check_existing end def check_existing return external_check if external_check["error"] == "generic" return external_check if external_check["error"] == "invalid_domain" return external_check if external_check["error"] == "unsupported_tld" return external_check if external_check["available"] return external_check if internal_check["available"] internal_check end private def domain_checker_result domain_checker = DomainChecker.new(domain: CGI.escape(domain)) domain_checker_result = domain_checker.check_new end memoize :domain_checker_result # ... end
  90. 90. class DomainChecker # ... def details { entry_a: dns_adapter.a_value, entry_ns: dns_adapter.ns_value, entry_cname: dns_adapter.cname_value }.merge(domain_checker_result) end private def dns_adapter DnsAdapter.new(domain: CGI.escape(domain)) end memoize :dns_adapter #... end
  91. 91. class DomainChecker # ... private def get_token WsAuthentication.new(AppConfig.wsauthentication.url) .authenticate(AppConfig.wsauthentication.user, AppConfig.wsauthentication.pass) end memoize :get_token end
  92. 92. DomainChecker class problems
  93. 93. Long class DomainChecker class problems
  94. 94. Constructor with hash parameter, but only use one key of the hash. DomainChecker class problems
  95. 95. A method implementation that only call a private method without parameters DomainChecker class problems
  96. 96. Memoize (hell) dependency: memoized methods used like variables. DomainChecker class problems
  97. 97. A lot ifs: ifs with ifs with else with if with else (it’s hard until to explain) DomainChecker class problems
  98. 98. Code hard to understand (internal x external checks) DomainChecker class problems
  99. 99. An unused private method DomainChecker class problems
  100. 100. Instance method creates another instance of itself DomainChecker class problems
  101. 101. • Many responsibilities: ✴ Validate domain format ✴ Validate domain logic ✴ Format return to use in view ✴ Do HTTP requests ✴ Parse HTTP responses DomainChecker class problems
  102. 102. DomainChecker class problems introduces new patterns and principles
  103. 103. Write-only code Once write no one can read DomainChecker class introduces
 new patterns and principles
  104. 104. Close Closed Principle Closed for modification, more closed for extension. DomainChecker class introduces
 new patterns and principles
  105. 105. Inception Pattern Where an instance of a class creates a new instance of itself and aggregates
 the new instance state to your state DomainChecker class introduces
 new patterns and principles
  106. 106. DomainChecker class probably is the worst Ruby class I've seen in my life
  107. 107. How did we fix it?
  108. 108. Why is WOP applied?
  109. 109. Lack of knowledge Causes of WOP
  110. 110. Immaturity in software development Causes of WOP
  111. 111. No collaborative environment Causes of WOP
  112. 112. No coaching Causes of WOP
  113. 113. Tight deadlines Causes of WOP
  114. 114. Why simplify if you can complicate? Causes of WOP
  115. 115. To use "cool things" because they are cool, even if they are not a solution. Causes of WOP
  116. 116. Some mysteries of the human mind (that we can't explain) Causes of WOP
  117. 117. How to avoid WOP?
  118. 118. Read a lot How to avoid WOP?
  119. 119. How to avoid WOP?
  120. 120. But do not learn only Ruby How to avoid WOP?
  121. 121. How to avoid WOP?
  122. 122. Use code review How to avoid WOP?
  123. 123. Read code from others programmers How to avoid WOP?
  124. 124. Write code yourself can read in the future How to avoid WOP?
  125. 125. Participate of open source projects: contributing, discussing, reading. How to avoid WOP?
  126. 126. Do coaching of less experienced developers (teaching is a good way to learn too) How to avoid WOP?
  127. 127. Do not write code for you: write it to the application, to the team. How to avoid WOP?
  128. 128. Exchange experiences, ask. How to avoid WOP?
  129. 129. Use pair programming (not 100% IMHO) How to avoid WOP?
  130. 130. Learn from your mistakes and others How to avoid WOP?
  131. 131. Face bad code as an opportunity to get better How to avoid WOP?
  132. 132. Do not face bad code with complaints or making fun of the authors How to avoid WOP?
  133. 133. This funny code causes waste of time, resources and money How to avoid WOP?
  134. 134. Instead, show the authors of bad code the right way How to avoid WOP?
  135. 135. Show them the Ruby way How to avoid WOP?
  136. 136. Thank you! I hope you enjoyed @Prodis
  137. 137. The worst Ruby codes I’ve seen in my life RubyKaigi 2015 @Prodis

×