Building a No-Maintenance Universal Image Search with the Four Pillars of OOP
Nerd Disclaimer: while the following is a specific example, the ideas stated here date back to the late 1960s, and the basic principles apply to any integration work with multiple permutations. This is one of many modern takes on Object-Oriented Programming, and is not the original 1966 message-based Object-Oriented Programming which heavily uses Encapsulation and does not rely on Inheritance or Polymorphism.
Business Requirements: create a RESTful API for searching any query against half-a-dozen image providers, notably Giphy, Shutterstock, and Getty Images. Any image found should be licensed correctly, downloaded, and inserted into Barstool’s zillions of blog posts, video thumbnails, video stills, illustrations, vector art, or whatever else the content team ever needs or wants.
Solution: Use the four pillars of OOP. Don't overthink it.
Results: In the 12 months since its launch in May 2020, 0 commits have been needed to maintain functionality, and we’ve added several feature requests quickly and painlessly within a day or two. Nearly every non-original image seen in any of our content, including this blog site, was searched/downloaded/licensed-as-necessary via the API of this project.
The Four Pillars of OOP
1. Abstraction (or Keep It Simple Stupid — name things based on Business Requirements)
What are the Business Requirements? We need to search, we need to readMetadata, we need to license, and we need to download. So what will our code for each Provider do — or rather, what interface methods will each Provider implement?
But we haven’t connected anything yet! What about network requests? What about data? What about docs and swagger files and API keys?
Who cares, we can squirrel away the implementation details.
2. Encapsulation (or squirreling away the implementation details)
Shutterstock Creative subscriptions and licensing works differently from Getty Images API access. Giphy handles search pagination differently from Shutterstock Editorial cursor-based list-offsets.
All of this messy state and code should remain defined within the individual Providers. Localize complexity and hide it from outside access. For languages that support private methods, this is where private shines. Only expose what needs to be. Whether the subscriptions load as part of an async setter after the constructor is called, or whether the auth token refreshes prior to each API request, all of this mess can be individually encapsulated, and nothing can ever side-effect any of the encapsulated state.
Ok — great — but even if we do handle everything correctly, how do images on the internet work? How do we make sure everything displays and gets sized correctly?
3. Inheritance (or inheriting logic from existing systems that already work)
So here at Barstool we deliver and heavily cache all our image content through Fastly (a CDN) and their excellent Image Optimizer tool. With the help of some clever custom VCL snippets we can easily crop, resize, download, and even collage images together into playful mosaics.
So why reinvent the wheel? We can just make these Providers extensions of the existing image service — and with almost zero engineering effort we can now crop every new candid of Lebron James and slap it into a new meme (and maybe even put it on a shirt).
Work smart. Don’t reinvent the content-delivery-network-based internet to build a new search tool.
Ok, but what about the features we can’t Inherit? What about the complexity we can’t Encapsulate? How do we really implement Abstractions? How does it all come together seamlessly.
4. Polymorphism (or the condition of occurring in several different forms)
Getty Images Editorial search has a lot of parameters to explore the every waking moment of celebrities, sports teams, and world catastrophes. Whereas — Giphy takes the name of a meme, and it spits back 100 versions of that meme and its derivative sub-memes.
Therefore — every single Provider will need to implement its own vastly different search parameters, and each will return varied and unique results. But since we can call the same namespaced search method on any Provider, no matter what the details are, everything remains top-level and easy. Finding a photo of Kanye West is just as easy as resizing a photo; finding the original Peanut Butter Jelly Time gif is just as easy as cropping a gif. The implementation details may vary, but every search looks exactly like a search, and the code screams its purpose. Our code is blatantly obvious in what it does.
Conclusion
So for all the engineers out there tasked with architecting and writing a useful system — remember the basics. Return to the ancient wisdom. It should not matter how complicated or demanding the Business Requirements are. Even if you don’t know any Object-Oriented Languages and you’re working entirely in monads and FRP — let the four pillars support you. Abstract the essence of the Business Requirements and name public functions accordingly; Encapsulate the onerous details; Inherit and steal logic that already works; keep your code Polymorphic so future engineers can see at a glance that given provider="XYZ" the controller-layer will invoke search XYZ and return XYZ images. All of the moving parts are functionally partitioned and loosely coupled, and no single provider outage will have any impact on the system as a whole. The final code is organized, cleanly namespaced, and it just goddamn works.