Chapters

Hide chapters

Swift Apprentice

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 10 chapters
Show chapters Hide chapters

18. Access Control, Code Organization & Testing
Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Swift types can be declared with properties, methods, initializers and even other nested types. These elements can be thought of as the interface to your code and is sometimes referred to as the API or Application Programming Interface.

As code grows in complexity, controlling this interface becomes an important part of software design. You may wish to create methods that serve as “helpers” to your code, or properties that are designed to keep track of internal states that you don’t want as part of your code’s interface.

Swift solves these problems with a feature area known as access control, which lets you control the viewable interface of your code. Access control lets you, the library author, hide implementation complexity from users.

This hidden internal state is sometimes referred to as the invariant, which your public interface should always maintain. Preventing direct access to the internal state of a model and maintaining the invariant is a fundamental software design concept known as encapsulation. In this chapter, you will learn what access control is, the problems it solves, and how to apply it.

Problems introduced by lack of access control

Imagine for a moment you are writing a banking library. This library would help serve as the foundation for your customers (other banks) to write their banking software.

In a playground, start with the following protocol:

/// A protocol describing core functionality for an account
protocol Account {
  associatedtype Currency

  var balance: Currency { get }
  func deposit(amount: Currency)
  func withdraw(amount: Currency)
}

This code contains Account, a protocol that describes what any account should have — the ability to deposit, withdraw, and check the balance of funds.

Now add a conforming type with the code below:

typealias Dollars = Double

/// A U.S. Dollar based "basic" account.
class BasicAccount: Account {

  var balance: Dollars = 0.0

  func deposit(amount: Dollars) {
    balance += amount
  }

  func withdraw(amount: Dollars) {
    if amount <= balance {
      balance -= amount
    } else {
      balance = 0
    }
  }
}

This conforming class, BasicAccount, implements deposit(amount:) and withdraw(amount:) by simply adding or subtracting from the balance (typed in Dollars, an alias for Double). Although this code is very straightforward, you may notice a slight issue. The balance property in the Account protocol is designed to be read-only — in other words, it only has a get defined.

However, the implementation of BasicAccount requires balance to be declared as a variable so that the value can be updated when funds are deposited or withdrawn.

Nothing can prevent other code from directly assigning new values for balance:

// Create a new account
let account = BasicAccount()

// Deposit and withdraw some money
account.deposit(amount: 10.00)
account.withdraw(amount: 5.00)

// ... or do evil things!
account.balance = 1000000.00

Oh no! Even though you carefully designed the Account protocol to only be able to deposit or withdraw funds, the implementation details of BasicAccount that allow it to update its own balance could be used by any code.

Fortunately, you can use access control to limit the scope at which your code is visible to other types, files or even software modules!

Note: Access control is not a security feature that protects your code from malicious hackers. Rather, it lets you express intent by generating helpful compiler errors if a user attempts directly access implementation details that may compromise the invariant, and therefore, correctness.

Introducing access control

You can add access modifiers by placing a modifier keyword in front of a property, method or type declaration.

private(set) var balance: Dollars

Private

The private access modifier restricts access to the entity it is defined in, as well as any nested type within it — also known as the “lexical scope”. Extensions on the type within the same source file can also access the entity.

class CheckingAccount: BasicAccount {
  private let accountNumber = UUID().uuidString

  class Check {
    let account: String
    var amount: Dollars
    private(set) var cashed = false

    func cash() {
      cashed = true
    }

    init(amount: Dollars, from account: CheckingAccount) {
      self.amount = amount
      self.account = account.accountNumber
    }
  }
}
func writeCheck(amount: Dollars) -> Check? {
  guard balance > amount else {
    return nil
  }

  let check = Check(amount: amount, from: self)
  withdraw(amount: check.amount)
  return check
}

func deposit(_ check: Check) {
  guard !check.cashed else {
    return
  }

  deposit(amount: check.amount)
  check.cash()
}
// Create a checking account for John. Deposit $300.00
let johnChecking = CheckingAccount()
johnChecking.deposit(amount: 300.00)

// Write a check for $200.00
let check = johnChecking.writeCheck(amount: 200.0)!

// Create a checking account for Jane, and deposit the check.
let janeChecking = CheckingAccount()
janeChecking.deposit(check)
janeChecking.balance // 200.00

// Try to cash the check again. Of course, it had no effect on
// Jane’s balance this time :]
janeChecking.deposit(check)
janeChecking.balance // 200.00

Playground sources

Before jumping into the rest of this chapter, you’ll need to learn a new feature of Swift playgrounds: source files.

Fileprivate

Closely related to private is fileprivate, which permits access to any code written in the same file as the entity, instead of the same lexical scope and extensions within the same file that private provides.

private init(amount: Dollars, from account: CheckingAccount) { //...
fileprivate init(amount: Dollars, from account: CheckingAccount) { //...

Internal, public and open

With private and fileprivate you were able to protect code from being accessed by other types and files. These access modifiers modified access from the default access level of internal.

Internal

Back in your playground, uncomment the code that handles John writing checks to Jane:

// Create a checking account for John. Deposit $300.00
let johnChecking = CheckingAccount()
johnChecking.deposit(amount: 300.00)
// ...

Public

To make CheckingAccount visible to your playground, you’ll need to change the access level from internal to public. An entity that is public can be seen and used by code outside the module in which it’s defined.

public class CheckingAccount: BasicAccount {
public class BasicAccount: Account

// In BasicAccount:
public init() { }

// In CheckingAccount:
public override init() { }

Open

Now that CheckingAccount and its public members are visible to the playground, you can use your banking interface as designed.

class SavingsAccount: BasicAccount {
  var interestRate: Double

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

  func processInterest() {
    let interest = balance * interestRate
    deposit(amount: interest)
  }
}

open class BasicAccount: Account { //..
override func deposit(amount: Dollars) {
    // LOL
    super.deposit(amount: 1_000_000.00)
}

Mini-exercises

  1. Create a struct Person in a new Sources file. This struct should have first, last and fullName properties that are readable but not writable by the playground.
  2. Create a similar type, except make it a class and call it ClassyPerson. In the playground, subclass ClassyPerson with class Doctor and make a doctor’s fullName print the prefix "Dr.".

Organizing code into extensions

A theme of access control is the idea that your code should be loosely coupled and highly cohesive. Loosely coupled code limits how much one entity knows about another, which in turn makes different parts of your code less dependent on others. Highly cohesive code, as you learned earlier, helps closely related code work together to fulfill a task.

Extensions by behavior

An effective strategy in Swift is to organize your code into extensions by behavior. You can even apply access modifiers to extensions themselves, which will help you categorize entire sections of code as public, internal or private.

private var issuedChecks: [Int] = []
private var currentCheck = 1
private extension CheckingAccount {
  func inspectForFraud(with checkNumber: Int) -> Bool {
    issuedChecks.contains(checkNumber)
  }

  func nextNumber() -> Int {
    let next = currentCheck
    currentCheck += 1
    return next
  }
}

Extensions by protocol conformance

Another effective technique is to organize your extensions based on protocol conformance. You’ve already seen this technique used in Chapter 16, “Protocols”. As an example, let’s make CheckingAccount conform to CustomStringConvertible by adding the following extension:

extension CheckingAccount: CustomStringConvertible {
  public var description: String {
    "Checking Balance: $\(balance)"
  }
}

available()

If you take a look at SavingsAccount, you’ll notice that you can abuse processInterest() by calling it multiple times and adding interest to the account repeatedly. To make this function more secure, you can add a PIN to the account.

class SavingsAccount: BasicAccount {
  var interestRate: Double
  private let pin: Int
  
  init(interestRate: Double, pin: Int) {
    self.interestRate = interestRate
    self.pin = pin
  }
  
  func processInterest(pin: Int) {
    if pin == self.pin {
      let interest = balance * interestRate
      deposit(amount: interest)
    }
  }
}
@available(*, deprecated, message: "Use init(interestRate:pin:) instead")
@available(*, deprecated, message: "Use processInterest(pin:) instead")

Opaque return types

Imagine you need to create a public API for users of your banking library. You’re required to create a function called createAccount that creates a new account and returns it. One of the requirements of this API is to hide implementation details so that clients are encouraged to write generic code. It means that you shouldn’t expose the type of account you’re creating, be it a BasicAccount, CheckingAccount or SavingsAccount. Instead you’ll just return some instance that conforms to the protocol Account.

func createAccount() -> Account {
  CheckingAccount()
}

func createAccount() -> some Account {
  CheckingAccount()
}

Swift Package Manager

Another powerful way to organize your code is to use Swift Package Manager, or SwiftPM for short. SwiftPM lets you “package” your module so that you or other developers can use it in their code with ease.

Testing

Imagine new engineers join your team to work on your banking library. These engineers are tasked with updating the SavingsAccount class to support taking loans. For that they will need to update the basic functionally of the code you’ve written. This is risky, since they’re not familiar with the code and their changes might introduce bugs to the existing logic. A good way to prevent this from happening is to write unit tests.

Creating a test class

In order to write unit tests, you first need to import the XCTest framework. Add this at the top of the playground:

import XCTest
class BankingTests: XCTestCase {
}

Writing tests

Once you have your test class ready, it’s time to add some tests. Tests should cover the core functionality of your code and some edge cases. The acronym FIRST describes a concise set of criteria for effective unit tests. Those criteria are:

func testSomething() {
}
BankingTests.defaultTestSuite.run()
Test Suite 'BankingTests' started at ...
Test Case '-[__lldb_expr_2.BankingTests testSomething]' started.
Test Case '-[__lldb_expr_2.BankingTests testSomething]' passed (0.837 seconds).
Test Suite 'BankingTests' passed at ...
	 Executed 1 test, with 0 failures (0 unexpected) in 0.837 (0.840) seconds

XCTAssert

XCTAssert functions are used in tests to assert certain conditions are met. For example, you can verify that a certain value is greater than zero or that an object isn’t nil. Here’s an example of how to check that a new account starts off with a balance of zero. Replace the testSomething method with this:

func testNewAccountBalanceZero() {
  let checkingAccount = CheckingAccount()
  XCTAssertEqual(checkingAccount.balance, 0)
}
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' started.
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' passed (0.030 seconds).
public private(set) var balance: Dollars = 0.0
error: -[BankingTests testNewAccountBalanceZero] : XCTAssertEqual failed: ("1.0") is not equal to ("0.0")
func testCheckOverBudgetFails() {
    let checkingAccount = CheckingAccount()
    let check = checkingAccount.writeCheck(amount: 100)
    XCTAssertNil(check)
}

Making things @testable

When you import Foundation, Swift brings in the public interface for that module. For your banking app, you might create a Banking module that you can import. This lets you see the public interface. But you might want to check internal state with XCTAssert. Instead of making things public that really shouldn’t be you can do this in your test code:

@testable import Banking

The setUp and tearDown methods

You’ll notice that both test methods start by creating a new checking account, and it’s likely that many of the tests you’d write will do the same. Luckily there’s a setUp method. This method is executed before each test, and its purpose is to initialize the needed state for the tests to run.

var checkingAccount: CheckingAccount!

override func setUp() {
  super.setUp()
  checkingAccount = CheckingAccount()
}
override func tearDown() {
  checkingAccount.withdraw(amount: checkingAccount.balance)
  super.tearDown()
}

Challenges

Before moving on, here are some challenges to test your knowledge of access control and code organization. It is best if you try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Singleton pattern

A singleton is a design pattern that restricts the instantiation of a class to one object.

Challenge 2: Stack

Declare a generic type Stack. A stack is a LIFO (last-in-first-out) data structure that supports the following operations:

Challenge 3: Character battle

Utilize something called a static factory method to create a game of Wizards vs. Elves vs. Giants.

let elf = GameCharacterFactory.make(ofType: .elf)
let giant = GameCharacterFactory.make(ofType: .giant)
let wizard = GameCharacterFactory.make(ofType: .wizard)

battle(elf, vs: giant) // Giant defeated!
battle(wizard, vs: giant) // Giant defeated!
battle(wizard, vs: elf) // Elf defeated!

Key points

  • Access control modifiers are private, fileprivate, internal, public and open. The internal access level is the default.
  • Modifiers can be used to control your code’s visible interface and hide complexity.
  • private and fileprivate protect code from being accessed by code in other types or files, respectively.
  • public and open allow code to be accessed from another module. The open modifier additionally allows entities to be overridden by other modules.
  • When access modifiers are applied to extensions, all members of the extension receive that access level.
  • Extensions that mark protocol conformance cannot have access modifiers.
  • The keyword available can be used to evolve a library by deprecating APIs.
  • You use unit tests to verify your code works as expected.
  • @testable import lets you test internal API.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now