Introduction of Object-Oriented Programming Concepts with Real-World Scenarios

Imagine you’re an architect tasked with designing a new house. You’ve got a blank canvas to work with, and you’re full of ideas about how to make it the perfect home. But where do you start? How do you organize all those ideas and turn them into something concrete?

Photo by Daniel K Cheung on Unsplash

Introduction

This is where OOP (Object Oriented Programming) comes into play and reduces the complexity of the software development process. Object-oriented programming (OOP) is a powerful tool that can help you bring structure to your code, just like an architect brings structure to a house. By breaking down your program into smaller, manageable individual pieces with clear responsibilities called objects, you can create more efficient, flexible, and scalable software that meets the needs of your users. So, how can you apply OOP concepts to your projects? Let’s dive in and find out.

https://media.giphy.com/media/iMBEgyXkFBtdCFS93i/giphy.gif

What are Object Oriented Programming (OOP) Concepts❓

Object Oriented Programming (OOP) Concepts are the fundamental ideas and principles that define the OOP paradigm. These concepts include 🔴encapsulation, 🟢inheritance, 🔵polymorphism and 🟠abstraction. They are used to organize and structure software development, making it easier to create and maintain complex applications.

What is Encapsulation 🔴❓

Encapsulation is the process of hiding the implementation details of an object from the outside world and providing a simple interface for interacting with the object. It involves grouping related data and methods into a single unit and controlling access to them through visibility modifiers such as private, public, and protected.

What is the Visibility Modifers ❓

In object-oriented programming (OOP), visibility modifiers are used to control the access level of class members such as fields, methods, and inner classes. There are three types of visibility modifiers in most OOP languages: public, private, and protected.

The public modifier means that a class member is accessible from any code in the program. It can be accessed by any code that has an instance of the class or has a reference to a static member.

The private modifier means that a class member is only accessible from within the same class. It cannot be accessed by code outside the class, not even from subclasses.

The protected modifier means that a class member is accessible from within the same class and its subclasses. It cannot be accessed by code outside the class hierarchy. (We’ll talk about subclasses in Inheritance section.)

https://media.giphy.com/media/e7yNPQmGUozyU/giphy.gif

Encapsulation Examples 👨‍💻

class User {
private var name: String = ""

fun getName(): String {
return name
}

fun setName(userName: String) {
name = userName
}
}

In this example, we have a class called User that has a private property called name. By marking the property as private, we're preventing direct access to the name property from outside the class, encapsulating the data.

Instead of directly accessing or modifying the name property, the class provides two public methods: getName() and setName(). The getName() method returns the value of the name property, and the setName() method allows us to modify the name property. By using these methods, we can control the access and modification of the name property, encapsulating the methods.

Let’s look at another example 🧐

data class Item(val id: Long, val price: Double, val name: String, val stockCount: Int, val quantity: Int )

class ShoppingCart(private val items: MutableList<Item>) {
private var totalPrice: Double = 0.0

fun addItem(item: Item): Boolean {
if (item.quantity > item.stockCount) {
return false // Don't add the item to cart
}

items.add(item)
totalPrice += (item.price * item.quantity)
return true
}

fun removeItem(item: Item) {
items.remove(item)
totalPrice -= (item.price * item.quantity)
}

fun getTotalPrice(): Double {
if (isTotalPriceReachedFreeCargoThreshold()) {
return totalPrice
}

return totalPrice + getCargoPrice()
}

private fun getCargoPrice(): Double = 33.0

private fun isTotalPriceReachedFreeCargoThreshold(): Boolean {
return totalPrice >= 100
}

}

In this example, we have a class called ShoppingCart that encapsulates a list of Item objects. The class has a private property called items that stores the list of items, and a private property called totalPrice that stores the total price of all the items in the shopping cart.

The addItem(item: Item): Boolean method is used to add an Item to the cart. It takes an Item If the quantity requested is more than the stock count of the Item, the method returns false, indicating that the item was not added to the cart. Otherwise, it adds the item to the cart and updates the totalPrice property. By doing that we encapsulated the cart items.

