Hello once again! Today's lesson is centered around leveraging the principles of Object-Oriented Programming (OOP) — Encapsulation, Abstraction, Polymorphism, and Composition — to enhance code readability and structure. Buckle up for an exciting journey ahead!
OOP principles act as a scaffold for building readable, maintainable, and flexible code — these are the characteristics we seek while refactoring. By creating logical groupings of properties and behaviors in classes, we foster a codebase that's easier to comprehend and modify. Let's put this into perspective as we progress.
Encapsulation involves bundling related properties and methods within a class, thereby creating an organization that mirrors the real world.
Suppose we possess scattered student information within our program.
Ruby1student_name = "Alice" 2student_age = 20 3student_grade = 3.9 4 5def display_student_info 6 puts "Student Name: #{student_name}" 7 puts "Student Age: #{student_age}" 8 puts "Student Grade: #{student_grade}" 9end 10 11def update_student_grade(new_grade) 12 $student_grade = new_grade 13end
Although functional, the code could cause potential confusion as the related attributes and behaviors aren't logically grouped. Let's encapsulate!
Ruby1class Student 2 def initialize(name, age, grade) 3 @name = name 4 @age = age 5 @grade = grade 6 end 7 8 def display_student_info 9 puts "Student Name: #{@name}" 10 puts "Student Age: #{@age}" 11 puts "Student Grade: #{@grade}" 12 end 13 14 def update_student_grade(new_grade) 15 @grade = new_grade 16 end 17end
After refactoring, all student-related properties and methods are contained within the Student
class, thereby enhancing readability and maintainability.
Next up is Abstraction. It is about exposing the relevant features and concealing the complexities.
Consider a code snippet calculating a student's grade point average (GPA) through complex operations:
Ruby1def calculate_gpa(grades) 2 total_points = 0 3 grade_points = {'A' => 4, 'B' => 3, 'C' => 2, 'D' => 1, 'F' => 0} 4 grades.each do |grade| 5 total_points += grade_points[grade] 6 end 7 total_points / grades.size.to_f 8end
We can encapsulate this within the calculate_gpa
method of our Student
class, thereby simplifying the interaction.
Ruby1class Student 2 attr_reader :gpa 3 4 def initialize(name, grades) 5 @name = name 6 @grades = grades 7 @gpa = calculate_gpa 8 end 9 10 def calculate_gpa 11 total_points = 0 12 grade_points = {'A' => 4, 'B' => 3, 'C' => 2, 'D' => 1, 'F' => 0} 13 @grades.each do |grade| 14 total_points += grade_points[grade] 15 end 16 total_points / @grades.size.to_f 17 end 18end
We can now access the gpa
as an attribute of the student object, which is calculated behind the scenes.
Polymorphism provides a unified interface for different types of actions, making our code more flexible.
Assume we are developing a simple graphics editor. Here is a code snippet without Polymorphism:
Ruby1class Rectangle 2 def draw_rectangle 3 puts "Drawing a rectangle." 4 end 5end 6 7class Triangle 8 def draw_triangle 9 puts "Drawing a triangle." 10 end 11end
We have different method names for each class. We can refactor this to have a singular draw
method common to all shapes:
Ruby1class Shape 2 def draw 3 raise NotImplementedError, 'This is an abstract base class method' 4 end 5end 6 7class Rectangle < Shape 8 def draw 9 puts "Drawing a rectangle." 10 end 11end 12 13class Triangle < Shape 14 def draw 15 puts "Drawing a triangle." 16 end 17end
Now, regardless of the shape of the object, we can use draw
to trigger the appropriate drawing behavior, thus enhancing flexibility.
Our last destination is Composition, which models relationships between objects and classes. Composition allows us to design our systems in a flexible and maintainable way by constructing complex objects from simpler ones. This principle helps us manage relationships by ensuring that objects are composed of other objects, thus organizing dependencies more neatly and making individual parts easier to update or replace.
Consider a system in our application that deals with rendering various UI elements. Initially, we might have a Window
class that includes methods both for displaying the window and managing content like buttons and text fields directly within it.
Ruby1class Window 2 def initialize 3 @content = "Default content" 4 end 5 6 def add_text_field(content) 7 @content = content 8 end 9 10 def display 11 puts "Window displays: #{@content}" 12 end 13end
This approach tightly couples the window display logic with the content management, making changes and maintenance harder as we add more elements and functionalities. Let's now see how we can update this code with composition.
To implement Composition, we decouple the responsibilities by creating separate classes for content management (ContentManager
) and then integrating these into our Window
class. This way, each class focuses on a single responsibility.
Ruby1class ContentManager 2 def initialize(content = "Default content") 3 @content = content 4 end 5 6 def update_content(new_content) 7 @content = new_content 8 end 9 10 def get_content 11 @content 12 end 13end 14 15class Window 16 def initialize 17 @manager = ContentManager.new 18 end 19 20 def display 21 puts "Window displays: #{@manager.get_content}" 22 end 23 24 def change_content(new_content) 25 @manager.update_content(new_content) 26 end 27end
By refactoring with Composition, we've encapsulated the content management within its class. The Window
class now "has a" ContentManager
, focusing on displaying the window. This separation allows for easier modifications in how content is managed or displayed without altering the other's logic. Composition, in this way, enhances our system's flexibility and maintainability by fostering a cleaner and more modular design.
Great job! We've learned how to apply OOP principles to refactor code for improved readability, maintainability, and scalability.
Now, get ready for some exciting exercises. Nothing strengthens a concept better than practice! Happy refactoring!