OCP In Action

OCP In Action

The principle to any lazy programmer (or productive if that's what you call it) (Designed by catalyststuff / Freepik)

ยท

3 min read

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? ๐Ÿค”

  1. I'll have to edit the checks
  2. If am using a switch, I'll have compile errors if i don't have a default case
  3. Cost of change rises
  4. 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 :))

ย