OCP In Action
The principle to any lazy programmer (or productive if that's what you call it) (Designed by catalyststuff / Freepik)
Here, we're going to talk about a really common problem, I myself used to do it until I learned better, so I think it's really cool to discuss it today and find possible solutions and refactoring techniques, as well as explain some tips and tricks and what code debts mean IRL.
Problem ๐
enum Product {
case iPhone
case iPad
case iDunno
}
func displayDetails(of product: Product) {
if product == .iPhone {
print("iPhone")
} else if product == .iPad {
print("iPad")
} else if product == .iDunno {
print("๐คทโโ๏ธ")
}
}
What If I added a new case to the enum? ๐ค
- I'll have to edit the checks
- If am using a switch, I'll have compile errors if i don't have a default case
- Cost of change rises
- am adding code debt of things i've to do when feature requests come in
First Iteration of Refactoring
You can give the enum the ability to display its details and end up with this
extension Product {
func displayDetails() -> String {
switch self {
case .iPhone:
return "iPhone"
case .iPad:
return "iPad"
case .iDunno:
return "๐คทโโ๏ธ"
}
}
}
func enhancedDisplayDetails(of product: Product) {
print(product.displayDetails())
}
Here we fixed the code duplication issue (DRY), and we also fixed code debt by centralizing the area of where changes might happen (only 1 reason to change), but still we risk OCP since adding cases can still break the app
Second Iteration of Refactoring
Using Protocols and Abstractions
protocol Presentable {
func displayDetails() -> String
}
protocol Billable {
var price: String { get set }
func displayBill() -> String
}
protocol Product: Presentable, Billable {
var name: String { get set }
}
struct AppleProduct: Product {
var name: String
var price: String
func displayBill() -> String {
return "$\(price)"
}
func displayDetails() -> String {
return "\(name) - \(displayBill())"
}
}
struct GiftProduct: Product {
var name: String
var price: String = "Free"
func displayBill() -> String {
return price
}
func displayDetails() -> String {
return "\(name) - \(displayBill())"
}
}
func enhancedFurtherlyDisplayDetails(of product: Product) {
print(product.displayDetails())
}
With protocols we added a feature of not only displaying any Apple Product, but also Gift Products that can be given automatically, still there is some code duplication where I needed to write the code for displayBill and displayDetails twice, which can be fixed through Swift's default protocol implementation
Third Iteration of refactoring
Using Default Implementation of protocols
protocol Presentable {
func displayDetails() -> String
}
protocol Billable {
var price: String { get set }
func displayBill() -> String
}
protocol Product: Presentable, Billable {
var name: String { get set }
}
extension Product {
func displayBill() -> String {
return "$\(price)"
}
func displayDetails() -> String {
return "\(name) - \(displayBill())"
}
}
struct AppleProduct: Product {
var name: String
var price: String
}
struct GiftProduct: Product {
var name: String
var price: String = "Free"
func displayBill() -> String {
return price
}
}
func enhancedFurtherlyDisplayDetails(of product: Product) {
print(product.displayDetails())
}
In here you can see that we eliminated some code from both AppleProduct
and GiftProduct
in case of displayDetails()
, however for displayBill()
and we overriden it for displaying free without any currency
Then you can test the functions if you needed because you expect an output from them, if the logic inside them fails, you know there is a problem
also if you want to localize stuff in a flexible, and scalable way, I suggest exporting the displaying logic into its own class where it receives the price as a double, do some presentation logic to it and if you have more than 1 currency, you are free to give that its own class as well to add some localization features into your project :))
Good Day :))