Understanding the Open-Closed Principle in Software Engineering
Software Design8 min readMay 14, 2025

Understanding the Open-Closed Principle in Software Engineering

AS

Asif Shaikh

Mobile Engineer


# Understanding the Open-Closed Principle in Software Engineering

Welcome back to our series on the SOLID principles of software engineering! In Part 1, we explored the Single Responsibility Principle, learning how each class should have just one job to keep our code clean and manageable.

Today, in Part 2, we're diving into the Open-Closed Principle (OCP), a super useful idea that makes our code flexible and easy to grow, like a LEGO set that you can keep adding to without breaking what's already built.

We'll explain OCP in simple, everyday terms, use Kotlin (a popular language for Android development) for examples, and even sprinkle in some Android-inspired scenarios to make it relatable.

So let's get started 🚀

## What is the Open-Closed Principle?

The Open-Closed Principle sounds fancy, but it's actually pretty straightforward.

It says:

> "Software entities (like classes, modules, or functions) should be _open for extension_ but _closed for modification_."

Let's break that down:

Open for extension: You can add new features or behaviors to your code easily, like adding a new character to a game.
Closed for modification: You shouldn't have to change the existing code to add those new features, so the original code stays safe and untouched.

Think of it like a smartphone. When you want a new feature, like a cool camera filter, you download a new app or update (extension). You don't need to crack open the phone and rewire its circuits (modification). OCP helps us design code that works the same way—new stuff can be added without messing with what's already there.

## Why Does OCP Matter?

Imagine you're building an Android app, like a to-do list app. Users love it, but they start asking for new features, like reminders or color-coded tasks. If you have to rewrite big chunks of your original code every time you add something new, you might accidentally break the app, frustrate users, or spend hours fixing bugs. OCP helps you avoid this by making your code flexible and ready for growth.

Following OCP means:

Your app can handle new features without breaking.
It's easier to maintain and test.
* You save time and avoid headaches when requirements change (and they _always_ change!).

## A Real-World Example: The To-Do List App

Let's say you're building a to-do list app for Android. You have a feature that displays tasks in a simple list. But now, users want different ways to display tasks—like a grid view or a calendar view. Without OCP, you might end up with code that looks like this:

### What's Wrong Here?

kotlin
01
class TaskDisplay {
02
fun displayTasks(tasks: List<Task>, viewType: String) {
03
when (viewType) {
04
"list" -> displayAsList(tasks)
05
"grid" -> displayAsGrid(tasks)
06
"calendar" -> displayAsCalendar(tasks)
07
else -> displayAsList(tasks)
08
}
09
}
10
11
private fun displayAsList(tasks: List<Task>) { /* ... */ }
12
private fun displayAsGrid(tasks: List<Task>) { /* ... */ }
13
private fun displayAsCalendar(tasks: List<Task>) { /* ... */ }
14
}


Every time you want to add a new display type (say, a "kanban" view), you have to _modify_ the TaskDisplay class and add another if statement. This:

Makes the code messy and hard to read.
Risks breaking existing display types if you make a mistake.
* Violates OCP because the class isn't _closed for modification_.

### Fixing It with OCP

To follow OCP, we can redesign the code so it's _open for extension_ (you can add new display types) but _closed for modification_ (you don't change the original code). Here's how we can do it using Kotlin and an interface:

kotlin
01
interface TaskDisplay {
02
fun display(tasks: List<Task>)
03
}
04
05
class ListDisplay : TaskDisplay {
06
override fun display(tasks: List<Task>) {
07
// Display tasks as a list
08
}
09
}
10
11
class GridDisplay : TaskDisplay {
12
override fun display(tasks: List<Task>) {
13
// Display tasks as a grid
14
}
15
}
16
17
class CalendarDisplay : TaskDisplay {
18
override fun display(tasks: List<Task>) {
19
// Display tasks as a calendar
20
}
21
}
22
23
class TaskManager(private val display: TaskDisplay) {
24
fun showTasks(tasks: List<Task>) {
25
display.display(tasks)
26
}
27
}


### How Does This Follow OCP?

Open for extension: Want a new display type, like a kanban view? Just create a new class (e.g., KanbanDisplay) that implements TaskDisplay. No need to touch the existing code.
Closed for modification: The TaskManager and existing display classes stay unchanged when you add new display types.

Here's how you'd use it:

kotlin
01
val listDisplay = ListDisplay()
02
val taskManager = TaskManager(listDisplay)
03
taskManager.showTasks(tasks)


Now, adding a new KanbanDisplay is as simple as:

kotlin
01
class KanbanDisplay : TaskDisplay {
02
override fun display(tasks: List<Task>) {
03
// Display tasks as a kanban board
04
}
05
}
06
07
// Usage
08
val kanbanDisplay = KanbanDisplay()
09
val taskManager = TaskManager(kanbanDisplay)


No changes to TaskManager or other classes needed!

## Another Android Example: Notifications

Let's look at another Android example to drive this home. Suppose your app sends notifications to users, like reminders for tasks. Initially, you only support push notifications, but later you want to add email notifications and SMS notifications.

### Without OCP

You might write something like:

kotlin
01
class NotificationSender {
02
fun sendNotification(message: String, type: String) {
03
when (type) {
04
"push" -> sendPushNotification(message)
05
"email" -> sendEmailNotification(message)
06
"sms" -> sendSMSNotification(message)
07
}
08
}
09
10
private fun sendPushNotification(message: String) { /* ... */ }
11
private fun sendEmailNotification(message: String) { /* ... */ }
12
private fun sendSMSNotification(message: String) { /* ... */ }
13
}


Again, adding a new notification type (like in-app notifications) means modifying the NotificationSender class, which violates OCP.

### With OCP

Here's a better way:

kotlin
01
interface Notification {
02
fun send(message: String)
03
}
04
05
class PushNotification : Notification {
06
override fun send(message: String) {
07
// Send push notification
08
}
09
}
10
11
class EmailNotification : Notification {
12
override fun send(message: String) {
13
// Send email notification
14
}
15
}
16
17
class SMSNotification : Notification {
18
override fun send(message: String) {
19
// Send SMS notification
20
}
21
}
22
23
class NotificationManager(private val notification: Notification) {
24
fun notifyUser(message: String) {
25
notification.send(message)
26
}
27
}


Now, if you want to add an in-app notification, just create a new class:

kotlin
01
class InAppNotification : Notification {
02
override fun send(message: String) {
03
// Show in-app notification
04
}
05
}
06
07
// Usage
08
val inAppNotification = InAppNotification()
09
val notificationManager = NotificationManager(inAppNotification)


The NotificationManager doesn't need any changes, and your app can keep growing with new notification types.

## Tips for Applying OCP in Your Code

1. Use Interfaces or Abstract Classes: They let you define a contract (like TaskDisplay or Notification) that new classes can follow without changing existing code.
2. Think About Future Features: When designing a feature, ask, "What might users want next?" Design your code to make those additions easy.
3. Keep It Simple: Don't overcomplicate things with too many interfaces. Use OCP where you expect change, like features that might expand (e.g., display types, notifications).
4. Test Your Extensions: Since you're adding new classes, make sure they work as expected without breaking the rest of the app.

## Wrapping Up

The Open-Closed Principle is like building a house with extra rooms ready for guests—you can welcome new features without tearing down walls. By designing your code to be _open for extension_ and _closed for modification_, you make your Android apps (or any software) easier to maintain, grow, and test.

In our to-do list and notification examples, we saw how using interfaces in Kotlin lets us add new display types or notification methods without touching the original code. This saves time, reduces bugs, and keeps your app flexible for whatever users dream up next.

Stay tuned for Part 3 of our SOLID series, where we'll explore the Liskov Substitution Principle! In the meantime, try applying OCP to your next Android project. Have you used OCP before, or do you have a feature in mind where it could help? Let me know in the comments!

Happy coding, and see you in the next post!

---

_This blog post is part of a series on SOLID principles. Check out Part 1: Single Responsibility Principle if you missed it!_

Published on May 14, 2025 by Asif Shaikh

Contact