I just finished reading Confident Ruby for the second time and wanted to capture the lessons from it. The first time I read this book was (i think) beginning/mid 2013. I am really happy I went through it again, I learned some really good things to add to my toolbelt.
- Cast your inputs and outputs – In order to avoid constantly checking for Nil, Integer, Array, etc, just cast or munge the data you have into the format you need. For instance, instead of returning nil, a single item or a collection, be nice and return Array(return_value). It will help the user on the other side be more certain of what they are dealing with. The oppositie is true when you are receiving data, cast it so that you are certain what you are dealing with and that you are handling that assertion of format in one place.
- There are several built in conversion methods, use the one that makes the most sense for the problem you are solving. For instance, if your system depends on something being a float, cast it using Float(num_or_string) otherwise use the more gracious to_f.
- Prefer Fetch with Blocks – It seems like a great deal of work in web apps relates to dealing with hashes. Instead of using the standard hash[‘x’], use hash.fetch(‘x’) { default_value_or_raise }. You can even use the raise value for if the hash does not have a value to help you find the error when you run into it in the future. hash.fetch(‘x’) { raise :this_symbol_is_easy_to_find }. An added benefit of using blocks, is that they are not evaluated until the fetch fails, whereas if you do hash.fetch(‘x’, expensive_operation). It is evaled when the fetch executes.
- &&= – I use ||= constantly to memoize truthy values. Advi demonstrates using the &&= operator to assign a value to a nested attribute if the first one is present. For instance foo &&= foo.clone ensures that a foo exists before you call the clone on it.
- Segregate Actions – Your code is generally doing one of 4 things; Collecting Input, Performing Work, Delivering Output, Handling Failures. Keep those separate and when you are done with one, be done with it.
- Replace String With Class – As best as you can, express domain logic using Classes. Using strings or hashes is quick, but the class will help you clarify the domain and you have the side benefit of Polymorphism to handle a lot of switching. A great example of this is a Parameter Object. In the book you start with a drawing a point on a map to represent a location. This quickly expands to a FuzzyPoint (a point with a radius around it) and a StarPoint. The classes allow the encapsulation of all of the uniqueness of these.
- Yield Builder Objects (197) – Instead of locking users into an api implementation, allow for a block to be passed in and yield to the block in builders. This allows them to configure stuff you may not have accounted for. For instance, when you create a PointBuilder to create the things above. If it accepts a block where you yield the point to it, you can accept things like PointBuilder.new(x: 5,y: 7) { |p| p.maginitude = 75 }. Your PointBuilder automatically can handle SOOO many more cases.
- Document Assumptions – When assuming something will be a certain way, document it using code. I.e. cast float using Float or if you want a collection use Array. This will increase clarity for you and other programmers.
- Receive Policies Instead of Data (206) – When you have different ways you want to handle edge cases (for instance, return false, log differently, raise in some cases), instead of passing in flags for :ignore_errors, :log_stuff, :raise prefer to accept a block that specifies what you want to happen. The in the rescue area you can do something like if block_givin? then yield(thing, error) else raise end
- Call back instaed of returning (211) – A callback on success is more meaningful than a true false return value. In the book we have a import_purchase method that is used a lot. That import_purchase cna accept a block and on_success, on_reduntant, on_fail yield itself to the block along with a the message (on_success, etc). The block passed in then defines what happens on those cases. Completely inverts the control so that instead of hard wiring in what is going to happen, you dictate it view the block that is passed in.
- Catch and Throw – This is a concept i see somethings, always obscurely. But you can create a block catch(:done) method end. Then if you throw(:done) it will go up the call stack until there is something that catches it and continue executing below it.