The getTotalPrice() method calculates the total price of all items in the cart, including the shipping cost, if applicable. If the total price of the cart is greater than or equal to the free shipping threshold (in this case, 100), then the method returns only the totalPrice property. Otherwise, it adds the shipping cost to the totalPrice property and returns the result.

The Shopping Cart class encapsulates all the management and logic related to cart items, preventing any external modification to these processes.

Benefits of Encapsulation

Firstly, it ensures that the internal workings of the ShoppingCart class are hidden from external code, preventing accidental modification or misuse. This increases the reliability and stability of the codebase.

https://media.giphy.com/media/hpF9R9M1PHN5e5liSx/giphy.gif

Secondly, encapsulation makes it easier to maintain and update the code, as changes can be made to the internal implementation of the ShoppingCart class without affecting external code that uses it. This promotes modularity and can help to reduce development time.

Thirdly, encapsulation can help to improve the security of the code, as sensitive data or logic can be kept private and inaccessible to unauthorized code. This can be particularly important when working with payment systems or other sensitive data.

What is Inheritance 🟢❓

https://media.giphy.com/media/l0HlKYxenTClHlOV2/giphy.gif

Inheritance allows you to create a new class based on an existing class. The new class, known as the subclass, inherits all the properties and methods of the existing class, known as the super class/parent class, and can also add new properties and methods of its own.

Inheritance is based on the idea of “is-a” relationship, meaning that a subclass “is-a” type of its superclass. For example, a car “is-a” type of vehicle, so you could create a Car subclass that inherits from a Vehicle superclass.

Inheritance Example 👨‍💻

Let’s consider an online shopping platform that sells various types of products such as electronics, clothing, and home appliances. Each type of product has some common attributes such as name, description, and price, as well as some specific attributes such as size, color, and material.

To implement this in Kotlin using inheritance, we can create a parent class called Product by using openkeyword which contains the common attributes. In other languages, there is usually no need to use a keyword such as open.

open class Product(val name: String, val description: String, val price: Double) {
// common methods and properties for all products
}

Then, we can create child classes for each type of product that inherits from the Product class by using :and adds their specific attributes. In other languages, the extends keyword is generally used.

class Electronics(val brand: String, val model: String, name: String, description: String, price: Double,) 
: Product(name, description, price) {
// additional methods and properties for electronic products
}

class Clothing(val size: String, val color: String, name: String, description: String, price: Double,)
: Product(name, description, price) {
// additional methods and properties for clothing products
}

As you can see, each child class inherits the common attributes from the parent Product class and adds its own specific attributes. This way, we can avoid duplicating the code for the common attributes across different types of products.

// Usage of classes and variables
fun main() {
val computer = Electronics(
brand = "Apple",model = "M2 Air",
name = "Apple Computer", description = "", price = 1999.99
)

val tShirt = Clothing(
size = "S", color = "Green",
name = "Basic T-Shirt", description = "", price = 29.99
)

println("Common variable ${computer.name} Specific variable ${computer.model}")
println("Common variable ${tShirt.name} Specific variable ${tShirt.size}")

}

Benefits of Inheritance

Code Reusability: The parent class (Product) contains common properties and methods that are shared by the child classes (Electronics and Clothing). By inheriting from the parent class, the child classes can reuse the code instead of rewriting it from scratch.

Modularity: Inheritance allows for the creation of modular code. By separating common properties and methods into a parent class, it is easier to maintain and update the code. This also helps to prevent errors and improve code readability. It also allows you to create a hierarchy of classes that can be organized by their shared properties and behaviors.

Polymorphism: Inheritance allows for polymorphism, which means that objects of the child classes can be treated as objects of the parent class. This allows for more flexibility in code design and makes it easier to write generic code.

What is Polymorphism 🔵❓

https://media.giphy.com/media/T6V1Cy3CwEi88/giphy.gif

Polymorphism refers to the ability of objects to take on multiple forms. Polymorphism is the process of using a single name or interface to represent multiple different types.

In OOP, there are two main types of polymorphism: compile-time polymorphism (also known as method overloading) and runtime polymorphism (also known as method overriding).

Compile-time Polymorphism (Overloading)

