Composing '99 Bottles of Beer' with Ruby's OOP Harmony'
Tags: ruby, oop, codenewbie, developer
Published: 2023-11-07 20:27:57

## 🌟 Introduction

Welcome back 😀! In a [previous post](https://ahmednadar.hashnode.dev/a-ruby-serenade-for-99-bottles-of-beer), I tackled the classic "[99 Bottles of Beer](https://sandimetz.com/99bottles)" problem using a procedural approach, leveraging [Ruby's case expression](https://ruby-doc.org/3.2.2/syntax/control_expressions_rdoc.html#label-case+Expression) to create a straightforward solution. Today, I revisit this catchy tune with a fresh perspective: Object-Oriented Programming (OOP). Thanks to insights from [Sandi Metz](https://twitter.com/sandimetz), [Katrina Owen](https://twitter.com/kytrinyx), and my friend Koichi.

## 🤖 Understanding Object-Oriented Programming in Our Digital Narrative

As we prepare to revisit the classic '99 Bottles of Beer' with an object-oriented approach, it's essential to grasp the fundamental concepts of [Object-Oriented Programming (OOP)](https://en.wikipedia.org/wiki/Object-oriented_programming) that we'll be employing:

\-**Encapsulation**: This is about bundling data with the methods that operate on it, encapsulating behaviour with the data it manipulates.

* **Inheritance**: A mechanism for creating new classes from existing ones, this enhances code reusability and establishes a natural hierarchy.
    
* **Polymorphism**: It allows entities to take on more than one form, enabling a single function to handle different types of objects.
    
* **Abstraction**: By hiding the complex reality behind simpler interfaces, we expose only the necessary components, making our code more approachable and easier to understand.
    

Embracing these principles doesn't merely organize our code; it's akin to composing a digital story where each object plays a part, modelled after real-world behaviours and interactions. In the context of our '99 Bottles of Beer' rendition, we're not just coding – we're crafting a symphony of objects that each carry their tune, harmonizing to create a cohesive and scalable application.

## 🎼 The SOLID Principles Concerto

In OOP, SOLID principles guide our design towards harmony:

1. [Single Responsibility Principle (SRP)](https://en.wikipedia.org/wiki/Single-responsibility_principle) - A class should have one, and only one, reason to change.
    
2. [Open/Closed Principle (OCP)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) - Classes should be open for extension but closed for modification.
    
3. [Liskov Substitution Principle (LSP)](https://en.wikipedia.org/wiki/Liskov_substitution_principle) - Subtypes must be substitutable for their base types.
    
4. [Interface Segregation Principle (ISP)](https://en.wikipedia.org/wiki/Interface_segregation_principle) - No client should be forced to depend on methods it does not use.
    
5. [Dependency Inversion Principle (DIP)](https://en.wikipedia.org/wiki/Dependency_inversion_principle) - Depend on abstractions, not on concretions.
    

Let's see how these principles are integrated into our '99 Bottles' code.

## 🛠️ Step-by-Step OOP Solution

### Act I: Crafting the Bottle Class with SRP

First, we encapsulate the idea of a bottle into its own class, which knows how to represent itself and its actions. In this `Bottle` class, we have defined methods to represent the bottle textually (`to_s`), the action to take (`action`), and to get the next bottle object (`next`). The `attr_reader` provides a way to read the `quantity` attribute outside the class. Create `bottle.rb` file.

```ruby
# This file contains the Bottle class which represents a bottle in the song.

class Bottle
	attr_reader :quantity

	def initialize(quantity)
		@quantity = quantity
	end

	# Define a method to represent the bottle count in a verse.
	def to_s
		case quantity
		when 0
		"no more bottles"
		when 1
		"1 bottle"
		else
		"#{quantity} bottles"
		end
	end
	
	# Method to print the action part of the verse.
	def action
		if quantity.positive?
		"Take one down and pass it around"
		else
		"Go to the store and buy some more"
		end
	end
	
	# Method to get the next bottle object.
	def next
		Bottle.new(quantity.positive? ? quantity - 1 : 99)
	end
end
```

*Why OOP?* Encapsulation ensures the bottle's behaviour is closely tied to its data.

### Act II: Building the BeerWall Class and OCP

Now, let's construct our wall. The `BeerWall` class interacts with bottle objects in a high-level way. We don't need to know how each bottle composes its verse; we only need to know that it can do so. The `sing` method prints all the verses of the song by iterating over each bottle and printing its verse. The `verse` method is a clear application of **SRP** as it has a single responsibility: to return the verse text for a given number of bottles. The `print_verse` method supports these principles internally, ensuring that the public interface remains unchanged while the class's internal workings can evolve as needed. Create `beer_wall.rb` file.

```ruby
# This class is responsible for printing the entire song.
require_relative "bottle"

class BeerWall
	def initialize(bottles)
		@bottles = bottles
	end
	
	# Prints a single verse for a given bottle.
	def verse(number)
		bottle = Bottle.new(number)
		"#{bottle} of beer on the wall, #{bottle} of beer.\n" \
		"#{bottle.action}, #{bottle.next} of beer on the wall.\n"
	end

	# Prints the entire song.
	def sing
		@bottles.downto(0) { |i| print_verse(Bottle.new(i)) }
	end
	
	private
		
	# Prints a single verse for a given bottle.
	def print_verse(bottle)
		puts "#{bottle} of beer on the wall, #{bottle} of beer."
		puts "#{bottle.action}, #{bottle.next} of beer on the wall.\n\n"
	end
end
```

*Why OOP?* The `BeerWall` class showcases abstraction by allowing us to interact with bottle objects in a high-level way.

While it is optionally, a driver script or a file that uses these classes to perform the desired actions. Create `ruby_sing.rb` file.

```ruby
require_relative "beer_wall"

BeerWall.new(99).sing
```

Run `ruby ruby_sing.rb` to see the [desired output](https://github.com/AhmedNadar/99BottlesChallenge/blob/main/result.md).

### Act III: Respecting LSP with Inheritance

Imagine we introduce a `BottleVariant` class for a special kind of bottle. This class would inherit from `Bottle`, and thanks to LSP, we can substitute `Bottle` with `BottleVariant` in our program without issues.

```ruby
	class BottleVariant < Bottle   
		# Specialized implementation... 
	end
```

### Act IV: ISP and the Art of Interfaces

Our classes use simple, clear interfaces. Each method does one thing and is used by the clients of the class. This adherence to ISP ensures that we don't have unnecessary dependencies in our classes.

### Act V: Embracing DIP with Abstractions

Finally, our `BeerWall` class depends on an abstraction (`Bottle`) and not on a concrete class. This is a simple form of DIP and allows us to change the underlying class (`Bottle`) without affecting `BeerWall`.

## 🧪 The Testing Suite

No OOP discussion is complete without testing. We'll demonstrate how to write simple tests for our classes using MiniTest, a testing suite included with Ruby by default. Create `99_bottels_oop_test.rb`file.

```ruby
require 'minitest/autorun'
require_relative '99_bottels_oop'
  
class BottleNumberTest < Minitest::Test

	def setup
		@beer_wall = BeerWall.new(99)
	end
	
	def test_the_first_verse
		expected = "99 bottles of beer on the wall, 99 bottles of beer.\n" \
		"Take one down and pass it around, 98 bottles of beer on the wall.\n"
		assert_equal expected, @beer_wall.verse(99)
	end
	
	def test_another_verse
		expected = "89 bottles of beer on the wall, 89 bottles of beer.\n" \
		"Take one down and pass it around, 88 bottles of beer on the wall.\n"
		assert_equal expected, @beer_wall.verse(89)
	end
	
	def test_verse_2
		expected = "2 bottles of beer on the wall, 2 bottles of beer.\n" \
		"Take one down and pass it around, 1 bottle of beer on the wall.\n"
		assert_equal expected, @beer_wall.verse(2)
	end
	
	def test_verse_1
		expected = "1 bottle of beer on the wall, 1 bottle of beer.\n" \
		"Take one down and pass it around, no more bottles of beer on the wall.\n"
		assert_equal expected, @beer_wall.verse(1)
	end
	
	def test_verse_0
		expected = "no more bottles of beer on the wall, no more bottles of beer.\n" 
		"Go to the store and buy some more, 99 bottles of beer on the wall.\n"
		assert_equal expected, @beer_wall.verse(0)
	end
end
```

Rub `ruby 99_bottels_oop_test.rb`

*Why OOP?* Testing is a fundamental part of OOP, ensuring each object behaves as expected.

## 🔄 Comparison with Procedural Approach

Let's compare this OOP solution with the procedural approach from the previous article.

In the procedural approach, we wrote a script where the logic flowed in a straight line. Functions were called in sequence, and the data was passed around from one function to another. This approach is straightforward to understand when the problem is simple. It's like following a recipe: each step is laid out, and you move from one to the next in order.

**Pros of the Procedural Approach:**

* **Simplicity**: For small scripts or simple problems, procedural code can be more straightforward to write and understand.
    
* **Performance**: Sometimes, procedural code can be faster because it involves less abstraction and fewer method calls.
    

**Cons of the Procedural Approach:**

* **Scalability**: As the complexity of the problem grows, procedural code can become harder to maintain and understand.
    
* **Reusability**: It's often more challenging to reuse parts of procedural code in other programs without modification.
    

On the other hand, the OOP approach encapsulates data and behaviour into objects. This mirrors real-world entities and allows for more complex interactions. Objects know how to manage their state and behaviour, leading to code that is more modular and easier to extend.

**Pros of the OOP Approach:**

* **Modularity**: Objects can be easily reused across different parts of the program or even in different programs.
    
* **Maintainability**: OOP makes it easier to keep the codebase organized and to manage complexity, especially as the project grows.
    
* **Flexibility**: Through inheritance and polymorphism, new functionality can be introduced with minimal changes to existing code.
    

**Cons of the OOP Approach:**

* **Complexity**: The additional layers of abstraction in OOP can make it more difficult to understand for beginners.
    
* **Performance Overhead**: Object creation and method calls can introduce performance overhead.
    

## 🧩 Conclusion and share

Through this exploration of the "99 Bottles of Beer" problem, we've delved deep into the nuances of Object-Oriented Programming. By shifting our perspective from procedural to OOP, we've transformed a straightforward script into a well-orchestrated ensemble of objects, each playing its part in harmony.

This journey has been as much about learning and applying OOP principles as it has been about inviting collaboration and sharing. In the [previous post](https://ahmednadar.hashnode.dev/a-ruby-serenade-for-99-bottles-of-beer), I shared my [GitHub repository](https://github.com/AhmedNadar/99BottlesChallenge) with the solution that employed a procedural approach. Now, I encourage you to visit the repository again to see the [OOP solution in action](https://github.com/AhmedNadar/99BottlesChallenge/tree/main/AhmedNadar/OOP).

I'm eager to hear your thoughts on the approaches we've discussed. Which do you lean towards in your own practice—procedural or OOP—and why? Your insights and diverse solutions enrich the conversation and contribute to a broader understanding of problem-solving in programming.

## 📚 Further Resources

For those looking to deepen their understanding of OOP in Ruby, here are some resources to get you started:

* [Sandi Metz's "99 Bottles of OOP"](https://sandimetz.com/99bottles) - A deeper dive into OOP using our beloved beer song.
    
* [Practical Object-Oriented Design in Ruby](https://www.poodr.com) - Sandi Metz's guide to OOP principles in Ruby.
    

Until next time, Happy Coding 😀💎💻