Open-Closed Design Principle in Swift
Open-Closed Design Principle in Swift

Open-Closed Design Principle in Swift.

Yogendra Singh
AppleCommunity
Published in
5 min readApr 19, 2020

--

Open-Closed Principle.

The following tutorial need good understanding of Generics in Swift and Protocols.

Open for extension and closed for modification.

Open-closed principle described that the software entities (class, module, function etc) are open for the extension but closed for the modification. i.e entities should be designed in a such a way that without modifying its source code behaviour can be extended.

  • A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
  • A module will be said to be closed if it is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).

Suppose we have a Employee having firstName, lastName, location and role.

enum Role: String {
case ASE = "Associate Software Engineer"
case SE = "Software Engineer"
case ATL = "Associate Tech Lead"
case TL = "Tech Lead"
case PM = "Project Manager"
}
enum Location: String {
case noida = "Noida"
case gurgoan = "Gurgoan"
case banglore = "Banglore"
}
class Employee {
var id: Int
var firstName: String
var lastName: String
var role: Role
var location: Location
}

And we have a list of employee we have a requirement to filter them according to their location. So What we will do, we create class EmployeeFilter and create a method filterByLocation which takes array of Employees and location as arguments.

class EmployeeFilter {func filterByLocation(_ employees: [Employee], _ location: Location) -> [Employee] {
var filteredEmployees = [Employee]()
for employee in filteredEmployees {
if employee.location == location {
filteredEmployees.append(employee)
}
}
return filteredEmployees
}
}

Suppose later the requirements are changes and your client asked that he need Employee list by filtering their roles. Now what you will do, you will create a new method filterByLocation or something else for filtering Employees by their roles. Again later if client want a filter on the employees for specific location and specific role the you have to add one more method for filtering the employees.

class EmployeeFilter {func filterByLocation(_ employees: [Employee], _ location: Location) -> [Employee] {
var filteredEmployees = [Employee]()
for employee in filteredEmployees {
if employee.location == location {
filteredEmployees.append(employee)
}
}
return filteredEmployees
}
func filterByRole(_ employees: [Employee], _ role: Role) -> [Employee] {
var filteredEmployees = [Employee]()
for employee in filteredEmployees {
if employee.role == role {
filteredEmployees.append(employee)
}
}
return filteredEmployees
}
func filterByLocationAndRole(_ employees: [Employee], _ location: Location, _ role: Role) -> [Employee] {
var filteredEmployees = [Employee]()
for employee in filteredEmployees {
if employee.location == location && employee.role == role{
filteredEmployees.append(employee)
}
}
return filteredEmployees
}
}

Here every time we are changing the code when new requirement comes. This violated the Open-Closed Principle. So What is the solution of this.

Solution:

Now we are going to create two generic protocols to follow OCP. One protocol will define the rules for filtering and the other protocol for filter.

protocol FilterRule {
associatedtype T
func isRuleApplied(_ item: T) -> Bool
}
protocol FilterItem {
associatedtype T
func filter<Rule: FilterRule>(_ items: [T], filterRule: Rule) -> [T] where T == Rule.T
}

Next we will create a class LocationRule for defining location rule for filtering employees which is conforming the FilterRule Protocol.

class LocationRule: FilterRule {
typealias T = Employee
private var location: Location
init(_ location: Location) {
self.location = location
}
func isRuleApplied(_ item: Employee) -> Bool {
return item.location == location
}
}

And a class EmployeeFilter for filtering employees which is conforming the FilterItem Protocol.

class EmployeeFilter: FilterItem {
typealias T = Employee
func filter<Rule>(_ items: [Employee], filterRule: Rule) -> [Employee] where Rule : FilterRule, T == Rule.T {
var result = [Employee]()
for employee in items {
if filterRule.isRuleApplied(employee) {
result.append(employee)
}
}
return result
}
}

Now if the requirement is coming for filtering Employee according to Role. The we simple define a new rule for Role and pass this to filter method of EmployeeFilter method along with the list of Employees. Now we are extending the behaviour without touching the existing code.

class RoleRule: FilterRule {
typealias T = Employee
private var role: Role
init(_ role: Role) {
self.role = role
}
func isRuleApplied(_ item: Employee) -> Bool {
return item.role == role
}
}

Similarly, if we got a new requirement for filtering employee for a specific location and specific role then define new rule for that like below.

class LocationAndRoleRule<T, RuleA: FilterRule, RuleB: FilterRule> : FilterRule where RuleA.T == RuleB.T, T == RuleA.T {    let first: RuleA
let second: RuleB
init(_ first: RuleA, _ second: RuleB) {
self.first = first
self.second = second
}
func isRuleApplied(_ item: T) -> Bool {
first.isRuleApplied(item) && second.isRuleApplied(item)
}
}

Now the final filter code is below.

func filterEmployees() {let emp1 = Employee(id: 1, firstName: "AAAA", lastName: "BBB", role: .SE, location: .noida)
let emp2 = Employee(id: 2, firstName: "CCCCC", lastName: "DDD", role: .TL, location: .gurgoan)
let emp3 = Employee(id: 3, firstName: "FFFFF", lastName: "GGGG", role: .TL, location: .gurgoan)
let emp4 = Employee(id: 4, firstName: "HHHH", lastName: "IIIII", role: .ASE, location: .banglore)
let employees = [emp1, emp2, emp3, emp4]
// Filter by Location
print("#### Filter by Location ######")
let filterEmployee = EmployeeFilter()
let filteredEmployee = filterEmployee.filter(employees, filterRule: LocationRule(.banglore))
for employee in filteredEmployee {
print("Name: \(employee.firstName)\nLocation: \(employee.location.rawValue)")
}
// Filter by Location and Role.
print("#### Filter by Location and Role ######")
let locationAndRoleRule = LocationAndRoleRule(LocationRule(.gurgoan), RoleRule(.TL))
let filteredEmployee2 = filterEmployee.filter(employees, filterRule: locationAndRoleRule)
for employee in filteredEmployee2 {
print("Name: \(employee.firstName)\nLocation: \ (employee.location.rawValue)\nRole: \(employee.role.rawValue)")
}
}

So we can see that without touching the previous code we have extended the behaviour. The above code is written in such a way that it is allowing to extend the behaviour without modification of the code.

!!! Happy Coding !!!

Thank you for reading, please hit the recommend icon if like this 😊. Questions? Leave them in the comment.

--

--