Compile-time polymorphism is achieved by having multiple methods with the same name but different parameters in a class.

fun blablaFun() {}
fun blablaFun(number: Int) {}
fun blablaFun(number: Double) {}
fun blablaFun(number: Int, number2: Float) {}

During compile-time, the correct method to be called is determined based on the number and types of arguments passed to the method.

Compile-time Polymorphism (Overloading) Example 👨‍💻

fun showDialog(context: Context, title: String, message: String) {
val builder = AlertDialog.Builder(context)
builder.setTitle(title)
builder.setMessage(message)
builder.show()
}

fun showDialog(
context: Context, title: String, message: String,
positiveText: String,
negativeText: String,
onPositiveClicked: () -> Unit,
onNegativeClicked: () -> Unit
) {
val builder = AlertDialog.Builder(context)
builder.setTitle(title)
builder.setMessage(message)
builder.setPositiveButton(positiveText) { dialog, which ->
onPositiveClicked()
}
builder.setNegativeButton(negativeText) { dialog, which ->
onNegativeClicked()
}
builder.show()
}

The first function takes three parameters: context, title, and message. It creates an AlertDialog with the provided title and message and displays it using the builder.show() method.

The second function takes additional parameters: positiveText, negativeText, onPositiveClicked, and onNegativeClicked. This function creates a more complex AlertDialog with positiveText and negativeText buttons and their corresponding click listeners, onPositiveClicked and onNegativeClicked. When the user clicks either button, the corresponding click listener gets executed.

Since the second function has more parameters than the first one, we can say that the second function is an overloaded version of the first function. When we call the showDialog function with three parameters, it will execute the first version of the function, and when we call it with additional parameters, it will execute the second version of the function. This approach allows developers to create more flexible and reusable functions, by providing different versions of the same function with different parameters.

Note: By using Kotlin’s default and named arguments, you can provide polymorphism without having to write many functions.

Runtime Polymorphism (Overriding) 👨‍💻

https://media.giphy.com/media/13AN8X7jBIm15m/giphy.gif

Runtime polymorphism is achieved by having a method in a subclass that has the same signature (i.e., name and parameters) as a method in its superclass. During runtime, the correct method to be called is determined based on the actual object being referenced, not just the type of the reference.


open class PaymentMethod {
open val type: String = ""

open fun pay(amount: Double) {
println("Payment completed with the default payment method.")
}
}

class CreditCard : PaymentMethod() {
override val type: String = "CreditCard"
override fun pay(amount: Double) {
// code to process the credit card payment goes here
// take credit card number, name, CVV etc then go checkout
println("Payment completed with credit card.")
}
}

class BankTransfer : PaymentMethod() {
override val type: String = "BankTransfer"
override fun pay(amount: Double) {
// code to process the bank transfer payment goes here
// show your IBAN number
println("Payment completed with bank transfer.")
}
}

class Order(private val paymentMethod: PaymentMethod) {
fun processPayment(amount: Double) {
paymentMethod.pay(amount)
}
}

In this example, we have an Order class that has a processPayment method. The processPayment method takes an amount and calls the pay method on a PaymentMethod object to process the payment.

We have two subclasses of the PaymentMethod class, CreditCard and BankTransfer, which override the pay method to provide their own implementation of how the payment should be processed.

During checkout, when the user selects a payment method, an instance of the appropriate subclass (e.g. CreditCard or BankTransfer) is created and passed to the Order object. When the processPayment method is called, it calls the pay method on the passed-in payment method object, which executes the appropriate implementation of the pay method.

The implementation of the pay method is determined at runtime based on the type of the object. When we call the pay method on an object of type CreditCard, the implementation of the CreditCard class will be executed, and when we call the pay method on an object of type BankTransfer, the implementation of the BankTransfer class will be executed.

What is Abstraction ⚪❓

http://objectorientedthought.blogspot.com/2015/09/abstraction-in-oop.html

Abstraction refers to the ability to represent complex systems or ideas in a simplified way. Abstraction allows us to focus on the essential features of an object or system, while hiding unnecessary details and complexity.

