Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

start with OOP #5

Merged
merged 1 commit into from
Jun 9, 2020
Merged

start with OOP #5

merged 1 commit into from
Jun 9, 2020

Conversation

Gusarov2k
Copy link
Owner

No description provided.

movie.rb Outdated
attr_accessor :link, :original_title, :year, :country, :release_date, :genres, :runtime, :popularity, :film_director,
:stars

def initialize(params = {})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше использовать keyword arguments.
Hash of params это очень старый трюк, в современном руби к нему лучше не прибегать.

movie.rb Outdated
movies.each do |movie|
puts "#{movie.original_title} (#{movie.release_date.strftime('%d - %B - %Y')} " \
"#{movie.genres}) - #{movie.runtime}"
def has_genre?(param)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше стараться аргументы называть конкретно по смыслу, а не «аргумент», «параметр» и т.д.


class MovieCollection < Movie
require 'date'
require 'csv'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require лучше оставить снаружи класса (в начале файла), а то оно выглядит не так как ведёт себя (как будто включит CSV только внутрь класса, но это не так)

attr_accessor :movies

def initialize(file_name)
input_arg = ARGV[1]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет, если мы пишем библиотечный класс, лучше ему не работать с ARGV (пусть это делает скрипт его использующий), иначе его будет неудобно использовать.
Пусть просто принимает имя файла который открыть (и держит «имя файла по умолчанию» в константе)

CSV.read(file_name, col_sep: '|',
headers: %i[link original_title year country release_date
genres runtime popularity film_director stars],
converters: %i[date integer float]).map { |row| @movies.push(Movie.new(row.to_hash)) }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

нет, push не нужен тут :) надо подумать как тут правильно использоать map.
И кстати список заголовков (да и список конвертеров) можно вынести в костанты, тогда код будет легче читать.


def sort_by(param)
if movies.first.send(param).is_a? Date
films = movies.select { |val| val.send(param).is_a? Date }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

На самом деле это не очень хороший ход — а если как раз у первого фильма дата не распарсилась?
Я бы скорее усложнил немного парсинг даты в фильмах (там же её не то чтобы нету, просто у некоторых она только годом, её вполне можно превратить в «1 января того года» просто)

films = movies.select { |val| val.send(param).is_a? Date }
films.sort_by { |val| val.send(param) }
else
movies.sort_by { |val| val.send(param) }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort_by(&param) можно просто.
кстати, что сделает этот код при сортировке по длительности фильма?

end

def filter(param)
if param.keys.first == :genres || param.keys.first == :country
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не, должно со всеми полями работать :)
И кроме того:

  1. работать когда фильтров несколько: filter(year: 2001, country: 'USA')
  2. работать с паттернами, например: filter(year: 2000..2005, title: /terminator/i) (тут пригодится метод ===)

end

def stats(param)
movies.group_by { |movie| movie.send(param) }.transform_values(&:count).sort_by(&:last)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А что будет при попытке посчитать статистику по актёрам, или по жанрам? ;)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

↑ На этот вопрос по-моему всё ещё нету ответа :)

.rubocop.yml Outdated
@@ -3,3 +3,12 @@ Metrics/LineLength:
StyleGuide: '#80-character-limits'
Enabled: true
Max: 120

Metrics/MethodLength:
Max: 40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это очень много :)

.rubocop.yml Outdated
Max: 40

Metrics/AbcSize:
Max: 50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это очень-очень много. Для учебного кода 20 — это в общем норм, больше не стоит. Если метод становится слишком сложным, надо его упрощать и/или разбивать на другие методы.

movie.rb Outdated
@runtime = args[:runtime]
@popularity = args[:popularity]
@film_director = args[:film_director]
@stars = args[:stars]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если мы знаем, какие у метода будут аргументы с ключами, это имеет смысл так и описать (keyword arguments):

def initialize(link:, original_title:, year:...)

Во-первых, код станет проще внутри метода, во-вторых по описанию метода сразу будет видно чего он ожидает, в-третьих, станет невозможно его вызвать неправильно (с не теми аргументами)


HEADERS_KEYS = %i[link original_title year country release_date genres runtime popularity film_director stars].freeze
CONVERTERS_KEYS = %i[date integer float].freeze
INPUT_ARG = ARGV[1]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет, это неверно :) Константа — это константа, не то что имеет право меняться при каждом запуске.

val.release_date = Date.parse(val.release_date + '-01')
elsif val.release_date.is_a? Integer
val.release_date = Date.new(val.release_date)
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Эту логику (как правильно хранить дату) должен взять на себя фильм, коллекции для этого никакого дела нету.

