Rebased Team writing about tech we use.

Languages, frameworks, libraries,
tools. Certified for 0% fluff.

101: Advanced OOP structure

Piotr Szmielew

on
31 Aug 2018

Welcome to the first blog post in our cycle directed to less-experienced developers. We will be diving into the world of more advanced concepts - however, each one should be easily digestible by junior+ - mid developer. It’s one of the ways we want to give back to the community.

In this blog post we will focus on solving a simple task:

Retrieve a random quote from API https://talaikis.com/api/quotes/random/ and display it in terminal (in some pretty format).

What is the catch here? We want our code to be thoroughly tested and use proper Object Oriented Programming (OOP).

Side note: the techniques shown here will be a case of overengineering, given the complexity of the task

Let’s start coding! How about starting with some simple code that will do the job well?

it is barely testable (it would require a lot of mocking monkey-patch-style)

it isn’t very object-oriented

So, let’s start some refactoring! Let me stress again that we will be doing overengineering here.

Let’s look at our code and start by wrapping it all in a class and changing print to simply returning string (always try to separate side-effects of functions as much as you can):

require'bundler/inline'gemfiledosource'https://rubygems.org'gem'httparty'endrequire'json'classQuotedefrandom_quoteserver_raw_output=HTTParty.get('https://talaikis.com/api/quotes/random/').bodyquote_hash=JSON.parse(server_raw_output)quote_hash['author']+': '+quote_hash['quote']+"\n"endend# this is a pythonic way of only running this code# if it's executed directly and# not required from elsewhere# we do this for the sake of testing laterprintQuote.new.random_quoteif$PROGRAM_NAME==__FILE__

(Why instance method instead of class one? You will see soon, in this state it doesn’t matter, but in the end it will make a difference).

Let’s stop here for a moment and think about the first of rules for good object-oriented design SOLID principles, i.e., Single Responsibility Principle.

Single Responsibility Principle

Every module or class should have responsibility over a single part of the functionality provided by the software, and the class should entirely encapsulate that responsibility.

In other words (more practical): classes should have only one reason to change. Let’s take a look… oops! When we’re looking at our class, we see A LOT of reasons to change. Starting from switching connection protocol, through changing the parsing method to different printing style. We need to do something about it and stop violating SRP so… violently.

Great step for make it compliant with SRP is to work on untangling implicit classes connections, one by one. First of all, looking from the top, we encounter the line:

Yep, we have an implicit class usage. We will use very strict OOP here, meaning an object can use only classes explicitly provided in the constructor (usually you would also allow stdlib classes). So, let’s write the first of series of our small classes - that one will be called QuoteConnector. It will serve only as a method of requesting data from a remote server and returning a string representing its body.

# lib/quote_connector.rbrequire'httparty'classQuoteConnectorURL='https://talaikis.com/api/quotes/random/'.freeze# since this is constant, we REALLY don't want to mutate itdefinitialize(adapter: HTTParty)@adapter=adapterenddefcalladapter.get(URL).bodyendprivateattr_reader:adapterend

Let’s stop here for a second. We did something in the constructor - something that should be familiar with people who coded in Java. It’s called Dependency Injection. That way in a typical environment we will be using the HTTParty library and in our test environment, we will inject our custom, mocked class (without re-opening existing classes or injecting our mocks elsewhere). Otherwise, it’s an elementary class - this is how we roll.

Let’s look at irb usage of our class:

irb(main):017:0>QuoteConnector.new.call=>"{\"quote\":\"That which is so universal as death must be a benefit.\",\"author\":\"Friedrich Schiller\",\"cat\":\"death\"}"

So, everything works as expected. Time to write some tests! I will be using Minitest - because I like it.

A little better. We need to provide QuoteConnector in the constructor to be compliant with our own rules.

Let’s look down… A-ha! We see a reference to the JSON class which isn’t specified anywhere. Time to change that! Let’s write another class - this time called QuoteParser.

# lib/quote_parser.rb# let's take note that it's a bit of overkill# since JSON is in stdlib# but hey, we're doing EXOOP (extreme oop)!classQuoteParserdefinitialize(object_to_parse:,parser_class: JSON)@object_to_parse=object_to_parse@parser_class=parser_classenddefcallparser_class.parse(object_to_parse)endprivateattr_reader:object_to_parse,:parser_classend

How does it work? Let’s look into irb…

irb(main):033:0>QuoteParser.new(object_to_parse: QuoteConnector.new.call).call=>{"quote"=>"Beauty is only skin deep. I think what's really important is finding a balance of mind, body and spirit.","author"=>"Jennifer Lopez","cat"=>"beauty"}

Looking good! And it seems the classes are working together, excellent.

We need three mocked classes to inject them into Quote, and then we’re checking if the chain of classes is returning the message as we intend it to.

In conclusion - we’ve written four small classes, each having exactly one responsibility and one reason to change. We’ve tested them extensively and tested classes that integrated them together. Pretty neat I think!

What could be changed here? For example, we could have all these classes in a single namespace, like a Quote module. Also, we could use some testing library to use verified mocks (with only real methods with correct arity, i.e., the number of arguments they take).

Whole code can be found in repository on our github including entire history of commits, showing different steps of refactoring.