Hello and welcome back! Today, we're diving into the principles of Object-Oriented Programming (OOP) in C++ — Encapsulation, Abstraction, Polymorphism, and Composition — to enhance code readability and structure. Let's embark on this thrilling coding journey!
OOP principles provide the framework for creating readable, maintainable, and flexible code. These are the characteristics we aspire to during refactoring. By organizing properties and behaviors into logical class structures, we develop a codebase that's easier to understand and modify. This will become clearer as we move forward.
Encapsulation is about bundling related properties and methods within a class, thereby creating an organized structure that mirrors the real world.
Suppose we have scattered student information within our program.
C++1#include <iostream> 2#include <string> 3 4std::string student_name = "Alice"; 5int student_age = 20; 6double student_grade = 3.9; 7 8void display_student_info() { 9 std::cout << "Student Name: " << student_name << std::endl; 10 std::cout << "Student Age: " << student_age << std::endl; 11 std::cout << "Student Grade: " << student_grade << std::endl; 12} 13 14void update_student_grade(double new_grade) { 15 student_grade = new_grade; 16}
This functional code can become confusing as related attributes and behaviors aren't logically grouped. Let's encapsulate!
C++1#include <iostream> 2#include <string> 3 4class Student { 5private: 6 std::string name_; 7 int age_; 8 double grade_; 9 10public: 11 Student(const std::string& name, int age, double grade) 12 : name_(name), age_(age), grade_(grade) {} 13 14 void displayStudentInfo() const { 15 std::cout << "Student Name: " << name_ << std::endl; 16 std::cout << "Student Age: " << age_ << std::endl; 17 std::cout << "Student Grade: " << grade_ << std::endl; 18 } 19 20 void updateStudentGrade(double newGrade) { 21 grade_ = newGrade; 22 } 23};
By refactoring, all student-related properties and methods are encapsulated within the Student
class, enhancing readability and maintainability. To enhance code readability and maintain consistency, it's common to use a naming convention where private member variables end with an underscore (_
). This helps distinguish between member variables and other variables within class methods, reducing potential naming conflicts.
Next, let's discuss Abstraction. It's about revealing essential features and concealing complexities.
Consider a code snippet calculating a student's grade point average (GPA) through complex operations:
C++1double calculateGPA(const std::vector<char>& grades) { 2 int totalPoints = 0; 3 std::map<char, int> gradePoints = {{'A', 4}, {'B', 3}, {'C', 2}, {'D', 1}, {'F', 0}}; 4 5 for (char grade : grades) { 6 totalPoints += gradePoints[grade]; 7 } 8 return static_cast<double>(totalPoints) / grades.size(); 9}
We can incorporate this into a method inside our Student
class to simplify the interaction.
C++1#include <vector> 2#include <map> 3 4class Student { 5private: 6 std::string name_; 7 std::vector<char> grades_; 8 double gpa_; 9 10public: 11 Student(const std::string& name, const std::vector<char>& grades) 12 : name_(name), grades_(grades), gpa_(calculateGPA()) {} 13 14 double calculateGPA() const { 15 int totalPoints = 0; 16 std::map<char, int> gradePoints = {{'A', 4}, {'B', 3}, {'C', 2}, {'D', 1}, {'F', 0}}; 17 18 for (char grade : grades_) { 19 totalPoints += gradePoints[grade]; 20 } 21 return static_cast<double>(totalPoints) / grades_.size(); 22 } 23 24 double getGPA() { 25 gpa_ = calculateGPA(); 26 return gpa_; 27 } 28};
Now, the gpa
is an attribute of the student object, calculated transparently.
In C++, Polymorphism allows us to use a unified interface for different actions, enhancing code flexibility.
Consider a simple graphics editor without Polymorphism:
C++1class Rectangle { 2public: 3 void drawRectangle() { 4 std::cout << "Drawing a rectangle." << std::endl; 5 } 6}; 7 8class Triangle { 9public: 10 void drawTriangle() { 11 std::cout << "Drawing a triangle." << std::endl; 12 } 13};
Here, each class has its own method name. Let's refactor this to use Polymorphism with virtual functions:
C++1class Shape { 2public: 3 virtual void draw() const = 0; // Pure virtual function 4}; 5 6class Rectangle : public Shape { 7public: 8 void draw() const override { 9 std::cout << "Drawing a rectangle." << std::endl; 10 } 11}; 12 13class Triangle : public Shape { 14public: 15 void draw() const override { 16 std::cout << "Drawing a triangle." << std::endl; 17 } 18};
Now, irrespective of the shape, we can call the draw()
method to execute the correct drawing behavior, thus enhancing flexibility.
Now, we will explore Composition, which models relationships between classes. Composition allows us to build complex objects from simpler ones, facilitating system design in a flexible and maintainable way by organizing dependencies.
Imagine our application has a Window
class that tightly integrates methods for displaying the window alongside managing content directly.
C++1#include <iostream> 2#include <string> 3 4class Window { 5private: 6 std::string content_; 7 8public: 9 Window() : content_("Default content") {} 10 11 void addTextField(const std::string& content) { 12 this->content_ = content; 13 } 14 15 void display() const { 16 std::cout << "Window displays: " << content_ << std::endl; 17 } 18};
This design couples window display logic with content management, complicating maintenance if we extend functionalities. We can improve this design by employing Composition. With Composition, we decouple responsibilities by designing separate classes for content management (ContentManager
) and then integrating them into our Window
class. This way, each class focuses on single responsibilities.
C++1#include <string> 2 3class ContentManager { 4private: 5 std::string content_; 6 7public: 8 ContentManager(const std::string& content = "Default content") 9 : content_(content) {} 10 11 void updateContent(const std::string& newContent) { 12 content_ = newContent; 13 } 14 15 std::string getContent() const { 16 return content_; 17 } 18}; 19 20class Window { 21private: 22 ContentManager manager_; 23 24public: 25 Window() : manager_("Default content") {} 26 27 void display() const { 28 std::cout << "Window displays: " << manager_.getContent() << std::endl; 29 } 30 31 void changeContent(const std::string& newContent) { 32 manager_.updateContent(newContent); 33 } 34};
By employing Composition, we've encapsulated content management within its class. The Window
class now "has a" ContentManager
, focusing solely on displaying the window. This separation allows for easier modifications in content management or display without affecting each other's logic. Composition thus enhances our design's flexibility and maintainability by encouraging a cleaner, modular structure.
Well done! We have explored applying OOP principles in C++ to refactor code for better readability, maintainability, and scalability.
Now, get ready for some exciting practices. Nothing solidifies a concept more than hands-on experience! Happy refactoring!