Пишу на руби уже больше 4х лет
Когда-то коммитил в Travis CI
много чего еще
Вижу много кода, который мог бы быть лучше
Поделиться опытом
Получить фидбек
...
Со временем изменения вносить все тяжелее и тяжелее
Любое изменение требует все больше и больше времени
class Nameserver < ActiveRecord::Base
def self.import
nameservers = fetch_nameservers
geoip = GeoIP.new(Rails.root.join('db', 'GeoLiteCity.dat'))
nameservers.each do |nameserver|
Rails.logger.info "Processing nameserver: #{nameserver[:ip]}"
nameserver = nameserver.slice(:country_id, :city, :state, :ip)
if valid_for_import?(nameserver)
fill_nameserver_with_additional_attributes(geoip, nameserver)
create!(nameserver)
end
end
...
...
def self.fill_nameserver_with_additional_attributes(nameserver)
nameserver[:city] = nameserver[:city].presence
city = geoip.city(nameserver[:ip])
nameserver[:latitude] = city.latitude
nameserver[:longitude] = city.longitude
end
def self.fetch_nameservers
open('http://public-dns.tk/nameservers.json') { |f|
JSON.parse(f.read).map(&:symbolize_keys)
}
end
...
...
def self.valid_for_import?(nameserver)
valid_country?(nameserver) &&
valid_state?(nameserver) &&
valid_ip_address?(nameserver) &&
exists?(:ip => nameserver[:ip]) &&
is_able_to_resolve?(nameserver)
end
private
# def self.valid_country?(nameserver)
# def self.valid_state?(nameserver)
# def self.valid_ip_address?(nameserver)
# def self.is_able_to_resolve?(nameserver)
...
...
# Еще 10 страниц кода в том же классе
# И этот код уже не связан с импортом
end
end
Литературы очень много
Low coupling
High cohesion
Small classes
Composition over inheritance
...
class Nameservers::Importer
def initialize(options)
@logger = options.fetch(:logger) {
Rails.logger
}
@repository = options.fetch(:repository) {
Nameservers::Nameserver
}
@validator = options.fetch(:validator) {
Nameservers::Validator.new(:repository => @repository)
}
@fetches_namespace_data = options.fetch(:fetches_namespace_data) {
Nameservers::DataLoader(:geoip =>
GeoIP.new(Rails.root.join('db', 'GeoLiteCity.dat')))
}
end
class Nameservers::Importer
def call(nameservers)
nameservers.each do |nameserver|
@logger.info "Processing nameserver: #{nameserver[:ip]}"
nameserver = nameserver.slice(:country_id, :city, :state, :ip)
if @validator.valid?(nameserver)
nameserver = @fetches_namespace_data.call(nameserver)
@repository.create!(nameserver)
end
end
end
end
Создавайте больше классов.
При создании задумывайтесь, что может меняться.
Выделяйте зависимости (еще больше классов).
Изолируйте изменения.
class Invoice < ActiveRecord::Base
attr_accessible :title
def as_json(options = {})
super(methods: [:user_name], only: [:id, :date_range, :price])
end
def report(filename)
CSV.generate(filename)
# ...
end
def background_color
settings.background_color || 'rgba(0, 0, 255, 0.5)'
end
def schedule_post_upload_processing
InvoiceProcessor.delay(priority: 15).after_create(id)
end
end
class Invoice < ActiveRecord::Base # persistence
attr_accessible :title # fixed with strong params, do you agree?
def as_json(options = {}) # ActiveModel::Serializers, CustomSerializer
super(methods: [:user_name], only: [:id, :date_range, :price])
end
def report(filename) # InvoiceReportGenerator, ReportGenerator?
CSV.generate(filename)
# ...
end
def background_color # really? background color?
settings.background_color || 'rgba(0, 0, 255, 0.5)'
end
def schedule_post_upload_processing # definitely persistence
InvoiceProcessor.delay(priority: 15).after_create(id)
end
end
Идея - по возможности stateless классы подобного вида:
class Service
def initialize(*dependencies)
# @dependencies = dependencies
end
# methods that use dependencies
end
Тем, кто писал на Angular.js будет знакома концепция
class Nameservers::Importer def initialize(logger, repository, geoip, validator) end end
class Nameservers::Validator def initialize(logger, repository) end end
class Nameservers::EventLogger def initialize(logger_file_path) end end
module Nameservers::Registry
def self.repository
Nameservers::Nameserver # < ActiveRecord::Base
end
def self.importer
@importer ||= Nameservers::Importer.new(:validator => validator,
:repository => repository,
:geoip => Geoip.new(root + '/db/geoipcitylite.dat'))
end
endmodule Nameservers::Registry
def self.validator
Nameservers::Validator.new(:repository => repository)
end
def self.serializer
Oj
end
private
def self.root
File.expand_path('../../', __FILE__)
end
end
container.register :root_path, '/tmp'
container.register :repository, Nameserver
container.register :validator do
NameserverValidator.new(:repository => repository)
end
container.register :importer do |validator, repository, root_path|
NameserverImporter.new(:validator => validator,
:repository => repository,
:geoip => Geoip.new(root_path + '/db/geoipcitylite.dat'))
end
Можно использовать вот такие "контейнеры" как для всего приложения, так и для конкретного запроса.
Их даже можно обьединить.
При использовании такой схемы, можно сделать обертку над ActionController, которая будет автоматически вставлять аргументы:
class ImportController
def import_nameservers(params, importer, nameserver_data_source)
importer.call(nameserver_data_source.all(params[:data_source_url]))
end
end
Hint: poniard
Можно сделать проще:
class ApplicationController
def container
# setup and cache container
end
end
class ImportController
def import_nameservers
nameservers = container[:nameserver_data_source].all(params[:data_source_url])
container[:importer].call(nameservers)
end
end
class Nameserver < ActiveRecord::Base
def on(event, &callback)
(@events[event] ||= []) << callback
end
def trigger(event, *args)
@events[event].each do |callback|
callback.call(*args)
end
end
end
class Nameserver < ActiveRecord::Base
around_save :notify_on_save
private
def notify_on_save
if new_record?
# ...
end
yield
if connection_address_changed?
trigger(:connection_address_changed, new_connection_address, old_connection_address)
end
# ...
end
end
class NameserversController
def update
@nameserver = Nameserver.find(params[:ip])
@nameserver.on :become_available do
schedule_background_job_for(@nameserver)
end
@nameserver.on :connection_address_changed do |new_connection_address, old_connection_address|
notify_via_websockets(:create_connection, new_connection_address)
notify_via_websockets(:delete_connection, old_connection_address)
end
@nameserver.save
end
end
Вместо коллбеков можно использовать сам контроллер:
class Nameserver < ActiveRecord::Base
def on(event, observer)
(@observers[event] ||= []) << observer
end
def trigger(event, *args)
@observers[event].each do |observer|
if observer.respond_to?(event)
observer.send(event, self, *args)
end
end
end
end
class NameserversController
def update
@nameserver = Nameserver.find(params[:ip])
@nameserver.add_listener self
@nameserver.save
end
def connection_address_changed(nameserver, new_connection_address, old_connection_address)
notify_via_websockets(:create_connection, new_connection_address)
notify_via_websockets(:delete_connection, old_connection_address)
end
end
... или другие обьекты
class NameserversController
def update
@nameserver = Nameserver.find(params[:ip])
@nameserver.add_listener WebsocketNotifier.new(websocket_server_url)
@nameserver.save
end
end
class WebsocketNotifier
def initialize(websocket_server_url)
@websocket_server_url = websocket_server_url
end
def connection_address_changed(nameserver, new_connection_address, old_connection_address)
# NOTIFY
end
end
class EventSet
def on(event, observer)
@observers ||= {}
@observers[event] ||= []
@observers[event] << observer
end
def off(event, observer)
return unless @observers
return unless @observers[event]
@observers[event].delete(observer)
end
def trigger(event, *args)
if @observers && @observers[event]
@observers[event].each do |observer|
if observer.respond_to?(event)
observer.send(event, *args)
end
end
end
end
end
Идея - отделить реакцию на запрос от фреймворка
class CreatesUserFile # or UserFileCreator
def initialize(repository, uploader)
@repository = repository
@uploader = uploader
end
def call(params, request_body, listener)
attributes = extract_attributes(params)
attributes[:file_url] = @uploader.upload(request_body)
file = @repository.create(attributes)
listener.creation_succeeded(file)
rescue => e
listener.creation_failed(e)
end
end
Приняли аргументы - возвратите значение
Не изменяйте переданные аргументы
def fill_nameserver_attributes(nameserver)
city = $geoip.city nameserver[:ip].to_s
nameserver[:latitude] = city.latitude
nameserver[:longitude] = city.longitude
end
nameserver = {:ip => IPAddr.new('8.8.8.8') }
fill_nameserver_attributes(nameserver)
nameserver[:latitude]
=> 32.2
def fill_nameserver_attributes(nameserver)
city = $geoip.city nameserver[:ip].to_s
nameserver.merge :latitude => city.latitude,
:longitude => city.longitude
end
Если производительность вам в данном куске кода ОЧЕНЬ важна (что маловероятно):
def fill_nameserver_attributes(nameserver) city = $geoip.city nameserver[:ip].to_s nameserver[:latitude] = city.latitude nameserver[:longitude] = city.longitude nameserver end
API должно показывать что клиент должен рассчитывать только на возвращаемое значение.
class JSONLoggerFormatter
def call(message)
message.to_json << "\n"
end
end
class JSONLoggerFormatter
def call(message)
JSON.dump(message) << "\n"
end
end
class JSONLoggerFormatter
def initialize(json)
@json = json
end
def call(message)
@json.dump(message) << "\n"
end
end
class JSONLoggerFormatter
def initialize(json)
@json = json
end
def call(message)
@json.dump(message.to_value) << "\n"
end
end
class SerializedLoggerFormatter
def initialize(serializer)
@serializer = serializer
end
def call(message)
@serializer.dump(message.to_value) << "\n"
end
end