top of page
  • Writer's pictureJennifer Eve Vega

SOLID: Open-Close Principle

What does Open-Close Principle means?

This means classes should be Open for Extension, but Closed for Modification. The goal of this principle is to have little to no impact in our current code base whenever we add a new features / new behaviors. We want to isolate possible risks when adding new features by following this principle.


Open for Extension

A class is open when you are able to extend it (create a subclass). Some programming languages, like Swift, allow disabling class extension by adding the keyword final. To add more behavior and attributes, then you should extend a class instead of messing with the original code which will risk breaking it. Extending means you'll have to create a new class (in a new file), and not modify anything on the existing one.


But before you extend your classes, check out Liskov Substitution Principle and follow the guidelines in extending a concrete class to make sure you also don't break the parent class.


Example of class extension:

class Customer { ... }

class VIPCustomer: Customer { ... }

class RegularCustomer: Customer { ... }

You can also think of "Open for Extension" as adding a new behavior or adding a new feature by interface / protocol extension.


Example of interface extension:

protocol FetchDiscountProtocol {
	func fetchUserDiscount(_ userId: Id<User>) -> Double
}
class BillCalculationInteractor {
	private func computeTotal() -> Double {
		...
		let discount = fetchUserDiscount(...)
		return total - discount
	}
}
extension BillCalculationInteractor: FetchDiscountProtocol {
	func fetchUserDiscount(_ userId: Id<User>) -> Double { ... }
}

Closed for Modification

Modification simply means updating/editing the existing code base / class / file. I know it cannot be helped, but the goal is to update as little as possible to avoid breaking the working program.


If there are bugs in your current codebase, then of course, we have to update the current code base. The key goal of OCP is reducing risk when adding new features, so updating existing one due to bugs is something we should do in the same class/file.

It is not the responsibility of the child to fix the "mistakes/errors" done by the parent.

Are you familiar with this scenario?

Let's say we have a class for animals, we can feed it, and we can play with it.

class Animal {
	func feed(with food: Food)
	func play(with toy: Toy)
}

Now, we want to add a new behavior, which is to move, and perform movement.

class Animal {
	func feed(with food: Food) {...}
	func play(with toy: Toy) {...}
	
	func move() {
		walk()
	}
}

Since not all animals can move the same way, some will walk, some will crawl, fly, or swim. So, we limited the scope of the types of animals we can support, and added an if-else / switch condition based on the type of animals. We modified the Animal class and added more codes on the move() function.

class Animal {
	func feed(with food: Food) {...}
	func play(with toy: Toy) {...}
	
	// Not all mammals & reptiles walk/crawl, this is only for the sake of having an example
	func move(type: AnimalType) {
		switch type {
			case .mammal:
				walk()
			case .reptile:
				crawl()
			default:
				// Unsupported Animal Type
		}
	}
}

Now, we realize, not all Mammals walk, some are in the water! So we modified the Animal class again and added more condition.

class Animal {
	func feed(with food: Food) {...}
	func play(with toy: Toy) {...}
	
	func move(type: AnimalType) {
		switch type {
			case .mammal(let type):
				if type == .waterMammal {
					swim()
				} else {
					walk()
				}
			case .reptile:
				crawl()
			default:
				// Unsupported Animal Type
		}
	}
}

The issue here is when we support more animals, this condition will get bigger, and every time we update the Animal class, we risk breaking the working program. When we update the same file with other engineers, we also risk having conflicts, and when we fix the conflicts, we might break the other engineer's program. Which is why we want these classes and files closed for modification.


How can we reduce risk?

Given the same Animal class example above, let's try to add new features by adhering to the Open-Close Principle. Think of all these extensions done in different files.

class Animal {
	func feed(with food: Food) {...}
	func play(with toy: Toy) {...}
	
	func move() {
		// Subclasses will override this method
	}
}
class Mammal: Animal {
}
class LandMammal: Mammal {
// Not all land mammals walk, this is only for the sake of having an example
	override func move() {
		walk()
	}
}
class WaterMammal: Mammal {
	override func move() {
		swim()
	}
}
class Reptile: Animal {
// Not all reptiles can only crawl, this is only for the sake of having an example
	override func move() {
		crawl()
	}
}

Now, we are able to implement animal movement without the if-else condition, we are also able to simplify what each type of animals do.


If we are sure that these classes work, then we won't want to modify it in the future whenever we have new animal to support.


If we want to add Fish, then, we create a new file and let it extend the Animal class. By doing that, we won't be touching the current code base (current animal types), but we will still be able to support a new animal.

class Fish: Animal {
	override func move() {
		swim()
	}
}

OCP works together with other SOLID Principles

Open-Close Principle also adheres to the Single Responsibility Principle. As you notice, now that we have extended by animal type, each type is now focused on its own responsibility.


Another reminder, before you extend your classes, check out Liskov Substitution Principle and follow the guidelines in extending a concrete class to make sure you also don't break the parent class.


Aside from Single Responsibility Principle, OCP is also a good friend of Dependency Inversion Principle. In one of the examples I mentioned in DIP, when you are using different services, you are not going to be using the same file as the other service and do if-else. Instead, you should create a different file, a different class for the different service and let it conform to your protocol. By creating a different file, you have closed the other service for modification.


Isolating new issues

If there are new bugs introduced in the new file/class, we know that it's only happening to the new file/class, thus, we are also able to isolate the issue. If we don't adhere to the Open-Close Principle and we see that the new codes have introduced a new bug - we know we broke our program, and what's gonna happen next is that we'll have to test each condition in the file/class to find out where it went wrong.


Adding unit tests will definitely help us figure out if we broke our code, but that is a different topic to discuss.


For now, I'm gonna end this post here, and I hope I am able to clarify any confusion you have about Open Close Principle.


Recent Posts

See All

Comments


bottom of page