And a thanks to my employer, Kiip, for being supportive of my open source endeavors. We’re hiring! http://kiip.me
Big shout out to Engine Yard for sponsoring my open source work and supporting me in helping the Ruby community.
WARNING! OPINIONS AHEAD This talk has many opinions throughout. I try to back up my opinions with some evidence but of course there will be differing thoughts. My opinions are based on over t wo years of maintaining relatively popular Ruby libraries, as well as being a full-time Ruby dev for over five years.
What makes a good Ruby library? A Good Most people think “easy to use” or “simple” or “clean API.” But there is a lot more going on that is equally important. Ruby Library?
✓ Intuitive API Clearly, API matters. Most devs jump straight to code samples for libraries, and a clean API will be the first sell.
Intuitive API everyone always gets. But there are a LOT of other parts to But... a good Ruby library. Let’s go over them quickly, then in more detail later. The Other Parts
✓ Configurable Almost all libraries have tunable elements. Exposing those tunable elements in a predictable way is important to give your library the most power, while retaining its simplicity.
✓ Logged Easily overlooked, when a library DOESNT work, users of a library would like to know whats going on without diving straight into the source code. Properly structured log messages are a godsend.
✓ Exceptions If I’m using an HTTP library and make a request, I don’t want to see a SocketError pop up. I didn’t make a socket, how did I error? Libraries should catch their exceptions, wrap them in their own, and expose them in a documented way. More on this later.
✓ Documentation The biggest problem with open source. A library needs documentation.
✓ Support Users of your library need a way to get support if they need it.
So these are the boring parts. No one talks about them. The Boring Parts No one talks about them.
All of these things put together make a FANTASTIC library. A Good Ruby Library. Whole is greater than the sum of its parts.
1. Intuitive API
* Don’t do anything crazy, just stick I’m not going to spend much time on with the norms. this section because this is generally the part that people get right, * Ruby has been around for a long because they’re used to working with time now, we’ve got our conventions. Ruby already and this is what they focus on. * Helps people pick it up faster. Plus, during the talk I’m super * Gives users that “it just feels right” crunched for time, so I had to move feeling. on. Idiomatic Ruby Don’t try to be cool.
* Don’t try something fancy, configuration should be non-magical and straightforward. * Use plain Ruby (APIs), or maybe JSON/YAML for tools. Plain old Ruby Config should have NO MAGIC.
Central configuration Keep it in one place. * Don’t try something fancy, configuration should be non-magical and straightforward. * Use plain Ruby (APIs), or maybe JSON/YAML for tools.
Central Configuration 1 require "mylib" 2 3 MyLib.configure do |config| 4 config.setting = "value" 5 config.enable_something! 6 end * Central configuration * Plain old ruby object (`config`)
Instance configuration via initializers and option hashes Idiomatic, well understood. * Don’t try something fancy, configuration should be non-magical and straightforward. * Use plain Ruby (APIs), or maybe JSON/YAML for tools.
Instance Configuration 1 require "mylib" 2 3 obj = MyLib.new(:setting => "value") 4 obj.setting = "value2" * Use normal constructors and option hashes for instance-level configuration. * Very idiomatic, very clear.
Ruby’s stdlib Logger is GARBAGE. * Lack of namespacing. * Only goes to IO objects Basically renders logging useless in my opinion.
Usage: Your API 1 log = Log4r::Logger.new("mylib::connection") 2 log.debug("debug!") 3 log.info("information") This is the API that you would use as a developer of a library. Simple enough.
Usage: User API 1 log = Log4r::Logger.new("mylib") 2 log.outputters = 3 Log4r::Outputter.stderr 4 log.level = Log4r::INFO This is what the user of your library would have to do to enable logging for your library. Super easy, predictable.
Don’t: * stdout/stderr: Tempting, but extremely annoying and can cause crashes if pipe is closed. * “debug” option is fine, but don’t require it. Sometimes I want debug logging, but not other debug behavior. • Print to stdout/stderr * log4r may not be the best, but the last thing we need is more fragmentation. • Rely on “debug” config option • Use something custom
Without Exceptions: Abstraction Leakage * Lack of properly handling exceptions causes abstraction leakage. * Goal: Catch any possible exceptions, wrap and expose using your own.
Abstraction Leakage 1 require "twitter" 2 3 Twitter.socialize! Example: Let’s say you’re doing something with Twitter. It doesn’t matter what, it’s just important to know that there is net work activity going on.
Bad: Result ! ruby app.rb app.rb:in `initialize': unknown socket domain: twitter.com (SocketError) ! from app.rb:in `new' ! from app.rb:in `<main>' This is the API that you would use as a developer of a library. Simple enough.
Single Parent Exception Class * Allows a “catch all” for a specific library very easily. * Can find all children classes easily. * Inherit from StandardError, so that catch-alls still work on that.
Single Parent Class 1 require "twitter" 2 3 begin 4 Twitter.socialize! 5 rescue Twitter::Error 6 # Twitter is always down so 7 # I stopped caring. 8 end A single parent class makes it easy to catch-all in a specific library, such as this case.
* Don’t be afraid, make a lot! The user can always “catch all” * Make the name descriptive, makes it a lot easier to read code. Many Subclasses Be descriptive!
Descriptive Subclasses ! ruby app.rb app.rb:in `initialize': Failed to connect to twitter (Twitter::ConnectionError) ! from app.rb:in `new' ! from app.rb:in `<main>' With descriptive, specific error classes, its easy to see what is going on and to catch that specific example.
* Don’t reinvent the wheel: FooNotEnoughArguments, Use Standard just use ArgumentError There aren’t many available, but the basics are good. Exceptions When it makes sense.
* Two kinds: API docs (libraries) and usage docs (tools) * Both should be kept in the same repo as the code, ideally. * Write it AS YOU CODE. Same as tests. 5. Documentation
* Its easy to push off docs until the end so your lib isn’t changing much anymore, but documentation is SO BORING that you’ll end up hating yourself. Write it from * Write it as you go (like tests). If the library changes, then you’ll have to change docs, so there is more work, but at least you’ll have the docs at the end. * Example: Vagrant 0.1 docs pushed off until the end, took me 2 weeks to write the beginning. them. Really burned me out. More work, but less pain.
* Docs should be in the same repo as the code. * Use a branch, won’t ever be merged into master, so clean it and Keep it just keep it going. * Makes it easy to contribute. (Pull requests!) * Can see “point-in-time” documentation for specific versions with the code.
* API docs, use YARD * Similar to RDoc/JavaDoc, so its familiar. * Generates pretty HTML, PDFs. * You automatically get generated docs from rubydoc.info API: Use YARD http:/ yardoc.org
* For other docs, use plain old markdown, but try to fit it into a Jekyll-powered site. * Two birds, one stone: Other: - Readable from the source. - Able to generate a pretty website. Markdown + Jekyll Readable + Website!
* If I don’t see support, I either don’t use the project or I use the code as reference to implement something myself. Unsupported code is not “open source” It’s just reference material.
Support doesn’t have to equal work Providing support doesn’t mean you need to work much harder. Its simply more important to provide the opportunity for support.
Support just needs to be SOMETHING. Users just need something. IRC, mailing list, issues, etc.
With all these things put together, you don’t get a good Ruby library, you A Great get a _great_ Ruby library. From learning about your library to developing with it to maintaining it, everyone will thank you. Ruby Library.