class: title, center, middle Ruby Idioms You're Not Using Yet ================================ ## Craig Buchek ## ## RubyConf 2014 ## ### November 17, 2014 ### https://booch.github.io/presentations/ruby_idioms/slides.html ??? There's a URL you can use to follow along. --- About Me ======== Rubyist since 2005 Independent contactor * Rails rescue projects * Agile techniques This Agile Life podcast ??? Occasional forays working with other languages. This just makes me appreciate Ruby more. Always looking for contract work. --- What Is An Idiom? ================= 1\. An expression that cannot be understood from the meanings of its separate words but that has a separate meaning of its own. ??? First I want to talk briefly about what an idiom is. The dictionary gives us sevaral definitions. Most people seem to pick this first definition when talking about idioms in programming languages. --- What Is An Idiom? ================= 2\. A style or form of expression that is characteristic of a particular person, type of art, etc. ??? But I think this definition is closer to what we mean. --- What Is An Idiom? ================= My definition: A way in which we normally express ourselves in a language. --- Non-Idiomatic Ruby ================== ~~~ ruby for i in 1..5 puts "Craig is great" end ~~~ ~~~ ruby def expirationDates currentYear = Date.today.year i = 0 dates = [] while i <= 20 dates.push(currentYear + i) i += 1 end return dates end ~~~ ??? I can tell that these were written by someone not very familiar with Ruby. Maybe they're "writing Java in Ruby". Not only is the 2nd one longer than it needs to be, but it takes more effort for me to understand what it means. --- Idiomatic Ruby ============== ~~~ ruby 5.times do puts "Craig is great at Ruby" end ~~~ ~~~ ruby def expiration_dates current_year = Date.today.year [current_year .. current_year + 20] end ~~~ ??? Here are a couple simple examples of idiomatic Ruby. I can tell that the person who wrote these is at least somewhat familiar with Ruby. --- Idiom Or Pattern? ================= What's the difference? ??? This is something I've struggled with. Is there a difference? I don't know. People disagree. --- Pattern ======= A template for a common solution to a common problem ??? A pattern is just a template for a common solution to a common problem. Patterns aren't just for design and architecture level issues. For example, Kent Beck's "Smalltalk Best Practice Patterns" talks about patterns at the method level. So I'm going to treat the 2 terms somewhat interchangably. --- Why Use Idioms? =============== They're tools that Ruby gives you They're tools that Rubyists can help you with ??? They're not necessarily tools that Ruby gives you *on purpose*. Often, Ruby is optimized to make idiomatic versions work better than non-idiomatic. --- Memoization =========== ~~~ ruby def yesterday @yesterday ||= Date.today - 1 end ~~~ ??? We'll start out with a simple one. Hopefully most of us here use this one pretty regularly. If the instance variable is set to something that's not falsey, then the instance variable will be returned. Otherwise the calclation will be performed, stored into the instance variable for next time, and returned. This won't work well if your caluclation might return false or nil. --- Memoization =========== Use to cache "expensive" operations * CPU * I/O Don't use without thinking! ??? Sometimes you really do need to recaluclate every time. In my example on the previous slide, if your process might run for more than 1 day, you'll have a bug -- and it'll be hard to track down. --- Memoization =========== Use a gem? What about parameters? ??? There's a gem, but for the basic case, it's not worth bothering. When you have to cache based on parameters, you'd need to store in a Hash. This happens a lot less in Ruby -- probably due to extracting objects. --- Fetch with Default Block ======================== ~~~ ruby RV = ENV["RUBY_VERSION"] || "2.1.4" RV = ENV.fetch("RUBY_VERSION") # Raises KeyError if not found RV = ENV.fetch("RUBY_VERSION") { "2.1.4" } RV = ENV.fetch("RUBY_VERSION") { raise "Must specify RUBY_VERSION" } ~~~ ??? This is a simple one. Comes from Avdi's blog and Ruby Tapas. http://devblog.avdi.org/2009/03/16/go-fetch/ This works for `Hash`es, `Array`s, and `Hash`-like objects, such as `ENV`. --- Fetch with Default Block ======================== ~~~ ruby RV = ENV["RUBY_VERSION"] || "2.1.4" RV = ENV.fetch("RUBY_VERSION") # Raises IndexError if not found RV = ENV.fetch("RUBY_VERSION") { "2.1.4" } RV = ENV.fetch("RUBY_VERSION") { raise "Must specify RUBY_VERSION" } ~~~ ??? Each has its pros and cons: * First is easiest, if you know the hash won't contain a `nil` or `false` * Or of you don't need to distinguish * Second is helpful to ensure you don't pass a `nil` around * Prevents NoMethodError at a later time * Third is best for cases where missing, `nil`, and `false` need to be handled differently * Fourth is good for failing fast and being explicit about how to fix the problem Avdi's advice is to default to the 3rd over the 1st, unless you think about it. --- Fetch with Default Block ======================== Alternative ----------- If you're the one creating the `Hash`, you can new it up with a different default: ~~~ ruby h = Hash.new { :not_found } h[:anything] # => :not_found ~~~ ??? Note that `Array` does not have this feature (returning a default for an undefined element). --- Tag Module ========== A mix-in module whose sole purpose is to add a type to an object. ??? This one comes from Avdi Grimm's "Exceptional Ruby". In Ruby, about the only place we idiomatically test for types is exception handling. I'm open to other ideas for uses of type tagging though. --- Tag Module ========== ~~~ ruby module MyLib module Error; end def some_method do_something rescue StandardError => error error.extend(MyLib::Error) raise end end ~~~ Now the caller can rescue on `MyLib::Error` or the original exception. --- Page Object =========== Use an object to represent a screen within your app Methods represent actions you can perform on the screen You can also ask questions about elements on the page ??? This one is mainly used for full-stack acceptance test. Abstracts away the actions, so you don't have to specify details. --- Page Object =========== ~~~ ruby Given /^I have logged in$/ do @home_page = LoginPage.new.login(USERNAME, PASSWORD) end class LoginPage include Capybara::DSL def initialize visit "/login" end def login(user, password) fill_in :user, user fill_in :password password click 'login' HomePage.new(page) end end ~~~ ??? Makes cucumber steps more object-oriented instead of procedural. Reduces the problem in Cucumber of having tons of global methods (in "worlds"). --- Test Spies ========== I've had a long hatred of mocking It doesn't fit the patterns: * Setup, Exercise, Verify, Teardown * Arrange, Act, Assert * Given, When, Then ??? My hatred of mocking might not be for the reason that you expect. What I hate is that a mock makes an assertion in the arrange section. --- Test Spies ========== ~~~ ruby require "rspec" RSpec.describe "An invitiation" do let(:invitation) { double("invitation", deliver: nil) } it "should be delivered" do expect(invitation).to receive(:deliver) invitation.deliver end end ~~~ ??? The expectation is in the "wrong" place. This prevents reuse of the setup (`before` block in RSpec). --- Test Spies ========== ~~~ ruby RSpec.describe "An invitiation" do let(:invitation) { double("invitation", deliver: nil) } it "should be delivered" do invitation.deliver expect(invitation).to have_received(:deliver) end end ~~~ ??? Now we can exercise the code in a common `before` block if we want. (Although exercise is more of a "during".) And the assertion only runs in one place. --- Allow Class or Instance ======================= ~~~ ruby date1 = Date.new(2014, 11, 17) date2 = Date.new(2014, 11, 17, Date::Calendar::Julian) julian_1st = Date.new(Int64::MIN, Date::Calendar::Julian) gregorian_1st = Date.new(1752, 9, 14, Date::Calendar::Gregorian) default_calendar = Date::Calendar.new(julian_1st, gregorian_1st) date3 = Date.new(2014, 11, 17, default_calendar) ~~~ ??? This one I discovered when writing the Date class for the Crystal standard library. For `date3`, we're passing an instance. For `date2`, we're specifying the class. I'm honestly not sure if this is a very good idiom or not. But it seemed useful at least in the case of this API. --- Allow Class or Instance ======================= ~~~ ruby class Date def initialize(year, month, day, calendar = Date::Calendar.default) calendar = calendar.new if calendar.is_a?(Class) ... end end ~~~ ??? The code just news up an instance of the class if it's found to be a class. In this case, the Julian and Gregorian claendars have different behavior, so we need them to be separate classes. Alternatively, we could make them constants that are instances of the class. But then we'd have to give them singleton methods. TODO: Show an example where we chose singleton methods? --- Configuration Query =================== Ask a question about the configuration ??? Really just a case of good abstraction and API design. We take the "normal" config query up a level of abstraction. --- Configuration Query =================== ~~~ ruby ExtraLogging.for_applicant?(applicant_id) ~~~ ??? Note that I'm not asking about the config file. I'm asking about the higher level info that the config file can provide. --- Configuration Query =================== ~~~ ruby class ExtraLogging def self.for_applicant?(applicant_id) applicant_ids.include?(applicant_id.to_s) end def self.applicant_ids @applicant_ids ||= config.fetch('applicant_ids'){[]}.map(&:to_s) end def self.config @config ||= YAML.load_file(CONFIG_FILENAME) end def self.reload @applicant_ids = nil @config = nil end end ~~~ ??? I'm using several other idioms here: * Defining a class method with `self` * Memoization * Unmemoization (cache busting) * Fetch with default block * Functional programming with arrays --- Module Factory ============== First, a story of discovery ??? This is the idiom that instigated this presentation. --- Module Factory ============== ~~~ ruby class User include MyORM::Model table 'people' end ~~~ Isn't `table` more like an option to the module being included? --- Module Factory ============== But this isn't valid Ruby: ~~~ ruby class User include MyORM::Model, table: 'people' end ~~~ ??? I looked for a solution for a long time. --- Module Factory ============== Then I saw Virtus do this: ~~~ ruby class User include Virtus.model(constructor: false, mass_assignment: false) end ~~~ --- Module Factory ============== Uses the Factory Pattern to create a module Configures the created module per the parameters we pass it --- Module Factory ============== ~~~ ruby module Virtus def self.model(options = {}) Module.new.tap do |mod| if options.fetch(:constructor) { true } mod.send(:include, Model::Constructor) end end end end ~~~ ??? We're using several other Ruby idioms here: * Defining a class method * Modifying an object in-line with `tap` * Fetch with Default Value * Options Hash * Accessing a private method with `send` We can do a lot of different things with configuration: * Take positional parameters * Take a block * Use the Builder Pattern If we're going to call the factory more than a few times, we'll probably want to memoize modules for reuse where possible. --- Module Factory ============== Other names for this: - Parameterized Module Inclusion ??? My buddy Amos wants me to remind you that method names can start with an uppercase. --- Circuit Breaker =============== Stop trying to access an inaccessible resource. * Slow or unresponsive network resource * A queue has been filled up * A thread pool or connection pool that's been exhausted ??? This one comes to us by way of Martin Fowler (of "Patterns of Enterprise Application Architecture" fame) and Michael Nygard (from "Release It!"). * Reduces resources tied up in operations which are likely to fail. * Reduces cascading failures due to resource issues. --- Circuit Breaker =============== ~~~ ruby def do_something make_external_call unless circuit_broken? rescue Timeout::Error break_circuit raise $! end def circuit_broken? @circuit_broken ||= false end def break_circuit @circuit_broken = true end def reset_circuit @circuit_broken = false end ~~~ Would probably use an object and a block to implement. ??? https://github.com/wsargent/circuit_breaker/tree/master --- Circuit Breaker =============== Lots of additional options available: * What to do when the circuit is broken * Drop the request * Raise an exception * Use a promise to return a result later * Conditions for deciding when to flip the breaker * Thresholds * Conditions to automatically reset the circuit * Logging and alerting --- Blue-Green Deployment ===================== You have 2 complete production environments One is live, the other is "previous" or "next" Deploy to one while the other is still running After smoke testing the deploy, swap the live with "next" Very easy to switch back to "previous" ??? This is a deployment pattern. I'd like to see more Rubyists adopt this, as they have Continuous Deployment. The only tricky part is database integrity. --- Conclusions =========== Learn idioms -- they're great tools Ruby idioms help us make code more readable Our idioms are not static Be on the lookout for new idioms ??? I hope you'll think about using some of these. The main take-away here is that we can invent or discover new idioms that fit with our existing idioms. --- Further Investigation ===================== Honorable mentions: - Config blocks - DSLs - Flip-flops - Block or value - Enumerable / Enumerator - More OOP/OOD - More refactoring - Keyword arguments - Soft typing - Other languages --- References ========== * Confident Ruby by Avdi Grimm * Exceptional Ruby by Avdi Grimm * SmallTalk Best Practice Patterns by Kent Beck * Martin Fowler's Bliki ??? Most of SmallTalk patterns and idioms apply to Ruby. See Avi Bryan'ts talk at RailsConf 2007 (I think) on SmallTalk and Ruby. --- Credits ======= * Memoization - suggestion from James Edward Gray II * Tag Module and Fetch - Avdi Grimm * Page Object - Selenium developers * Spies - RSpec developers * Config Query - Kyle Stevens and myself * Module Factory - Virtus developers * Don Morrison (elskwid) * Piotr Solnica (solnic) * Circuit Breaker - Martin Fowler * Blue-Green Deploymnet - Martin Fowler ??? These are where I found them, not necessarily the origin. http://martinfowler.com/bliki/CircuitBreaker.html http://martinfowler.com/bliki/BlueGreenDeployment.html https://code.google.com/p/selenium/wiki/PageObjects --- Colophon ======== Slide-show software used is [Remark][1]. [1]: https://github.com/gnab/remark --- Feedback ======== @CraigBuchek github.com/booch craig@boochtek.com