Polymorphism unimplemented method. What people usually do? - ruby

If I have a model like this:
class Media
attr_reader :title, :created_at
end
class Video < Media
end
class Picture < Media
attr_reader :image_url
end
So, in the Picture class there is one attribute that should not exist in the superclass. What people usually do when you want to retrieve all the media? Would you want to throw exception on the method image_url? Or how would you re-model it?

If you're interested in all media, then you shouldn't care if it's a video or a picture. In other words, a method that accepts a collection of Media should not call any methods from Media descendants. If you need all pictures, then work with Picture, don't overgeneralize.
If you still do need to process all media while telling the types apart, you can branch on a type.
def process_media media
media.each do |m|
if m.is_a? Picture
# work with picture
elsif m.is_a? Video
# work with video
end
end
end
An alternative is to use duck typing. That is, if it has a method image_url, then it must be some kind of a picture.
def process_media media
media.each do |m|
if m.respond_to? :image_url
# work with picture
elsif m.respond_to? :bitrate
# work with video
end
end
end

Related

Same dataset-/filter-logic on different models in sequel (DRY)

I am building a sinatra web app with a sequel database backend. The primary tasks of this app is collecting status messages from different robots, store them in a database and provide various methods to view them. A common denominator in these messages is, that they provide a WGS84 position in lat/lon.
Now I want to provide various filters for querying messages based on their positions, but I want to write these filters only once, test them only once but re-use them in all model-classes with a lat/lon entry.
To boil it down to a very simple example:
Sequel.migration do
up do
create_table(:auvmessages) do
primary_key :id
Float :lat
Float :lon
String :message
end
create_table(:asvmessages) do
primary_key :id
Float :lat
Float :lon
Integer :chargestate
end
end
end
class Auvessage < Sequel::Model
dataset_module do
def north_of(lat)
self.where{ latitude > lat}
end
end
end
class Asvessage < Sequel::Model
dataset_module do
def north_of(lat)
self.where{ latitude > lat}
end
end
end
In both model classes have north_of(lat) to filter for messages which originate north of a given latitude. This function is fairly simple and you can easily repeat it two or three times, but what about more complex cases?
I have played around a bit with modules outside of dataset_module but nothing seem to be right.
Is there a preferred way how to re-use filters over different models? I have searched a lot, but didn't find any satisfying answer.
Edit:
To make my question a bit more precise: I want to move all functions like north_of(lat) (there are a lot more) into a service class. What I want to know now, is the best way to integrate that service class into a sequel-model:
"Just" include it?
Extend dataset_module, and if so, how?
Writing a dataset-plugin?
...
You can pass an existing module to dataset_module:
module NorthOf
def north_of(lat)
where{latitude > lat}
end
end
Auvessage.dataset_module NorthOf
Asvessage.dataset_module NorthOf
As a followup: I have taken #jeremy-evans answer and extended it by a parametrisation scheme for modules. So from now on I can test my filters by mocking and my model classes have just a list of includes in their dataset_module.
I like it.
As explanation my slightly modified example:
Sequel.migration do
up do
create_table(:auvmessages) do
primary_key :id
Float :lat
Float :lon
String :message
end
create_table(:asvmessages) do
primary_key :id
Float :gps_lat
Float :gps_lon
Integer :chargestate
end
end
end
module GPSFilter
def self.create(lat_name, lon_name)
Module.new do
include GPS
define_method :lat_col_name do
lat_name
end
define_method :lon_col_name do
lon_name
end
end
end
def north_of(lat)
where( "#{lat_col_name} > #{lat}" )
end
##### default parameters #####
def lon_col_name
"lon"
end
def lat_col_name
"lat"
end
end
class Auvmessage < Sequel::Model
dataset_module do
include GPSFilter
end
end
class Asvmessage < Sequel::Model
dataset_module do
include GPSFilter.create :gps_lat, :gps_lon
end
end
Here is a link to Uncle Bob's Screaming Architecture blog post which might be of help.
Now, answering your question, it seems that north_of, as well as many other methods, are actually part of your domain logic. This logic should not go in persistence abstractions, or controllers, or views, etc.
Design, build and write tests for the set of objects that solves your problem in the language of the domain of your problem. Then, you'll have at hand a rich set of functionality that you can simply use on Models, Controllers, CLIs, etc.
I usually put my service objects in a lib/ directory and write simple unit tests, without any of the persistence boilerplate that sets up test databases. They usually run very fast as well.

