Code Smarter, Not Harder: Applying Best Programming Practices
For me, writing code is a challenging task. Especially when there are a lot of tasks, and you have to write a lot of code too. We may wrap in our laptop writing codes in one class, but is that efficient? So, there are ways to make the process more efficient and effective. By applying best programming practices such as OOP and SOLID, you can write better code that is easier to maintain, test, and modify.
In this article, I will explain with an example of OOP and SOLID Principles to up your coding game! 🎯
✨ OO Principle
The first one is the famous OO Principle. Based on partech.com, OO Principle is the creation of objects that has both data and functions. The fundamental OO Principle contains four principles: abstraction, encapsulation, inheritance, and polymorphism.
This principle may sound scary for one of you, but it’s not! I will give you explanations and examples in the context of a phone 📱 for you to understand this.
#1. Abstraction
Abstraction means using simplified class, rather than complex implementation code. It is about creating a simplified view of an object that contains only the necessary attributes and behaviors and hiding the details of the implementation. Abstraction is achieved through abstract classes and interfaces, which allow developers to define a common set of methods that can be used across multiple classes.
I will give you a simplified example. A phone user knows how to use the phone, but doesn’t need to understand the internal mechanics of the phone to use it.
class Phone:
function call()
function send_message()
function take_picture()
class Smartphone extends Phone:
function access_internet()
function play_music()
user = new Smartphone()
user.call()
user.send_message()
user.take_picture()
In this pseudo-code example, we have a Phone class that defines the basic functionalities of a phone, such as making a call, sending a message, and taking a picture. Then we have a Smartphone class that inherits from the Phone and adds additional features like accessing the internet and playing music.
The user of the phone only needs to know how to use these functions without having to understand how the internal mechanics of the phone work. This is an example of abstraction, where the user is only presented with a simplified interface to interact with the phone, rather than having to deal with the complex details of how it works.
#2. Encapsulation
Encapsulation refers to the practice of bundling data and methods that operate on that data within a single unit, called a class. Encapsulation is achieved when the code hides the implementation details of an object and exposes only a high-level interface through which other code can interact with it.
Encapsulation provides several benefits to software development, including helping to improve security by hiding sensitive information from other parts of the program that do not need to access it and the internal implementation of an object can be changed without affecting the external interface
Here is an example of pseudocode to illustrate encapsulation.
class Phone {
private:
batteryLevel; // encapsulated battery level
isOn; // encapsulated on/off state
public:
Phone() {
batteryLevel = 100;
isOn = false;
}
void turnOn() {
isOn = true;
display("Phone is on!");
}
void turnOff() {
isOn = false;
display("Phone is off.");
}
void charge(int amount) {
// code to charge
}
void makeCall(string number) {
if (isOn && batteryLevel >= 10) {
// code to make call
} else if (!isOn) {
display("Please turn on the phone before making a call.");
} else {
display("Cannot make call. Low battery level.");
}
}
void display(string message) {
// code to display message on phone screen
}
};
In this example, battery_level and is_on variables are private, which means that they can only be accessed within the Phone class itself. The public methods, such as turnOn(), turnOff(), charge(), and makeCall(), provide a way for the user to interact with the phone without having to worry about internal details.
For example, the user can call the charge() method to charge the phone’s battery without knowing how the charging process works internally. The charge() method takes care of checking if the phone is on or off and whether the battery level is within a valid range.
Similarly, the user can make a call by calling the make_call() method, which handles checking if the phone is on and if the battery level is sufficient and displaying the appropriate message on the phone screen.
#3. Inheritance
Inheritance is related to parent and child, just like the name. Objects are often very similar but not entirely the same (This is just like children inherit their parent’s characteristics, but not completely the same). So, in this case, Inheritance is the process by which one class acquires the properties and behavior of another class. It allows for code reuse, reducing the amount of code needed to be written and creating a more organized and hierarchical structure.
Here is an example of pseudocode to illustrate Inheritance.
class Phone {
batteryCapacity;
screenSize;
brand;
constructor(capacity, size, brand) {
this.batteryCapacity = capacity;
this.screenSize = size;
this.brand = brand;
}
makeCall(number) {
// Code to make a phone call
}
sendMessage(message) {
// Code to send a text message
}
}
class Smartphone extends Phone {
operatingSystem;
constructor(capacity, size, brand, os) {
super(capacity, size, brand);
this.operatingSystem = os;
}
takePicture() {
// Code to take a picture using the smartphone camera
}
accessInternet() {
// Code to access the internet using the smartphone
}
}
// Example usage:
myPhone = new Smartphone(5000, 6, "Samsung", "Android");
myPhone.makeCall("1234567890");
myPhone.accessInternet();
In this example, we have a Phone class that defines the basic properties and methods of a phone, such as batteryCapacity, screenSize, and makeCall(). We then have a Smartphone class that extends Phone and adds additional properties and methods specific to smartphones, such as operatingSystem, takePicture(), and accessInternet().
By using inheritance, we can avoid duplicating code and instead build upon the existing Phone class to create a more specific Smartphone class that inherits all the basic properties and methods from the parent class.
In conclusion, the four fundamental principles of Object-Oriented Programming (OOP) — abstraction, encapsulation, inheritance, and polymorphism — are crucial concepts for any developer to understand. These principles help developers create efficient and organized code, and enable code reuse and maintenance.
Abstraction allows developers to simplify complex implementation code into a simplified class, encapsulation bundles data and methods that operate on that data within a single unit, inheritance allows one class to acquire properties and behavior of another class, and polymorphism enables objects of different classes to be used interchangeably.
That’s the OO principle. I hope you understand what am I trying to tell you 🥳.
✨ SOLID Principles
Finally, it’s SOLID Principles! 🔥
What exactly are these?
SOLID is an acronym for the first five Object-Oriented Design (OOD) principles by Robert C. Martin. It’s an acronym for the five principles that comprise the SOLID principles: Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle.
The goals of using SOLID Principles are to promote good software design practices and help developers build software that is easy to modify and extend.
I will give you explanations and examples in the context of an animal 🐱 for you to understand this.
#1. Single Responsibility Principle
The Single Responsibility Principle (SRP) states that a class should have only one reason to change. In other words, a class should be responsible for only one thing. This principle helps ensure that classes are focused and do not become too complex.
In the context of animals, we can think of a class that represents a specific type of animal, such as a Lion 🦁. A lion class should only contain methods that relate to the behavior of a lion, such as hunting, roaring, and moving.
class Lion {
function hunt(prey) {
// Code to hunt prey
}
function roar() {
// Code to roar
}
function move() {
// Code to move
}
}
In this example, the Lion class has a single responsibility, which is to represent the behavior of a lion. The methods in the class are related to this responsibility and do not include any unrelated functionality.
#2. Open-Closed Principle
The Open-Closed Principle (OCP) states that classes should be open for extension but closed for modification. In other words, we should be able to extend the behavior of a class without modifying the existing code.
In the context of animals, we can think of a class that represents a group of animals, such as mammals. We may want to add new types of mammals in the future without modifying the existing mammal class.
class Mammal {
function nurseYoung() {
// Code to nurse young
}
}
class Dog extends Mammal {
function bark() {
// Code to bark
}
}
class Cat extends Mammal {
function meow() {
// Code to meow
}
}
In this example, the Mammal class is closed for modification, meaning that we cannot change the existing behavior of the class. However, we can extend the class by creating new subclasses such as Dog 🐶 and Cat 🐱, which add additional functionality without modifying the existing code.
#3. Liskov Substitution Principle
The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, a subclass should be able to replace its parent class without causing any unexpected behavior.
In the context of animals, we can think of a class that represents a specific type of animal, such as a Bird 🐦. A bird can fly, so we might create a Bird class with a fly() method. However, some birds such as penguins 🐧 cannot fly. Based on LSP, we should be able to replace the Bird class with a Penguin class without causing any unexpected behavior.
class Bird {
function fly() {
// Code to fly
}
}
class Penguin extends Bird {
function swim() {
// Code to swim
}
}
function makeBirdFly(Bird $bird) {
bird.fly();
}
eagle = new Bird();
penguin = new Penguin();
makeBirdFly(eagle); // This will work fine
makeBirdFly(penguin); // This will cause an error since penguins can't fly
In this example, we have a Bird class with a fly() method, which represents the behavior of a usual bird. We then have a Penguin class that extends Bird but does not implement the fly() method since penguins cannot fly.
We should be able to pass a Penguin object to a function that expects a Bird object, but this will cause an error since penguins cannot fly.
#4. Interface Segregation Principle
A class should not be required to implement methods it does not need. That’s the core of I in SOLID.
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. Instead, the class should implement a separate interface for each distinct functionality it requires. This promotes low coupling and high cohesion between classes.
// Animal interface
interface Animal {
void eat();
void sleep();
void move();
}
// FlyingAnimal interface, which extends Animal interface
interface FlyingAnimal extends Animal {
void fly();
}
// SwimmableAnimal interface, which extends Animal interface
interface SwimmableAnimal extends Animal {
void swim();
}
// Bird class, which implements FlyingAnimal interface
class Bird implements FlyingAnimal {
void eat() {
// code to eat food
}
void sleep() {
// code to sleep
}
void move() {
// code to move around
}
void fly() {
// code to fly
}
}
// Fish class, which implements SwimmableAnimal interface
class Fish implements SwimmableAnimal {
void eat() {
// code to eat food
}
void sleep() {
// code to sleep
}
void move() {
// code to move around
}
void swim() {
// code to swim
}
}
In this example, we have an Animal interface that defines the basic behaviors that all animals should have: eating, sleeping, and moving. We also have two more specialized interfaces, FlyingAnimal and SwimmableAnima. These two extend the Animal interface and define additional behaviors specific to flying and swimming animals.
The Bird class implements the FlyingAnimal interface, which means it must implement all the methods of the Animal interface and the fly() method defined by the FlyingAnimal interface.
Similarly, the Fish class implements the SwimmableAnimal interface, which means it must implement all the methods of the Animal interface and the swim() method defined by the SwimmableAnimal interface.
#5. Dependency Inversion Principle
We will meet again with the word ‘abstract’ because Dependency Inversion Principle is about making sure that our code is flexible and easy to change in the future. It suggests that we should depend on abstractions, rather than concrete implementations.
interface Animal {
function speak();
}
class Dog implements Animal {
function speak() {
print("Woof!");
}
}
class Cat implements Animal {
function speak() {
print("Meow!");
}
}
class AnimalSound {
private Animal animal;
constructor(animal) {
this.animal = animal;
}
function makeSound() {
animal.speak();
}
}
// Example usage:
let dog = new Dog();
let cat = new Cat();
let dogSound = new AnimalSound(dog);
let catSound = new AnimalSound(cat);
dogSound.makeSound(); // Output: "Woof!"
catSound.makeSound(); // Output: "Meow!"
For example, let’s say we have an Animal interface that has a speak() method. We also have Dog and Cat classes that implement the speak() method. If we want to use the speak() method in another class, like an AnimalSound class, we can pass in an Animal object as a dependency through its constructor. This allows us to easily switch out the type of animal without having to modify the AnimalSound class.
Overall, the goal of the SOLID principles is to reduce dependencies so that engineers change one area of software without impacting others (bmc.com). By following these principles, we can promote better design, reduce the risk of breaking changes, and improve the overall quality of our software systems.
✨ Conclusion
In conclusion, programming can be a challenging task, especially when dealing with complex code. However, by applying best programming practices such as Object-Oriented Programming (OOP) and SOLID principles, we can write better, more efficient, and more organized code that is easier to maintain, test, and modify.
The fundamental OOP principles (abstraction, encapsulation, inheritance, and polymorphism) enable developers to create efficient and organized code.
While the SOLID principles (Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle) help developers build software that is easy to modify and extend. By following these principles, we can reduce dependencies, promote better design, reduce the risk of breaking changes, and improve the overall quality of our software systems.
And that’s a wrap, Thank you! 💖
I will appreciate your feedback 💬 & clap 👏.
If you want to collaborate, don’t hesitate to contact me at deyunarusmiland@gmail.com or through Linkedin ☕