Created
December 7, 2018 19:10
-
-
Save kopylovvlad/ac8f873b72fd5310c2013341154d26f7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
------------------------------ | |
******************** | |
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,[email protected],123,2012-06-18 | |
Tom Servo,[email protected],456,2011-09-05 | |
Crow T. Robot,[email protected],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", "[email protected]") 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