RSpec - how to test if object sends messages to self in #initialize

After reading this question I really do not like the answer.
Rails / RSpec: How to test #initialize method?
Maybe I am having a third scenario. This is what I have now, inspired by second code from that answer.
# Picture is collection of SinglePictures with same name and filename,
# but different dimensions
class Picture
attr_accessor :name, :filename
attr_reader :single_pics, :largest_width
def initialize(name, filename, dimensions=nil)
#largest_width = 0
#single_pics = {}
add_single_pics(dimensions) if dimensions
end
def add_single_pics(max_dimension)
# logic
end
end
describe '#initialize' do
it 'should not call add_single_pics if dimensions is not given' do
subject = Picture.new('Test Picture', 'Test-Picture')
expect(subject.largest_width).to eq 0
end
it 'should call add_single_pics if dimensions are given' do
subject = Picture.new('Test Picture', 'Test-Picture', 1920)
expect(subject.largest_width).to eq 1920
end
end
I really don't like this because I am testing the functionality of add_single_pics in #initialize tests. I would like to write somehow this in spec:
expect(subject).not_to have_received(:add_single_pics)
expect(subject).to have_received(:add_single_pics)
But I get
Expected to have received add_single_pics, but that object is not a spy
or method has not been stubbed.
Can I fix this somehow?
Spies are an alternate type of test double that support this pattern
by allowing you to expect that a message has been received after the
fact, using have_received.
https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/basics/spies
Only spy object can store the method calls. To test your real class in the way that you want, you have to use expect_any_instance_of statement before the class will be initialized:
expect_any_instance_of(Picture).to receive(:add_single_pics)
Picture.new('Test Picture', 'Test-Picture')
In this case your add_single_pics method will be called, but its logic will not be run, if you need to run it you need to call the and_call_original method on the matcher:
expect_any_instance_of(Picture).to receive(:add_single_pics).and_call_original

Best way to DRY up code using procs and blocks and/or dynamic methods

I am writing a way to parse websites, each "scraper" has it's own way gather information, but there is plenty of common functionality between two methods.
Differences:
One scraper uses Nokogiri to open the page via css selectors
the other scraper uses an RSS feed to gather information
Similarities:
each scraper creates an "Event" object that has the following attributes:
title
date
description
if for the Nokogiri scraper, we do something like this:
event_selector = page.css(".div-class")
event_selector.each_with_index do |event, index|
date = Date.parse(event.text) #code I want to share
end
for the RSS scraper, we do something like this
open(url) do |rss|
feed = RSS::Parser.parse(rss)
feed.items.each do |event|
description = Sanitize.fragment(event.description)
date = description[/\d{2}-\d{2}-20\d{2}/]
date = Date.strptime(date, '%m-%d-%Y') #code I want to share
end
end
^^ The date is grabbed via a regex from the description and then converted into a Date object via the .strptime method
as you can see each scraper uses 2 different method calls/ways to find the date. How could I abstract this information into a class?
I was thinking of something like this:
class scrape
attr_accessor :scrape_url, :title, :description, :date, :url
def initialize(options = {})
end
def find_date(&block)
# Process the block??
end
end
and then in each of the scraper methods do something like
scrape = Scrape.new
date_proc = Proc.new {Date.parse(event.text)}
scrape.find_date(date_proc)
Is this the right way to go about this problem? In short I want to have common functionality of two website parsers to pass the desired code into a instance method of a "scrape" class. I would greatly appreciate any tips to tackle this scenario.
Edit: Maybe it would make more sense if I say that I want to find the "date" of an event, but the way I find it - the behavior - or the specific code that is run, is different.
You could use an Event builder. Something like this:
class Event::Builder
def date(raw)
#date = Date.strptime(raw, '%m-%d-%Y')
end
# ... more setters (title, description) ...
def build
Event.new(date: #date, ... more arguments ..)
end
end
And then, inside the scraper:
open(url) do |rss|
builder = Event::Builder.new
feed = RSS::Parser.parse(rss)
feed.items.each do |event|
description = Sanitize.fragment(event.description)
date = description[/\d{2}-\d{2}-20\d{2}/]
builder.date(date)
# ... set other attributes ...
event = builder.build
# do something with the event ...
end
end
You should look into the Strategy or Template patterns. These are ways of writing code that does different things depending on some state or configuration. Essentially you'd write a Scraper class and then sub class it as WebScraper and RssScraper. Each class would inherit from the Scraper class all the common functionality but only differ in their implementation of how to get the date, description, etc.

Who can explain how this code block works: Carrierwave MD5 as filename

I am using the Carrierwave gem with a Sinatra app to upload and store images.
I am following How to: Use file's MD5 as filename and everything works as expected. However I do not understand how the following piece of code provided on the how-to page works:
class ImageUploader < CarrierWave::Uploader::Base
storage :file
def md5
chunk = model.send(mounted_as)
#md5 ||= Digest::MD5.hexdigest(chunk.read)
end
def filename
#name ||= "#{md5}#{File.extname(super)}" if super
end
end
I in particular do not understand what model.send(mounted_as)does, what the ||= operator means, and why the if super conditional is used (and what it does).
Could somebody please explain this to me?
Say, for example the model is Person and the ImageUploader is mounted as avatar.
class Person < ActiveRecord::Base
mount_uploader :avatar, ImageUploader
end
Then, the md5 method would be calling something to the affect of chunk = person.avatar and using this to calculate the hash of the file contents, which you want for the name.
The filename method checking to see if there is a #filename instance variable, as defined in the CarrierWave::Uploader::Store class.
class CarrierWave::Uploader::Store
def filename
#filename
end
end
Then calling this again to get the filename extension to use in the constructed filename. The #name variable is then just a temporary cache of the name, so that future calls to the method do not require the whole thing to be calculated again.
Edit:
The carrierwave uploader has methods/instance variables for the model (eg Person instance) and declared mount point in the model (eg avatar). These are from the mount_uploader declaration in your active record model.
module CarrierWave::Uploader::Mountable
attr_reader :model, :mounted_as
def initialize(model=nil, mounted_as=nil)
#model = model
#mounted_as = mounted_as
end
end
These are used for various things, as well as being made available for us to do things just such as you are trying. It is just an abstract way to call person.avatar, which returns the file (File instance not string path). This is then read into the MD5 lib, which gives the hexdigest.
Rewriting this in more plain terms
class ImageUploader < CarrierWave::Uploader::Base
def md5
uploaded_file = model.send(mounted_as) # person.avatar (File instance)
#md5 ||= Digest::MD5.hexdigest(uploaded_file.read) # hexdigest of file content
end
end

Changing associated objects don't get save with rails model object?

having this code block of an example rails model class:
class Block < ActiveRecord::Base
has_many :bricks, :autosave => true
def crunch
bricks.each do |brick|
if brick.some_condition?
brick.name = 'New data'
brick.save # why do I have to call this?
end
end
end
end
class Brick < ActiveRecord::Base
belongs_to :block, :autosave => true
end
I found that the only way to make sure the changes within the associated objects get saved for me, was to call brick.save manually. Even thought I use :autosave => true
Why?
Probably the autosave option has a misleading name. By the way, it's the expected behaviour. The option is meant for association. So if you modify an object in a relation and save the other object then ActiveRecord saves the modified objects. So, in your case, you could change your code to:
def crunch
bricks.each do |brick|
if brick.some_condition?
brick.name = 'New data'
end
end
save # saving the father with autosave should save the children
end
You could use any of the helper methods available: update_attribute, update_attributes, update_column...
More info: Rails: update_attribute vs update_attributes

Resources