In OOP, abstraction is achieved through the use of abstract classes and interfaces. An abstract class is a class that cannot be instantiated and serves as a blueprint for other classes. It may contain abstract methods, which are methods that have no implementation and must be overridden by the subclass that extends the abstract class.

Interfaces, on the other hand, are collections of abstract methods that define a set of behaviors that a class must implement. A class that implements an interface must provide an implementation for all of its methods.

Abstraction Example 👨‍💻

abstract class Product(val name: String, val price: Double) {
// Abstract method to calculate the total cost of the product
abstract fun calculateTotalCost(quantity: Int): Double
}

class Book(name: String, price: Double, val author: String) : Product(name, price) {
// Implementation of the abstract method for books
override fun calculateTotalCost(quantity: Int): Double {
return price * quantity
}
}

class Clothing(name: String, price: Double, val size: String) : Product(name, price) {
// Implementation of the abstract method for t-shirts
override fun calculateTotalCost(quantity: Int): Double {
val basePrice = price * quantity
return if (size == "XL") {
basePrice + 9.99 // XL t-shirts cost 9.99₺ extra
} else {
basePrice
}
}
}

In this example, we have an abstract class Product that represents a generic product in. This class has two properties: name and price, as well as an abstract method calculateTotalCost that takes a quantity and returns the total cost of the product.

We also have two subclasses (inheritance) of Product: Book and Clothing. These classes extend (inheritance) the Product class and implement their own version of the calculateTotalCost method based on the specific characteristics of each product.

For instance, the Book class simply multiplies the price by the quantity to calculate the total cost, while the Clothing class adds 9.99₺ to the total cost for XL-sized clothings.

By using abstraction in this way, we can create a modular and flexible system that can handle different types of products without having to rewrite code for each product type. We can simply create new subclasses of Product and implement their own version of the calculateTotalCost method, while relying on the abstract Product class to provide a consistent interface for all products in the system.

Benefits of Abstraction

Simplified Implementation: Abstraction allows the implementation of a complex system to be simplified. In the given example, the Product class abstracts away the implementation details of the product, making it easier for other parts of the system to interact with it.

Reduces Code Duplication: By providing a common interface for similar classes, abstraction reduces the amount of code duplication in a system. In the given example, the Product class provides a common interface for different types of products, such as Book and Clothing.

Encourages Modularity: Abstraction encourages modularity by breaking down a system into smaller, more manageable parts. In the given example, the Product class encapsulates the implementation details of a product, making it easier to modify or replace it without affecting other parts of the system. By defining abstract classes and interfaces, we can create a separation of concerns between different components of a system, which makes it easier to maintain and modify the system over time.

Increases Flexibility: Abstraction increases the flexibility of a system by allowing different implementations of the same interface to be used interchangeably. In the given example, the Product interface can be implemented by different types of products, such as Book and Clothing, without affecting the rest of the system.

Facilitates Testing: Abstraction facilitates testing by providing a clear separation between the interface and the implementation details. In the given example, the Product interface can be tested independently of the different implementations, such as Book and Clothing.

🔚 💚 👏 Conclusion

Most of us imagine architects designing and building homes or planning a company’s building. But the true purpose of an architect is to solve a problem. A good architect designs a building that will meet the needs of the users by combining functionality and aesthetics. Software engineering has a similar approach. With Object-Oriented Programming (OOP), programmers alike design code to provide a better experience for users by making code more modular, more readable, and more manageable.

Did you know that every Medium article can receive up to 50 claps? It's true. So, if you find this article helpful or enjoyable, I encourage you to show your appreciation by giving it a round of applause. It's a small gesture that means a lot to us writers, and it only takes a second. So, let's give those clapping hands a workout and see if we can reach that 50-clap goal together!

Source

Happy Coding 👩‍💻. Thanks for reading🤗. See you in the next articles👋.

REFERENCES

--

--

Cengiz Toru (🇵🇸 #FreePalestineFromGenocide)
Huawei Developers

(🇵🇸 #FreePalestineFromGenocide 🍉) | Muslim, Computer Engineer & Android Developer @ Hepsiburada (NASDAQ: HEPS ) , ex; Huawei, T-Soft, Arneca