Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pavel-sazonov/ba53a228b31963f8728a276bf8a22985 to your computer and use it in GitHub Desktop.
Save pavel-sazonov/ba53a228b31963f8728a276bf8a22985 to your computer and use it in GitHub Desktop.
------------------------------
********************
CONFIDENT RUBY
********************
------------------------------
каждый метод может состоять максимум из 4 частей
1 Collecting input
2 Performing work
3 Delivering output
4 Handling failures
example
<%
def location(item, value)
# Collecting input start
sub_table = get_sub_table(item, value)
# Collecting input end
if(sub_table.length==0)
# Handling failures
raise MetricFu::AnalysisError, "The #{item.to_s} '#{value.to_s}' "\
"does not have any rows in the analysis table"
else
# Collecting input start
first_row = sub_table[0]
# Collecting input end
# Performing work
case item
when :class
MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
when :method
MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
when :file
MetricFu::Location.get(first_row.file_path, nil, nil)
else
# Handling failures
raise ArgumentError, "Item must be :class, :method, or :file"
end
end
end
%>
------------------------------
Главный принцип ООП - все является обьектом и обьекты общаются между собою отправкой сообщений
Поэтому мы должны держать в памяти три вещи:
1) Мы должны идентифицировать сообщения, которые мы хотим отправлять что бы запустить ту или иную задачу
2) Мы должны идентицифировать роли которые принимают и посылают сообщения
3) Мы должны гарантировать что каждый метод получит тот объект с которым может работать
------------------------------
Предметная облаcть - сайт, где можно заказать книгу и скачать
Задача нам нужно парсить старую форму заказа которая является csv-файлом
где есть имя покупателя, его почта, id книги которую он хочет заказать и дата заказа
name,email,product_id,date
Crow T. Robot,crow@example.org,123,2012-06-18
Tom Servo,tom@example.org,456,2011-09-05
Crow T. Robot,crow@example.org,456,2012-06-25
Давайте определим какие действия надо совершать
1) Парсить каждую строку из csv-файла
2) Для каждой записи, использовать email чтобы взять запись о покупателе из БД или создать новую
3) Использовать id вещи чтобы взять запись о книги из БД
4) Добавить взял покупателя и книгу, создать запись в БД о покупке
5) Уведомить покупателя, дать ему ссылку по которой он может скачать книгу
6) Добавить в лог импорта запись об удачной транзакции
Теперь для каждого действия определим метод
1) #parse_legacy_purchase_records
2) для каждой CSV-записи, по email вызываем #get_customer
3) для каждой CSV-записи, по id книги вызываем #get_product
4) что бы создать запись о покупке вызываем #add_purchased_product
5) уведомляем покупателя через метод #notify_of_files_available
6) для каждой CSV-записи вызываем метод #log_successful_import
Идентифицируем роли
метод | роль
#parse_legacy_purchase_records | legacy_data_parser
#each | purchase_list
#email_address | purchase_record
#product_id | purchase_record
#get_customer | customer_list
#get_product | product_inventory
#add_purchased_product | customer
#notify_of_files_available | customer
#log_successful_import | data_importer
<%
def import_legacy_purchase_data(data)
purchase_list = legacy_data_parser.parse_purchase_records(data)
purchase_list.each do |purchase_record|
customer = customer_list.get_customer(purchase_record.email_address)
product = product_inventory.get_product(purchase_record.product_id)
customer.add_purchased_product(product)
customer.notify_of_files_available(product)
log_successful_import(purchase_record)
end
end
%>
------------------------------
SOME BEST PRACTICE FOR Collecting input
что бы не получать 500-е, проверяй соответствия
<%
if duck && duck.quack
end
if "/home/avdi/.gitconfig".respond_to?(:to_path)
end
# Rails-style
if duck.try(:quack)
end
%>
не используй функции только для возврата переменных
используй для этого константы
<%
def seconds_in_day
24 * 60 * 60
end
SECONDS_IN_DAY = 24 * 60 * 60
###
class TimeCalc
SECONDS_IN_DAY = 24 * 60 * 60
def seconds_in_week
seconds_in_days(7)
end
def seconds_in_days(num_days)
num_days * SECONDS_IN_DAY
end
end
%>
что бы убедиться в правильностью входных данных
можно внутри метода вызывать методы .to_i, .to_s
<%
class Meters
# ...
def to_meters
self
end
def -(other)
self.class.new(value - other.to_meters.value)
end
end
class Feet
# ...
def to_meters
Meters.new((value * 0.3048).round)
end
end
# убеждаемся что это метры а не дюймы
###
# expect instance of Meters or Feet
###
def report_altitude_change(current_altitude, previous_altitude)
change = current_altitude.to_meters - previous_altitude.to_meters
# ...
end
%>
не бойся создавать типы через
Array(), Float(), String(), Integer(), Rational(), или Complex()
как не странно, это все функции
<%
Integer(10)
# => 10
Integer(10.1)
# => 10
Integer("0x10")
# => 16
Integer("010")
# => 8
Integer("0b10")
# => 2
# Time defines #to_i
Integer(Time.now)
# => 1341469768
inventory = ['apples', 17, 'oranges', 11, 'pears', 22]
Hash[*inventory]
# => {"apples"=>17, "oranges"=>11, "pears"=>22}
%>
если входящие данные приходят из разных источников
можно заморочится на них и для каждого типа сделать обработчик
<%
module Graphics
module Conversions
module_function
def Point(*args)
case args.first
when Point then args.first
when Array then Point.new(*args.first)
when Integer then Point.new(*args)
when String then Point.new(*args.first.split(':').map(&:to_i))
else
raise TypeError, "Cannot convert #{args.inspect} to Point"
end
end
end
Point = Struct.new(:x, :y) do
def inspect
"#{x}:#{y}"
end
end
end
include Graphics
include Graphics::Conversions
Point(Point.new(2,3))
# => 2:3
Point([9,7])
# => 9:7
Point(3,5)
# => 3:5
Point("8:10")
# => 8:10
%>
module_function
First, it marks all following methods as private.
Second, it makes the methods available as singleton methods on the module.
Благодаря нему в самом модуле не надо прописывать self. что бы вызвать метод
И если мы этот модуль встраиваем в класс то этот метод сразу становится приватным
и еще немного магии если на вход идет класс
который может сам себя перевеси в массив или point
<%
def Point(*args)
case args.first
when Integer then Point.new(*args)
when String then Point.new(*args.first.split(':').map(&:to_i))
when ->(arg){ arg.respond_to?(:to_point) }
args.first.to_point
when ->(arg){ arg.respond_to?(:to_ary) }
Point.new(*args.first.to_ary)
else
raise TypeError, "Cannot convert #{args.inspect} to Point"
end
end
# Point can define .to_point itself
Point = Struct.new(:x, :y) do
def inspect
"#{x}:#{y}"
end
def to_point
self
end
end
%>
в case для условий можно использовать лямбды
<%
even = ->(x) { (x % 2) == 0 }
when ->(x) { (x % 2) == 0 }
%>
лямбде можно давать значения через ===
<%
even = ->(x) { (x % 2) == 0 }
even === 4
# => true
even.call(4)
# => true
even === 9
# false
even.call(9)
# false
%>
и лямбду можно сразу вставлять в case
<%
def number_type(number)
even = ->(x) { (x % 2) == 0 }
case number
when 42
"the ultimate answer"
when even
"even"
else
"odd"
end
end
%>
------------------------------
не бойся вызывать ArgumentError для входящих параметров
<%
def change_to(state)
raise ArgumentError unless ["stop", "proceed", "caution"].include?(state)
@state = state
end
%>
------------------------------
вместо строк можно использовать экземпляр класса с методом to_s
<%
State = Struct.new(:name) do
def to_s
name
end
end
%>
------------------------------
Оборачивай входящие объекты при помощи интерфейса/адаптера
<%
# наш адаптер который гарантирует что у входящего обхекта будет метод <<
class IrcBotSink
def initialize(bot)
@bot = bot
end
def <<(message)
@bot.handlers.dispatch(:log_info, nil, message)
end
end
class SomeClass
def initialize(sink)
# во время инициализаци проверяем что за объект у нас
@sink = case sink
when Cinch::Bot then IrcBotSink.new(sink)
else sink
end
end
end
%>
для быстрого создания адаптера можно использовать DelegateClass
<%
require 'cinch'
require 'delegate'
class BenchmarkedLogger
# быстро создаем адаптер к классу Cinch::Bot
class IrcBotSink < DelegateClass(Cinch::Bot)
def <<(message)
handlers.dispatch(:log_info, nil, message)
end
end
def initialize(sink)
@sink = case sink
when Cinch::Bot then IrcBotSink.new(sink)
else sink
end
end
end
%>
------------------------------
для обработки входящих параметров можно использовать предусловия
<%
require 'date'
class Employee
attr_accessor :name
attr_reader :hire_date
# сохраняем значения в переменных name и hire_date
def initialize(name, hire_date)
@name = name
self.hire_date = hire_date
end
# переписываем обработчик что бы на уровне initialize выдал исключение если это не дата
# это нам важно так как ниже мы часто обращаемся к этому параметру
def hire_date=(new_hire_date)
raise TypeError, "Invalid hire date" unless new_hire_date.is_a?(Date)
@hire_date = new_hire_date
end
def due_for_tie_pin?
((Date.today - hire_date) / 365).to_i >= 10
end
def covered_by_pension_plan?
hire_date.year < 2000
end
def bio
"#{name} has been a Yoyodyne employee since #{hire_date.year}"
end
end
%>
------------------------------
не бойся использовать .fetch для хэша
------------------------------
не бойся отлавливать поведение сразу через return
<%
def log_reading(reading_or_readings)
return if @quiet
end
%>
------------------------------
все нестандартные поведения стоит обрабатывать не как nil, а как другой класс
есть программа где есть пользователь может авторизоваться
<%
def current_user
if session[:user_id]
User.find(session[:user_id])
end
end
%>
как видно по коду, авторизованный пользователь будет экземпляром User,
а не авторизованный будет как nil
далее по коду всего приложения будет много мест где используется current_user
<%
def greeting
"Hello, " +
current_user ? current_user.name : "Anonymous" +
", how are you today?"
end
%>
<%
if current_user
render_logout_button
else
render_login_button
end
%>
<%
if current_user
@listings = current_user.visible_listings
else
@listings = Listing.publicly_visible
end
%>
<%
cart = if current_user
current_user.cart
else
SessionCart.new(session)
end
cart.add_item(some_item, 1)
%>
если внутри одной предметной облати что то может быть объектом класса или nil,
а это сильно влияет на поведение программы, то nil стоит заменить на класс
<%
def current_user
if session[:user_id]
User.find(session[:user_id])
else
GuestUser.new(session[:user_id])
end
end
%>
<%
class GuestUser
def initialize(session)
@session = session
end
# тут мы переписываем классы от модели User
def name
"Anonymous"
end
def has_role?(role)
false
end
def authenticated?
false
end
end
# а что то можем и добавить для User
class User
def authenticated?
true
end
end
%>
и переписываем код дальнейшего приложения что бы он был чище
<%
def greeting
"Hello, #{current_user.name}, how are you today?"
end
%>
но не забудь написать для обоих классов тесты
<%
shared_examples_for 'a user' do
it { should respond_to(:name) }
it { should respond_to(:authenticated?) }
it { should respond_to(:has_role?) }
it { should respond_to(:visible_listings) }
it { should respond_to(:last_seen_online=) }
it { should respond_to(:cart) }
end
describe GuestUser do
subject { GuestUser.new(stub('session')) }
it_should_behave_like 'a user'
end
describe User do
subject { User.new }
it_should_behave_like 'a user'
end
%>
PART 2
можно с этой же логикой пойти по другому
есть класс который проводит конвертизацию с записью информации в лог
по умолчанию он должен запускаться без лога
NullLogger - это класс-заглушка которая ничего не делает
<%
class FFMPEG
def initialize(logger=NullLogger.new)
end
%>
<%
# каждый метод переписан и принимает сколько угодно параметров
class NullLogger
# def debug(*); end
# def info(*); end
# def warn(*); end
# def error(*); end
# def fatal(*); end
end
%>
------------------------------
Не бойся переписывать входящие параметры как экземпляры класса
<%
# not
def get_point(x,y)
end
# yes
Point = Struct.new(:x, :y)
def get_point(point)
end
%>
------------------------------
что бы не допустить ошибок из-за опечаток,
проще в качестве входящего параметра давать не хэш,
а объект
<%
# instead
map = Map.new
map.draw_point(23, 32, starred: true, fuzzy_radius: 100)
# you should use
map = Map.new
p1 = FuzzyPoint.new(StarredPoint.new(23, 32), 100)
map.draw_point(p1)
# but better is use block
map.draw_starred_point(7, 9) do |point|
point.name = "gold buried here"
point.magnitude = 15
point.fuzzy_radius = 50
end
%>
------------------------------
не бойся ипспользовать блоки для манипуляции с сообщениями
например есть функция для удаления файлов
если файла нет, то выводиться исключение Errno::ENOENT
<%
def delete_files(files)
files.each do |file|
File.delete(file)
end
end
%>
иногда нам нужно обработать ошибку, а иногда вывести в logger
а иногда просто игнорить все исключения
для этого лучше всего использовать блок
это намного удобнее чем городить огромный хэш со входящими параметрами
<%
# версия 1
def delete_files(files)
files.each do |file|
begin
File.delete(file)
rescue => error
if block_given? then yield(file, error) else raise end
end
end
end
# игнорим все исключения
delete_files(['does_not_exist', 'does_exist']) do
# ignore errors
end
# выводим исключения в консоль
delete_files(['does_not_exist', 'does_exist']) do |file, error|
puts error.message
end
# выводим некоторые исключения в консоль
delete_files(['does_not_exist', 'does_exist', 'not_mine/some_file']) do |file, error|
case error
when Errno::ENOENT
# Ignore
else
puts error.message
end
end
# версия 2 - определяем блок по-умолчанию
def delete_files(files, &error_policy)
error_policy ||= ->(file, error) { raise error }
files.each do |file|
begin
File.delete(file)
rescue => error
error_policy.call(file, error)
end
end
end
# выводим только название файла
delete_files(['bad_file']) do |file, error|
puts file
end
%>
версия 3 - очень сложно
разные блоки для разных ситуаций
<%
def delete_files(files, options={})
error_policy =
options.fetch(:on_error) { ->(file, error) { raise error } }
symlink_policy =
options.fetch(:on_symlink) { ->(file) { File.delete(file) } }
files.each do |file|
begin
if File.symlink?(file)
symlink_policy.call(file)
else
File.delete(file)
end
rescue => error
error_policy.call(file, error)
end
end
end
%>
------------------------------
ВОЗВРАЩЕНИЕ РЕЗУЛЬТАТА
------------------------------
очень сложно поддерживать функцию которая может вернуть
nil, строку или массив
пусть она всегда возвращает массив пустой или нет
самое главное при написании функции помнить что она возвращает массив
<%
def find_word(prefix)
# помни что возвращаем массив
return [] if prefix.empty?
words = File.readlines('/usr/share/dict/words').
map(&:chomp).reject{|w| w.include?("'") }
words.select{|w| w =~ /\A#{prefix}/}
end
%>
если метод возвращает строки, то пусть вместо nil отдает пустую строку
“nil is the worst possible representation of a failure: it carries no meaning but can still break things.”
Excerpt From: Avdi Grimm. “Confident Ruby (for james tumino).” iBooks.
------------------------------
иногда метод может иметь больше вывода чем true/false
для таких случаем пусть метод возвращает экземпляр класса
опустим есть три возвращаемых значений :true/:false и :rescue что означает произошло исключение
например, пусть возвращает экземпляр вот такого класса
<%
class ImportStatus
def self.success() new(:success) end
def self.redundant() new(:redundant) end
def self.failed(error)
new(:failed, error)
end
attr_reader :error
def initialize(status, error=nil)
@status = status
@error = error
end
def success?
@status == :success
end
def redundant?
@status == :redundant
end
def failed?
@status == :failed
end
end
%>
<%
def import_purchase(date, title, user_email)
user = User.find_by_email(user_email)
if user.purchased_titles.include?(title)
ImportStatus.redundant
else
purchase = user.purchases.create(title: title, purchased_at: date)
ImportStatus.success
end
rescue => error
ImportStatus.failed(error)
end
%>
но иногда мы не хотим возвращать возвратные значения
но обработать некоторую информацию надо - тогда на помощь приходят лямбды
пример
класс
<%
class ImportStatus
def self.success() new(:success) end
def self.redundant() new(:redundant) end
def self.failed(error)
new(:failed, error)
end
attr_reader :error
def initialize(status, error=nil)
@status = status
@error = error
end
def on_success
yield if @status == :success
end
def on_redundant
yield if @status == :redundant
end
def on_failed
yield(error) if @status == :failed
end
end
%>
функция
<%
def import_purchase(date, title, user_email)
user = User.find_by_email(user_email)
if user.purchased_titles.include?(title)
yield ImportStatus.redundant
else
purchase = user.purchases.create(title: title, purchased_at: date)
yield ImportStatus.success
end
rescue => error
yield ImportStatus.failed(error)
end
%>
вызов функции
<%
import_purchase(date, title, user_email) do |result|
result.on_success do
send_book_invitation_email(user_email, title)
end
result.on_redundant do
logger.info "Skipped #{title} for #{user_email}"
end
result.on_error do |error|
logger.error "Error importing #{title} for #{user_email}: #{error}"
end
end
%>
как это тестить - меняя значение глобальной переменной
<%
describe '#import_purchases' do
context 'given good data' do
it 'executes the success callback' do
# have global variable
called_back = false
import_purchase(Date.today, "Exceptional Ruby", "joe@example.org") do |result|
result.on_success do
# change status of global var
called_back = true
end
end
# test value of it
expect(called_back).to be_true
end
end
end
%>
------------------------------
ОБРАБОТКА ОШИБОК
------------------------------
если у функции пожет быть всего одно исключение,
делай rescue на все тело функции
<%
def foo
# do some work...
begin
# do some more work...
rescue
# handle failure...
end
# do some more work...
end
def bar
# happy path goes up here
rescue #---------------------------
# failure scenarios go down here
end
%>
если один метод имеет в себе несколько BRE-обработчиков
выведи их в отдельные методы. К ним еще можно добавить обработку целез лябд
------------------------------
ПОЛЕЗНОСТИ
<%
# переписывается как
file_path_copy = file_path && file_path.clone
@file_path &&= @file_path.clone
%>
------------------------------
------------------------------
------------------------------
------------------------------
------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment