Giter VIP home page Giter VIP logo

clean-code-swift's Introduction

Clean Code Adapted for Swift

Index

  1. Introduction
  2. Variables
  3. Functions
  4. Objects and Data Structures
  5. Classes
  6. SOLID
  7. Testing
  8. Concurrency
  9. Error Handling
  10. Formatting
  11. Comments

Introduction

Humorous image of software quality estimation based on counting how many swear words you shouted while reading the code.

Software Engineering principles from Robert C. Martin's book Clean Code, adapted for Swift. This is not a style guide. It is a guide for producing readable, reusable, and refactorable code in Swift.

It is not necessary to strictly follow all the principles demonstrated, and even less are they a universal consensus. These principles are guidelines and nothing more, however, they have been codified over many years of collective experience by the authors of Clean Code.

Software engineering is just over 50 years old, and we are still learning a lot. When software architecture is as old as architecture itself, we may have stricter rules to follow. For now, let these guidelines serve as a criterion for evaluating the quality of Swift code that you and your team produce.

One more thing: learning this will not immediately turn you into a better software developer, and working with these principles for many years does not guarantee that you will not make mistakes. Each portion of code starts as a draft, like wet clay being shaped into its final form. Finally, we carve out imperfections by reviewing with our peers. Don't feel guilty about the first drafts that still need improvement. Instead, focus on refining your code.

Variables

Use meaningful and pronounceable variable names

Bad:

let currentDateStr = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)

Good:

let currentDate = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)

⬆ back to top

Use the same vocabulary for the same type of variable

Bad:

getUserInfo()
getClientData()
getCustomerRecord()

Good:

getUser()

⬆ back to top

Use searchable names

We will read more code than we write. It is important that the code we write is readable and searchable. Not giving meaningful names to variables that are significant for understanding our program hurts our readers. Make your names searchable.

Bad:

// What the heck does 86400000 stand for?
setTimeout(blastOff, 86400000)

Good:

// Declare them as `let` or `var` in uppercase letters.
let millisecondsPerDay = 86400000

setTimeout(blastOff, millisecondsPerDay)

⬆ back to top

Use explanatory variables

Bad:

let address = "One Infinite Loop, Cupertino 95014"
let cityZipCodeRegex = try! NSRegularExpression(pattern: "^[^,\\]+[,\\\\\\s]+(.+?)\\s*(\\d{5})?$")
saveCityZipCode(address.match(cityZipCodeRegex)![1], address.match(cityZipCodeRegex)![2])

Good:

let address = "One Infinite Loop, Cupertino 95014"
let cityZipCodeRegex = try! NSRegularExpression(pattern: "^[^,\\]+[,\\\\\\s]+(.+?)\\s*(\\d{5})?$")
if let match = cityZipCodeRegex.firstMatch(in: address) {
    let city = (address as NSString).substring(with: match.range(at: 1))
    let zipCode = (address as NSString).substring(with: match.range(at: 2))
    saveCityZipCode(city, zipCode)
}

⬆ back to top

Avoid Mental Mapping

Explicit is better than implicit.

Bad:

let locations = ["Austin", "New York", "San Francisco"]
locations.forEach({ l in
    doStuff()
    doSomeOtherStuff()
    // ...
    // ...
    // ...
    // Wait, what's `l` for again?
    dispatch(l)
})

Good:

let locations = ["Austin", "New York", "San Francisco"]
locations.forEach({ location in
    doStuff()
    doSomeOtherStuff()
    // ...
    // ...
    // ...
    dispatch(location)
})

⬆ back to top

Avoid unnecessary context

If your class/object name already tells you something, don't repeat it in your variable names.

Bad:

let car = [
    "carMake": "Honda",
    "carModel": "Accord",
    "carColor": "Blue"
]

func paintCar(car: [String: String], color: String) {
    car["carColor"] = color
}

Good:

let car = [
    "make": "Honda",
    "model": "Accord",
    "color": "Blue"
]

func paintCar(car: [String: String], color: String) {
    car["color"] = color
}

⬆ back to top

Functions

Function arguments (ideally 2 or fewer)

Careful management of parameters in functions plays a crucial role in facilitating testing and maintaining code, following SOLID principles. When a function has more than three arguments, complexity grows exponentially, requiring detailed tests for each parameter separately.

The ideal is to keep a maximum of two arguments, avoiding three whenever possible. If the function requires more parameters, an alternative is to consolidate them into an object. A function with more than two arguments often tries to cover multiple responsibilities, signaling the need for reassessment. In many cases, passing an object as an argument is sufficient.

Considering Swift's ability to efficiently create objects, using objects when dealing with multiple arguments is an effective practice.

⬆ Back to Top

Bad:

func createMenu(title: String, body: String, buttonText: String, cancellable: Bool) {
  // ...
}

Good:

struct MenuViewData {
  let title: String
  let body: String
  let buttonText: String
  let cancellable: Bool
}

func createMenu(viewData: MenuViewData) {
  // ...
}

// Usage:
let viewData = .init(
  title: "Foo", 
  body: "Bar", 
  buttonText: "Baz", 
  cancellable: true
)

createMenu(viewData: viewData)

⬆ back to top

Use default arguments instead of short-circuiting or using conditionals

Default arguments are generally cleaner than short-circuits. Be aware that if you use them, your function will only provide default values for undefined arguments. Other "falsy" values like '', "", false, nil and 0 will not be replaced by default values.

Bad:

func createMicrobrewery(name: String?) {
  let breweryName = name ?? "Hipster Brew Co."
  // ...
}

Good:

func createMicrobrewery(breweryName: String = "Hipster Brew Co.") {
  // ...
}

⬆ back to top

Functions should do one thing

This is by far the most important rule in software engineering. When functions do more than one thing, they become difficult to compose, test, and reason about. When you can isolate a function to just do one action, they can be refactored easily, and your code will be much cleaner. If you take nothing else away from this guide other than this, you'll be ahead of many developers.

Bad:

func emailClients(clients: [Client]) {
  clients.forEach { client in
    let clientRecord = database.lookup(client)
    if clientRecord.isActive() {
      email(client)
    }
  }
}

Good:

func emailActiveClients(clients: [Client]) {
  clients
    .filter { isActiveClient(client: $0) }
    .forEach { email(client: $0) }
}

func isActiveClient(client: Client) -> Bool {
  let clientRecord = database.lookup(client)
  return clientRecord.isActive()
}

⬆ back to top

Function names should say what they do

Bad:

func addToDate(date: Date, month: Int) {
  // ...
}

let date = Date()

// It's hard to tell by the function name what is being added
addToDate(date: date, month: 1)

Good:

func addMonthToDate(month: Int, date: Date) {
  // ...
}

let date = Date()
addMonthToDate(month: 1, date: date)

⬆ back to top

Functions should have one level of abstraction

When you have more than one level of abstraction, your function is usually doing too much. Splitting your functions leads to better reusability and testability.

Bad:

func parseInput(input: String) {
  // ...
  let inputData = ...
  // ...
  saveData(inputData)
}

func saveData(data: Data) {
  // ...
  database.save(data)
}

Good:

func parseInput(input: String) {
  // ...
  let inputData = ...
  saveData(data: inputData)
}

func saveData(data: Data) {
  // ...
  database.save(data)
}

⬆ back to top

Remove duplicated code

Do your best to avoid duplicated code. Duplicated code is bad because it means there's more than one place to change something if you need to make a change.

Imagine if you run a restaurant and you keep a list of all your clients in two places: one where you keep the order for the chef and another where you keep the order for the delivery. If you have clients that cancel, now you have to cancel in two places. If you only have one list, there's only one place to update!

What if you forget to update in one place and not the other? What if the delivery guy shows up while the chef is making something and he hasn't seen the cancellation? Now you have a problem.

Often, you have duplicated code because you have two or more slightly different things, that share a lot in common but their differences force you to have two or more separate methods that do much of the same things. Removing duplicated code means creating an abstraction that can handle those differences with only one function/method.

By doing this, you now have only one place to update if something changes.

Bad:

func showDeveloper(name: String) {
  print("Developer: \(name)")
  print("Coding...")
}

func showManager(name: String) {
  print("Manager: \(name)")
  print("Meeting...")
}

Good:

func showPerson(name: String, role: String) {
  print("\(role): \(name)")
  if role == "Developer" {
    print("Coding...")
  } else if role == "Manager" {
    print("Meeting...")
  }
}

⬆ back to top

Use Pure Objects

Objects are said to be pure when they don't share state with other objects. Imagine you're in outer space and you have a spaceship. This spaceship has a fuel tank. Imagine there are various different systems in the spaceship that can modify this fuel tank.

There are three different types of objects here:

  1. Impure Object:
class Spaceship {
  var fuelTank: Int

  init(fuelTank: Int) {
    self.fuelTank = fuelTank
  }

  func launch() {
    Rocket().ignite(boosters: self.fuelTank)
  }

  func addFuel(fuel: Int) {
    self.fuelTank += fuel
  }
}
  1. Less Pure Object:
class Spaceship {
  var fuelTank: Int

  init(fuelTank: Int) {
    self.fuelTank = fuelTank
  }

  func launch() {
    Rocket().ignite(boosters: self.fuelTank)
  }

  func visitSpaceStation(spaceStation: SpaceStation) {
    spaceStation.refuel(ship: self)
  }
}

class SpaceStation {
  func refuel(ship: Spaceship) {
    ship.addFuel(fuel: self.fuelTank)
  }

  var fuelTank: Int
}
  1. Pure Object:
class Spaceship {
  var fuelTank: Int

  init(fuelTank: Int) {
    self.fuelTank = fuelTank
  }

  func launch() {
    Rocket().ignite(boosters: self.fuelTank)
  }

  func refuel(amount: Int) -> Spaceship {
    return Spaceship(fuelTank: self.fuelTank + amount)
  }
}

Why are pure objects preferable? They are easier to test and understand. They cannot be changed by other systems while they are being used. Data passed to them can be trusted, and they have no side effects that can cause difficult-to-trace bugs.

⬆ back to top

Make decisions based on an object

Bad:

if car.engine == "v8" {
  // ...
}

if bike.tires == "fat" {
  // ...


}

Good:

class Engine {
  var type: String
}

class Tire {
  var type: String
}

if car.engine.type == "v8" {
  // ...
}

if bike.tires.type == "fat" {
  // ...
}

⬆ back to top

Avoid Using Flags as Function Parameters

Functions that have boolean flags as parameters are harder to understand than functions that do only one thing. Flags indicate that the function does more than one thing. Separate these functions into multiple functions if necessary.

Bad:

func createFile(name: String, temporary: Bool) {
  if temporary {
    // Creates a temporary file
  } else {
    // Creates a permanent file
  }
}

Good:

func createTemporaryFile(name: String) {
  // Creates a temporary file
}

func createPermanentFile(name: String) {
  // Creates a permanent file
}

⬆ Back to Top

Avoid Side Effects (Part 1)

A function produces a side effect if it does something other than taking an input value and returning another value(s). A side effect can be writing to a file, modifying a global variable, or accidentally transferring all your money to a stranger.

Now, you need side effects occasionally in your program. Like in the previous example, you might need to write to a file. What you want to do is to centralize where you are doing this. Don't have multiple functions and classes that write to a particular file. Have one service that does it. One and only one.

The main point is to avoid pitfalls like sharing state between objects with no structure, using mutable data types that can be written to by anything, and not centralizing where your side effects occur. If you can do this, you will be much happier than the vast majority of other programmers.

Bad:

// Global variable referenced by the following function
// If we had another function that uses this name, then it would be an array and could break your code
var name = "Matheus Gois"

func splitIntoFirstAndLastName() {
  name = name.split(separator: " ").joined(separator: " ")
}

splitIntoFirstAndLastName()

print(name) // 'Matheus Gois'

Good:

func splitIntoFirstAndLastName(name: String) -> (firstName: String, lastName: String) {
  let components = name.split(separator: " ").map { String($0) }
  guard components.count >= 2 else {
    return (firstName: name, lastName: "")
  }
  let firstName = components[0]
  let lastName = components[1..<components.count].joined(separator: " ")
  return (firstName: firstName, lastName: lastName)
}

let fullName = "Matheus Gois"
let nameComponents = splitIntoFirstAndLastName(name: fullName)

print(fullName) // 'Matheus Gois'
print(nameComponents.firstName) // 'Ryan'
print(nameComponents.lastName) // 'McDermott'

⬆ Back to Top

Avoid Side Effects (Part 2)

In Swift, primitive types are passed by value, and objects/arrays are passed by reference. In the case of objects and arrays, if your function makes a change to a shopping cart array, for example, by adding an item to be purchased, then any other function that uses the cart array will also be affected by this addition. This can be great, but it can also be bad. Let's imagine a bad situation:

The user clicks the "Buy" button, which invokes the purchase function that sends a series of requests and sends the cart array to the server. Due to a poor internet connection, the purchase function needs to resend the request. Now, imagine that in the meantime, the user accidentally clicks the Add to Cart button on a product he didn't want before the request started. If this happens and the request is sent again, then the purchase function will accidentally send the array with the new product added because there is a reference to the cart array that the addItemToCart function modified by adding an unwanted product.

A great solution would be for the addItemToCart function to always clone the cart array, edit it, and then return its clone. This ensures that no other function that has a reference to the shopping cart is affected by any changes made.

Two caveats of this approach:

  1. There may be cases where you really want to change the input object, but when you adopt this kind of programming, you will find that these cases are quite rare. Most things can be refactored to avoid side effects.
  2. Cloning large objects can be quite expensive in terms of performance. Luckily, in practice, this is not a problem because there are great libraries that allow this type of programming to be fast and not as memory-intensive as it would be if you manually cloned objects and arrays.

Bad:

var cart = [
  CartItem(item: "Widget", date: Date()),
  CartItem(item: "Gadget", date: Date())
]

func addItemToCart(item: CartItem) {
  cart.append(item)
}

Good:

func addItemToCart(cart: [CartItem], item: CartItem) -> [CartItem] {
  var updatedCart = cart
  updatedCart.append(item)
  return updatedCart
}

Do Not Write to Global Functions

Polluting globals is a bad practice in Swift because you can conflict with another library, and the user of your API would have no idea until they got an exception thrown in production. Let's think about an example: what if you wanted to extend the native Swift Array method to have a diff method that could show the difference between two arrays? You could write your new function on Array.prototype, but it could collide with another library that tried to do the same thing. And what if this other library was just using diff to find the difference between the first and last element of an array?

Bad:

extension Array {
  func diff(_ comparisonArray: [Element]) -> [Element] {
    let hash = Set(comparisonArray)
    return filter { !hash.contains($0) }
  }
}

Good:

class ExtendedArray<Element>: Array<Element> {
  func diff(_ comparisonArray: [Element]) -> [Element] {
    let hash = Set(comparisonArray)
    return filter { !hash.contains($0) }
  }
}

⬆ Back to Top

Favor Functional Programming over Imperative Programming

Swift is not a functional language in the same way Haskell is, but it has a touch of functional in it. Functional languages are cleaner and easier to test. Favor this type of programming when you can.

Bad:

let programmerOutput = [
  Programmer(name: "Uncle Bobby", linesOfCode: 500),
  Programmer(name: "Suzie Q", linesOfCode: 1500),
  Programmer(name: "Jimmy Gosling", linesOfCode: 150),
  Programmer(name: "Gracie Hopper", linesOfCode: 1000)
]

var totalOutput = 0

for programmer in programmerOutput {
  totalOutput += programmer.linesOfCode
}

Good:

let programmerOutput

 = [
  Programmer(name: "Uncle Bobby", linesOfCode: 500),
  Programmer(name: "Suzie Q", linesOfCode: 1500),
  Programmer(name: "Jimmy Gosling", linesOfCode: 150),
  Programmer(name: "Gracie Hopper", linesOfCode: 1000)
]

let totalOutput = programmerOutput
  .map { $0.linesOfCode }
  .reduce(0, +)

⬆ Back to Top

Encapsulate Conditionals

Bad:

if fsm.state == "fetching" && isEmpty(listNode) {
  // ...
}

Good:

func shouldShowSpinner(fsm: FSM, listNode: Node) -> Bool {
  return fsm.state == "fetching" && isEmpty(listNode)
}

if shouldShowSpinner(fsm: fsmInstance, listNode: listNodeInstance) {
  // ...
}

⬆ Back to Top

Avoid Negations in Conditionals

Bad:

func isViewNotPresent(view: UIView) -> Bool {
  // ...
}

if !isViewNotPresent(view: view) {
  // ...
}

Good:

func isViewPresent(view: UIView) -> Bool {
  // ...
}

if isViewPresent(view: view) {
  // ...
}

⬆ Back to Top

Avoid Conditionals

This seems like an impossible task. The first time people hear this, they say, "How am I supposed to do anything without using if?" The answer is that you can use polymorphism to achieve the same task in different cases. The second question is usually, "Well, that's great, but why would I do that?" The answer is a previously learned clean code concept: a function should do only one thing. When you have classes and functions that have if statements, you're telling your user that your function does more than one thing. Remember, do one thing.

Bad:

class Airplane {
  // ...
  func getCruisingAltitude() -> Int {
    switch self.type {
      case "777":
        return self.getMaxAltitude() - self.getPassengerCount()
      case "Air Force One":
        return self.getMaxAltitude()
      case "Cessna":
        return self.getMaxAltitude() - self.getFuelExpenditure()
      default:
        return 0
    }
  }
}

Good:

class Airplane {
  // ...
}

class Boeing777: Airplane {
  // ...
  func getCruisingAltitude() -> Int {
    return self.getMaxAltitude() - self.getPassengerCount()
  }
}

class AirForceOne: Airplane {
  // ...
  func getCruisingAltitude() -> Int {
    return self.getMaxAltitude()
  }
}

class Cessna: Airplane {
  // ...
  func getCruisingAltitude() -> Int {
    return self.getMaxAltitude() - self.getFuelExpenditure()
  }
}

⬆ Back to Top

Avoid Type Checking (Part 1)

Swift does not have types, which means your functions can take any type of argument. Sometimes this freedom can bite you, and it becomes tempting to do type checking in your functions. There are many ways to avoid having to do this. The first thing to consider is consistent APIs.

Bad:

func travelToTexas(vehicle: Any) {
  if let bicycle = vehicle as? Bicycle {
    bicycle.pedal(currentLocation: self.currentLocation, newLocation: Location("texas"))
  } else if let car = vehicle as? Car {
    car.drive(currentLocation: self.currentLocation, newLocation: Location("texas"))
  }
}

Good:

func travelToTexas(vehicle: Vehicle) {
  vehicle.move(currentLocation: self.currentLocation, newLocation: Location("texas"))
}

⬆ Back to Top

Avoid Type Checking (Part 2)

If you are working with basic primitive values like strings and integers, and you cannot use polymorphism, but still feel the need to check the type, you should consider using TypeScript. It is an excellent alternative to regular Swift, as it provides static typing on top of Swift's standard syntax. The problem with manual checking in Swift is that to do it well requires so much extra verbosity that the false "type safety" you get doesn't compensate for the loss of readability. Keep your Swift clean, write good tests, and have good code reviews. Or alternatively, do all of that but with TypeScript (which, as I mentioned, is a great alternative!).

Bad:

func combine(val1: Any, val2: Any) -> String {
  if let number1 = val1 as? Int, let number2 = val2 as? Int {
    return String(number1 + number2)
  } else if let string1 = val1 as? String, let string2 = val2 as? String {
    return string1 + string2
  }

  fatalError("Must be of type String or Number")
}

Good:

func combine(val1: Any, val2: Any) -> String {
  return String(describing: val1) + String(describing: val2)
}

⬆ Back to Top

Remove Dead Code

Dead code is as bad as duplicate code. There is no reason to keep it in your codebase. If it's not being called, get rid of it. It will still be safe in your version history if you ever need it.

Bad:

func oldRequestModule(url: String) {
  // ...
}

func newRequestModule(url: String) {
  // ...
}

let req = newRequestModule
inventoryTracker(item: "apples", requestModule: req, url: "www.inventory-awesome.io")

Good:

func newRequestModule(url: String) {
  // ...
}

let req = newRequestModule
inventoryTracker(item: "apples", requestModule: req, url: "www.inventory-awesome.io")

⬆ Back to Top

Objects and Data Structures

Use Getters and Setters

Using getters and setters to access data in objects is much better than simply looking for a property on an object. "Why?" you might ask. Well, here's an unordered list of reasons:

  • When you want to do more than get a property of an object, you don't have to search and change all the accessors in your code.
  • Makes it easier to do validation when setting.
  • Encapsulates the internal representation.
  • Easier to add logging and error handling when getting and setting.
  • You can use lazy loading on your object's properties, for example, fetching it from a server.

Bad:

func makeBankAccount() -> [String: Any] {
  // ...

  return [
    "balance": 0,
    // ...
  ]
}

let account = makeBankAccount()
account

["balance"] = 100

Good:

func makeBankAccount() -> BankAccount {
  // This is private
  var balance = 0

  // A getter, made public through the returned object below
  func getBalance() -> Int {
    return balance
  }

  // A setter, made public through the returned object below
  func setBalance(amount: Int) {
    // ... validate before updating the balance
    balance = amount
  }

  return BankAccount(getBalance: getBalance, setBalance: setBalance)
}

let account = makeBankAccount()
account.setBalance(100)

⬆ Back to Top

Make Objects Have Private Members

This can be achieved through closures (for ES5 and above).

Bad:

class Employee {
  var name: String

  init(name: String) {
    self.name = name
  }

  func getName() -> String {
    return self.name
  }
}

let employee = Employee(name: "John Doe")
print("Employee name: \(employee.getName())") // Employee name: John Doe
employee.name = "Jane Doe"
print("Employee name: \(employee.getName())") // Employee name: Jane Doe

Good:

func makeEmployee(name: String) -> () -> String {
  var privateName = name

  return {
    return privateName
  }
}

let employee = makeEmployee(name: "John Doe")
print("Employee name: \(employee())") // Employee name: John Doe

⬆ Back to Top

Classes

Method Chaining

This pattern is very useful in Swift, and you'll find it in many libraries like jQuery and Lodash. It allows your code to be expressive and less verbose. For this reason, I say, use method chaining and see how your code becomes cleaner. In your class functions, just return self at the end of each function, and you can chain more class methods on it.

Bad:

class Car {
  var make: String
  var model: String
  var color: String

  init(make: String, model: String, color: String) {
    self.make = make
    self.model = model
    self.color = color
  }

  func setMake(_ make: String) -> Car {
    self.make = make
    return self
  }

  func setModel(_ model: String) -> Car {
    self.model = model
    return self
  }

  func setColor(_ color: String) -> Car {
    self.color = color
    return self
  }

  func save() {
    print("\(self.make) \(self.model) \(self.color)")
  }
}

let car = Car(make: "Ford", model: "F-150", color: "red")
car.setColor("pink").save()

Good:

class Car {
  private var make: String
  private var model: String
  private var color: String

  init(make: String, model: String, color: String) {
    self.make = make
    self.model = model
    self.color = color
  }

  func setMake(_ make: String) -> Car {
    self.make = make
    return self
  }

  func setModel(_ model: String) -> Car {
    self.model = model
    return self
  }

  func setColor(_ color: String) -> Car {
    self.color = color
    return self
  }

  func save() -> Car {
    print("\(self.make) \(self.model) \(self.color)")
    return self
  }
}

let car = Car(make: "Ford", model: "F-150", color: "red")
car.setColor("pink").save()

⬆ Back to Top

Prefer Composition Over Inheritance

As famously stated in the Design Patterns book by the Gang of Four, you should prefer composition over inheritance where you can. There are many good reasons to use inheritance and many good reasons to use composition. The main point for this maxim is that if your mind instinctively goes for inheritance, try to think if composition could better model your problem. In some cases, it might.

You might be wondering then, "when should I use inheritance?" It depends specifically on your problem, but this is a decent list of when inheritance makes more sense than composition:

  1. Your inheritance represents an "is-a" relationship and not a "has-a" relationship (Human→Animal vs. User->UserDetails).
  2. You can reuse code from the base classes (Humans can move like all animals).
  3. You want to make global changes to derived classes by changing only the base class. (Changing the caloric cost for all animals when they move).

Bad:

class Employee {
  var name: String
  var email: String

  init(name: String, email: String) {
    self.name = name
    self.email = email
  }

  // ...
}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData: Employee {
  var ssn: String
  var salary: Double

  init(name: String, email: String, ssn: String, salary: Double) {
    self.ssn = ssn
    self.salary = salary
    super.init(name: name, email: email)
  }

  // ...
}

Good:

class EmployeeTaxData {
  var ssn: String
  var salary: Double

  init(ssn: String, salary: Double) {
    self.ssn = ssn
    self.salary = salary
  }

  // ...
}

class Employee {
  var name: String
  var email: String
  var taxData: EmployeeTaxData?

  init(name: String, email: String) {
    self.name = name
    self.email = email
  }

  func setTaxData(ssn: String, salary: Double) {
    self.taxData = EmployeeTaxData(ssn: ssn, salary: salary)
  }
  // ...
}

⬆ Back to Top

SOLID

Single Responsibility Principle (SRP)

As stated in Clean Code, "There should never be more than one reason for a class to change." It's tempting to pack a class with many functionalities, like when you can only bring one suitcase on your flight. The problem with this is that your class will not be conceptually cohesive and will give it multiple reasons to change. Minimizing the number of times you need to change a class is important because if many functionalities are in one class and you change a portion of it, it can be difficult to understand how it will affect other modules that depend on it in your code.

Bad:

class UserSettings {
  var user: User

  init(user: User) {
    self.user = user
  }

  func changeSettings(settings: Settings) {
    if self.verifyCredentials() {
      // ...
    }
  }

  func verifyCredentials() -> Bool {
    // ...
  }
}

Good:

class UserAuth {
  var user: User

  init(user: User) {
    self.user = user
  }

  func verifyCredentials() -> Bool {
    // ...
  }
}

class UserSettings {
  var user: User
  var auth: UserAuth

  init(user: User) {
    self.user = user
    self.auth = UserAuth(user: user)
  }

  func changeSettings(settings: Settings) {
    if self.auth.verifyCredentials() {
      // ...
    }
  }
}

⬆ Back to Top

Open/Closed Principle (OCP)

As stated by Bertrand Meyer, "Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification." But what does that mean? This principle basically says that you should allow users to add new functionalities without changing existing code.

Bad:

class SwiftAdapter: Adapter {
  override init() {
    super.init()
    self.name = "SwiftAdapter"
  }
}

class ObjcAdapter: Adapter {
  override init() {
    super.init()
    self.name = "ObjcAdapter"
  }
}

class HttpRequester {
  var adapter: Adapter

  init(adapter: Adapter) {
    self.adapter = adapter
  }

  func fetch(url: String) -> Promise<Response> {
    if self.adapter.name == "SwiftAdapter" {
      return makeSwiftCall(url: url).then { response in
        // transform the response and return
      }
    } else if self.adapter.name == "httpObjcAdapter" {
      return make

HttpCall(url: url).then { response in
        // transform the response and return
      }
    }
    fatalError("Adapter not supported")
  }
}

func makeSwiftCall(url: String) -> Promise<Response> {
  // make the request and return the promise
}

func makeHttpCall(url: String) -> Promise<Response> {
  // make the request and return the promise
}

Good:

class SwiftAdapter: Adapter {
  override init() {
    super.init()
    self.name = "SwiftAdapter"
  }

  func request(url: String) -> Promise<Response> {
    // make the request and return the promise
  }
}

class ObjcAdapter: Adapter {
  override init() {
    super.init()
    self.name = "ObjcAdapter"
  }

  func request(url: String) -> Promise<Response> {
    // make the request and return the promise
  }
}

class HttpRequester {
  var adapter: Adapter

  init(adapter: Adapter) {
    self.adapter = adapter
  }

  func fetch(url: String) -> Promise<Response> {
    return self.adapter.request(url: url).then { response in
      // transform the response and return
    }
  }
}

⬆ Back to Top

Liskov Substitution Principle (LSP)

This is a scary term for a very simple concept. It's formally defined as "If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of a program (correctness, task performance, etc.)." That's an even scarier definition.

The best explanation for this concept is if you have a parent class and a child class, then the base class and the child class can be used interchangeably without getting incorrect results. This might still be confusing, so let's look at the classic example of the Square-Rectangle problem. Mathematically, a square is a rectangle, but if you model it using an "is-a" relationship through inheritance, you quickly run into problems.

Bad:

class Rectangle {
  var width: Double
  var height: Double

  init() {
    self.width = 0
    self.height = 0
  }

  func setColor(color: String) {
    // ...
  }

  func render(area: Double) {
    // ...
  }

  func setWidth(width: Double) {
    self.width = width
  }

  func setHeight(height: Double) {
    self.height = height
  }

  func getArea() -> Double {
    return self.width * self.height
  }
}

class Square: Rectangle {
  override func setWidth(width: Double) {
    self.width = width
    self.height = width
  }

  override func setHeight(height: Double) {
    self.width = height
    self.height = height
  }
}

func renderLargeRectangles(rectangles: [Rectangle]) {
  rectangles.forEach { rectangle in
    rectangle.setWidth(width: 4)
    rectangle.setHeight(height: 5)
    let area = rectangle.getArea() // BAD: Returns 25 for the Square. Should be 20.
    rectangle.render(area: area)
  }
}

let rectangles: [Rectangle] = [Rectangle(), Rectangle(), Square()]
renderLargeRectangles(rectangles: rectangles)

Good:

class Shape {
  func setColor(color: String) {
    // ...
  }

  func render(area: Double) {
    // ...
  }
}

class Rectangle: Shape {
  var width: Double
  var height: Double

  init(width: Double, height: Double) {
    self.width = width
    self.height = height
  }

  func getArea() -> Double {
    return self.width * self.height
  }
}

class Square: Shape {
  var length: Double

  init(length: Double) {
    self.length = length
  }

  func getArea() -> Double {
    return self.length * self.length
  }
}

func renderLargeShapes(shapes: [Shape]) {
  shapes.forEach { shape in
    let area = shape.getArea()
    shape.render(area: area)
  }
}

let shapes: [Shape] = [Rectangle(width: 4, height: 5), Rectangle(width: 4, height: 5), Square(length: 5)]
renderLargeShapes(shapes: shapes)

⬆ Back to Top

Interface Segregation Principle (ISP) with Protocols

Although Swift doesn't adopt the traditional concept of interfaces, we can apply the Interface Segregation Principle (ISP) through protocols, leveraging Swift's flexibility.

ISP states that "Clients should not be forced to depend on interfaces they do not use." In Swift, where duck typing prevails, we can create protocols that represent segregated interfaces.

Bad:

// Single protocol with many requirements
protocol Styling {
    var font: UIFont { get }
    var backgroundColor: UIColor { get }
    var cornerRadius: CGFloat { get }

    func applyStyles()
}

// Protocol implementation
class BadStylableView: Styling {
    var font: UIFont
    var backgroundColor: UIColor
    var cornerRadius: CGFloat

    init(font: UIFont, backgroundColor: UIColor, cornerRadius: CGFloat) {
        self.font = font
        self.backgroundColor = backgroundColor
        self.cornerRadius = cornerRadius
    }

    func applyStyles() {
        // Apply styles based on configuration
    }
}

// Example of usage
let badView = BadStylableView(font: .systemFont(ofSize: 14), backgroundColor: .white, cornerRadius: 8)

Good:

// Protocol for styling configuration
protocol StyleConfigurable {
    var font: UIFont { get }
    var backgroundColor: UIColor { get }
    var cornerRadius: CGFloat { get }
}

// Protocol for applying styles
protocol StyleApplicable {
    func applyStyles()
}

// Default implementation for styling configuration
struct AppearanceConfig: StyleConfigurable {
    var font: UIFont
    var backgroundColor: UIColor
    var cornerRadius: CGFloat
}

// View adopting the protocols
class GoodStylableView: StyleApplicable {
    var styleConfig: StyleConfigurable

    init(styleConfig: StyleConfigurable) {
        self.styleConfig = styleConfig
        self.setup()
    }

    func setup() {
        applyStyles()
    }

    func applyStyles() {
        // Apply styles based on configuration
    }
}

// Example of usage
let goodView = GoodStylableView(styleConfig: AppearanceConfig(font: .systemFont(ofSize: 14)))

In the bad example, a single Styling protocol contains many requirements, forcing clients to implement methods and properties that may not be necessary. In the good example, we use segregated protocols (StyleConfigurable and StyleApplicable) to allow for a more specific and flexible implementation.

⬆ Back to Top

Dependency Inversion Principle (DIP)

This principle tells us two essential things:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

This might be

hard to understand at first, but if you've worked with AngularSwift, you've seen an implementation of this principle in the form of Dependency Injection (DI). While they are not identical concepts, DIP does not allow high-level modules to know the details of their low-level modules but configures them. This can be achieved through DI. A significant benefit is that it reduces coupling between modules. Coupling is a very bad development pattern because it makes your code harder to refactor.

As mentioned earlier, Swift doesn't have interfaces, so the abstractions needed are implicit contracts. That means the methods and classes that an object/class exposes to other objects/classes. In the example below, the implicit contract is that any Request module for InventoryTracker will have a requestItems method:

Bad:

class InventoryRequester {
  init() {
    self.REQ_METHODS = ["HTTP"]
  }

  func requestItem(item: String) {
    // ...
  }
}

class InventoryTracker {
  var items: [String]
  var requester: InventoryRequester

  init(items: [String]) {
    self.items = items

    // Bad: We created a dependency on a specific request implementation.
    // We should only have requestItems depend on a request method: `request`
    self.requester = InventoryRequester()
  }

  func requestItems() {
    self.items.forEach { item in
      self.requester.requestItem(item: item)
    }
  }
}

let inventoryTracker = InventoryTracker(items: ["apples", "bananas"])
inventoryTracker.requestItems()

Good:

class InventoryTracker {
  var items: [String]
  var requester: RequesterProtocol

  init(items: [String], requester: RequesterProtocol) {
    self.items = items
    self.requester = requester
  }

  func requestItems() {
    self.items.forEach { item in
      self.requester.requestItem(item: item)
    }
  }
}

protocol RequesterProtocol {
  func requestItem(item: String)
}

class InventoryRequesterV1: RequesterProtocol {
  func requestItem(item: String) {
    // ...
  }
}

class InventoryRequesterV2: RequesterProtocol {
  func requestItem(item: String) {
    // ...
  }
}

// Building our dependencies externally and injecting them, we can easily
// swap our request module for a new fancy one that uses WebSockets
let inventoryTracker = InventoryTracker(items: ["apples", "bananas"], requester: InventoryRequesterV2())
inventoryTracker.requestItems()

⬆ Back to Top

Testing

Tests are more critical than shipping. If you have no tests or an inadequate amount, then every time you ship code, you won't be sure if you didn't break anything. Deciding what constitutes an adequate amount is up to your team, but having 100% coverage (all statements and branches) is how you achieve very high confidence and peace of mind. This means that in addition to having a great testing framework, you also need to use a good coverage tool.

There's no excuse for not writing tests. There are many great testing frameworks for Swift, so find one that your team prefers. When you find one that works for your team, then aim to always write tests for every new feature/module you introduce. If your preferred method is Test-Driven Development (TDD), that's great, but the main point is to ensure you are reaching your coverage goals before launching any feature or refactoring an existing one.

One Concept per Test

Bad:

import XCTest

class MakeMomentSwiftGreatAgainTests: XCTestCase {

  func testHandlesDateBoundaries() {
    var date = MakeMomentSwiftGreatAgain("1/1/2015")
    date.addDays(30)
    XCTAssertEqual("1/31/2015", date)

    date = MakeMomentSwiftGreatAgain("2/1/2016")
    date.addDays(28)
    XCTAssertEqual("02/29/2016", date)

    date = MakeMomentSwiftGreatAgain("2/1/2015")
    date.addDays(28)
    XCTAssertEqual("03/01/2015", date)
  }
}

Good:

import XCTest

class MakeMomentSwiftGreatAgainTests: XCTestCase {

  func testHandles30DayMonths() {
    let date = MakeMomentSwiftGreatAgain("1/1/2015")
    date.addDays(30)
    XCTAssertEqual("1/31/2015", date)
  }

  func testHandlesLeapYear() {
    let date = MakeMomentSwiftGreatAgain("2/1/2016")
    date.addDays(28)
    XCTAssertEqual("02/29/2016", date)
  }

  func testHandlesNonLeapYear() {
    let date = MakeMomentSwiftGreatAgain("2/1/2015")
    date.addDays(28)
    XCTAssertEqual("03/01/2015", date)
  }
}

⬆ Back to Top

Concurrency

⬆ back to top

Async/Await is even cleaner than Promises

After iOS 13, Swift introduces async and await that offer an even cleaner solution. All you need is a function prefixed with the async keyword, and then you can write your logic imperatively without using completions to chain your functions. Use this if you can take advantage of Swift's features today!

Bad:

import Foundation

let url = URL(string: "https://en.wikipedia.org/wiki/Robert_Cecil_Martin")!

URLSession.shared.dataTask(with: url) { (data, response, error) in
    guard let data = data, error == nil else {
        print(error?.localizedDescription ?? "Unknown error")
        return
    }

    do {
        try data.write(to: URL(fileURLWithPath: "article.html"))
        print("File written")
    } catch {
        print(error.localizedDescription)
    }
}.resume()

Good:

import Foundation

async func getCleanCodeArticle() {
    let url = URL(string: "https://en.wikipedia.org/wiki/Robert_Cecil_Martin")!

    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        try await data.write(to: URL(fileURLWithPath: "article.html"))
        print("File written")
    } catch {
        print(error.localizedDescription)
    }
}

Task {
    await getCleanCodeArticle()
}

⬆ back to top

Error Handling

throw error is a good thing! They mean the program has successfully identified when something went wrong and is letting you know by halting the execution of the current function, unwinding the process (in Swift), and notifying you in the console with the process stack.

Don't ignore caught errors

Doing nothing with a caught error doesn't give you the ability to address it or react to the reported error. Just printing a log to the console (print) is not much better because it can often get lost among a bunch of other things printed to the console. If you wrap any piece of code in a do/catch, it means you believe an error might occur there and then you should have a plan, or create a code path for when it happens.

Bad:

do {
    try funcThatMightThrow()
} catch {
    print(error)
}

Good:

do {
    try funcThatMightThrow()
} catch {
    // One option (more noticeable than print):
    print(error)
    // Another option:
    notifyUserOfError(error)
    // Another option:
    reportErrorToService(error)
    // OR all three!
}

Formatting

Formatting is subjective. Like many of the rules here, there's no hard and fast rule you need to follow. The main idea is NOT to argue about formatting. There are tools that automate this process; let them handle it. Most Swift code should follow the Google Swift Style Guide.

Use spaces instead of tabs

Despite the epic struggle between spaces and tabs, the important thing is to be consistent. Swift uses spaces, and it's a common practice in other Swift projects. Do the same.

Bad:

func bad() {
βˆ™βˆ™βˆ™βˆ™var name:String?
βˆ™βˆ™βˆ™βˆ™guard let unwrappedName = name else {
βˆ™βˆ™βˆ™βˆ™βˆ™βˆ™βˆ™return
βˆ™βˆ™βˆ™βˆ™}
}

Good:

func good() {
    var name: String?
    guard let unwrappedName = name else {
        return
    }
}

Use blank lines effectively

Separating code blocks with blank lines can make the code more readable and organized. However, excessive blank lines can have the opposite effect, creating a sense of fragmentation. Use blank lines sparingly.

Bad:

func calculateTotalScore(score: Int) {
    
    var totalScore = 0
    
    for i in 1...score {
        
        totalScore += i
        
    }
    
    print("The total score is: \(totalScore)")
    
}

Good:

func calculateTotalScore(score: Int) {
    var totalScore = 0
    
    for i in 1...score {
        totalScore += i
    }
    
    print("The total score is: \(totalScore)")
}

Limit line length

The recommended length for a code line is 80 characters. This ensures that you don't have to scroll horizontally to read the code. It's common in many development environments to have two files side by side. Making this possible makes reading the code easier.

Bad:

let errorMessage = "This is a very long error message that exceeds the recommended line length of 80 characters and makes the code harder to read."

Good:

let errorMessage = "This is a short error message."

Use consistent spacing

Maintaining consistency in the use of spaces around operators and after commas contributes to code readability.

Bad:

let sum = 1+2
let array = [1 , 2,3, 4]

Good:

let sum = 1 + 2
let array = [1, 2, 3, 4]

Avoid excessive whitespace

Excessive whitespace can visually clutter the code and make it less readable. Maintain a moderate use of whitespace.

Bad:

func   foo  (   a : Int  , b   :   Int   ) -> Int {
    return a + b
}

Good:

func foo(a: Int, b: Int) -> Int {
    return a + b
}

Follow the formatting recommended by SwiftLint

Code formatting can be a controversial topic, but it's important to maintain a consistent standard within your project. SwiftLint is a helpful tool for enforcing formatting standards. Integrate SwiftLint into your workflow to ensure the code follows recommended practices.

⬆ back to top

Comments

Only comment on things that have business logic complexity.

Comments should be used to explain parts of the code that have non-obvious complexity or to provide additional information about business logic. Avoid commenting on the obvious or trivial details that can be easily understood by reading the code.

Bad:

func calculateTotalScore(score: Int) {
  // Initialize the total score
  var totalScore = 0
  
  // Loop through each individual score
  for i in 1...score {
    // Add the individual score to the total score
    totalScore += i
  }
  
  // Print the total score
  print("The total score is: \(totalScore)")
}

Good:

func calculateTotalScore(score: Int) {
    // The total score is calculated using the Gauss sum formula
    var totalScore = (score * (score + 1)) / 2
    
    // Print the total score
    print("The total score

 is: \(totalScore)")
}

⬆ back to top

Don't leave commented-out code in your codebase

Version control (like Git) is a powerful tool for tracking changes over time. There's no need to keep commented-out code in the codebase as it only adds noise and makes it harder to read.

Bad:

func doSomething() {
    // Previous code that is no longer needed
    // ...
    
    // Code commented out for future reference
    /*
    if someCondition {
        // Code to be executed
    }
    */
    
    // ...
}

Good:

func doSomething() {
    // Current code
    // ...
}

⬆ back to top

Avoid change markers

Avoid using change markers, such as slashes or lines of asterisks, to divide or highlight sections of code. Instead, use good code structure with proper indentation and formatting to make the code easily understandable.

Bad:

struct Example {
    // MARK: - Properties
    var name: String
    var age: Int
    
    // MARK - Initializer
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    // MARK - Functions
    
    // Method to perform an action
    func performAction() {
        // ...
    }
}

Good:

struct Example {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    func performAction() {
        // ...
    }
}

⬆ back to top

clean-code-swift's People

Contributors

maatheusgois avatar math-gois avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.