def stats(param)
special_keys = %i[genres stars]
group = if special_keys.include? param
movies.group_by { |movie| movie.send(param).split(',').shift }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хранить поля, для которых это релевантно, в виде массивов — это тоже дело фильма. У коллекции тут должен быть очень простой и обобщённый код.

movie.rb Outdated
elsif release_date.is_a? Integer
Date.new(release_date)
end
@genres = genres.split(',').shift
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а shift зачем?..

end
def actors
stars.split(',')
end
Copy link

@zverok zverok Jun 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем этот метод вообще? (если зачем-то таки нужен — можно решить через alias просто... или переименовать колонку исходную :))

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

↑ Этот комментарий пропущен.

movie.rb Outdated
genres runtime popularity film_director stars],
converters: %i[date integer float])
.map { |row| OpenStruct.new(row.to_hash) }
def genre?(movies, param)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это не соответствует заданию :) «Клиент» этого кода не должен думать что туда передать — он просто спрашивает movie.genre?('Drama')
Чтобы у фильма была возможность узнать какие жанры вообще бывают, имеет смысл чтобы коллекция, при конструировании фильма, передавала ему себя — тогда фильм может хранить в своей переменной ссылку на всю коллекцию, и при необходимости спрашивать у неё «какие вообще жанры существуют?»

else
params[key].member? d.send(key)
end
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Вместо params.keys.all do |key| и потом использовать param[key], мы можем использовать params.all? do |key, value|
  2. Логика тут запуталась. param[key].is_a?(Array) — какой случай это поддерживает, какой код? Работает ли сейчас filter(title: /terminator/i)? И т.д. Правильная логика — а) использовать === для сравнения (тогда и регулярные выражения, и диапазоны, и простые строки будут работать) и б) нам нужны всего две ветки if — для полей в которых массив (типа stars — stars: /Schwarzeneg/ тоже должно работать), и для всех остальных полей.
  3. А ещё лучше вынести проверку фильтра в фильм — метод выглядящий как movie.matches?(field, pattern) — тогда в коллекции будет совсем простой код, а в фильме он тоже будет довольно несложный и его легко будет протестировать.

end

def stats(param)
movies.group_by { |movie| movie.send(param) }.transform_values(&:count).sort_by(&:last)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

↑ На этот вопрос по-моему всё ещё нету ответа :)

.rubocop.yml Outdated
Max: 20

Metrics/ParameterLists:
Max: 11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше CountKeywordArgs: false (тогда он не будет ругаться ТОЛЬКО на методы с keyword args, что логично — их можно и две дюжины сделать без потери понятности)

demo.rb Outdated
movies = MovieCollection.new('movies.txt')
movies.sort_by(:release_date)

movies.filter(country: 'USA', year: 2001..2002, runtime: '92 min')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

К слову, лучше бы runtime тоже хранить числом, потому что более-менее реальное применение этого фильтра — что-то вроде «хочу посмотреть фильм не больше 1.5 часов, т.е. runtime: 60..90 (а «хочу посмотреть фильм длиной 92 минуты» — не очень реальное :))

end

def filter(params)
movies.select { |d| params.all? { |key, _| params[key] === d.send(key) } }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ — это как раз то значение, которое params[key], тут вы сами себе жизнь усложняете :)
А работает ли этот код с жанрами? И скажем с актёрами?

movies.map { |movie| movie.send(param) }.flatten.group_by(&:itself).transform_values(&:count).sort_by(&:last)
else
movies.group_by { |movie| movie.send(param) }.transform_values(&:count).sort_by(&:last)
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вместо двух веток, во многих случаях можно просто обобщить код. Например, flat_map(&param).group_by(&:itself) сработает для всех вариантов содержимого поля.

Copy link

@zverok zverok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

уже близко :)

end
def actors
stars.split(',')
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

↑ Этот комментарий пропущен.

headers: HEADERS_KEYS,
converters: CONVERTERS_KEYS).map do |row|
row[:all_movies] = self
Movie.new(row.to_hash)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно, во-первых, вот так:

Movie.new(all_movies: self, **row.to_hash)

(** это «распаковать keyword args из этого хеша»)
а во-вторых лучше всего сделать коллекцию единственным НЕ-keyword аргументом, чтобы сразу видна была разница:

# в Movie
def initialize(collection, link:, original_title: ....)

# вызов в коллекции:
Movie.new(self, row.to_h)

добавление методов

change methods

change methods

change methods

changes

change method

refactoring
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants