1.
Access Control, Code Organization & Testing
Written by Eli Ganim
You declare Swift types with properties, methods, initializers and even other nested types. These elements make up the interface to your code or the API (Application Programming Interface).
As code grows in complexity, controlling this interface becomes an essential part of software design. You may wish to create methods that serve as “helpers” to your code or properties that keep track of internal states 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 your code’s viewable interface. Access control enables you, the library author, to hide implementation complexity from users.
This hidden internal state is sometimes called the invariant, which your public interface should always maintain. Preventing direct access to the internal state and keeping the invariant valid 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 serves 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 read-only — in other words, it only has a get
requirement.
However, BasicAccount
implements balance
as a variable that is both readable and writeable.
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
make it possible for outside to change the internal state arbitrarily.
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 protecting your code from malicious hackers. Instead, 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.
Add the access control modifier private(set)
to the definition of balance
in BasicAccount
:
private(set) var balance: Dollars
The access modifier above is placed before the property declaration and includes an optional get
/set
modifier in parentheses. In this example, the setter of balance
is made private
.
You’ll cover the details of private
shortly, but you can see it in action already: your code no longer compiles!
By adding private
to the property setter, the property becomes inaccessible to the consuming code.
This example demonstrates the fundamental benefit of access modifiers: they restrict access to code that needs or should have access to and prevent access to code that doesn’t need it. Effectively, access control allows you to control the code’s accessible interface while defining whatever properties, methods or types you need to implement the behavior you want.
The private
modifier used in the brief example above is one of several access modifiers available to you in Swift:
-
private
: Accessible only to the defining type, all nested types and extensions on that type within the same source file. -
fileprivate
: Accessible from anywhere within the source file in which it’s defined. -
internal
: Accessible from anywhere within the module in which it’s defined. This level is the default access level. If you don’t write anything, this is what you get. -
public
: Accessible from anywhere that imports the module. -
open
: The same aspublic
, with the additional ability granted to override the code in another module.
Next, you will learn more about these modifiers, when to use them, and how to apply them to your code.
private
The private
access modifier restricts access to the entity it is defined in and 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.
To demonstrate, continue with your banking library by extending the behavior of BasicAccount
to make a CheckingAccount
:
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
}
}
}
CheckingAccount
has an accountNumber
declared as private
. CheckingAccount
also has a nested type Check
that can read the private
value of accountNumber
in its initializer.
Note: In this example, the
UUID
type provides a unique account number. This class is part of the Foundation module, so don’t forget to import it!
Checking accounts should be able to write and cash checks as well. Add the following methods to CheckingAccount
:
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()
}
While CheckingAccount
can still make necessary deposits and withdrawals, it can now also write and deposit checks! The method writeCheck(amount:)
verifies a sufficient balance before withdrawing the amount and creating a check. deposit(_:)
will not deposit an already cashed check.
Give this code a try in your playground by having John write a check to Jane:
// 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
Of course, this code works great; the real story is what this code can’t do. Remember that access control lets you control the interface to your code. Look at what the autocomplete window shows as the interface for CheckingAccount
:
The accountNumber
is treated as an implementation detail of CheckingAccount
and isn’t visible to client code.
Likewise, Check
makes the setter for cashed
private and requires consumers to use cash()
instead:
This interface gives Check
a way for consumers to mark a check as deposited, but not the other way around! In other words, it is not possible to un-cash a check.
Finally, even though accountNumber
was not visible on CheckingAccount
, the number is made accessible by anyone holding a Check
:
While the account
property got its value from the CheckingAccount
, that’s another implementation detail. The important thing is that access modifiers let the code shape its interface regardless of the code used to implement it.
Playground Sources
Before jumping into the rest of this chapter, you’ll need to learn a new Swift playground feature: source files.
In Xcode, make sure the Project Navigator is visible by going to View\Navigators\Show Project Navigator. Under the playground tree, look for a slightly dimmed folder named Sources:
Right-click on the folder, select New File, and name the file Account.swift. Move the Account
protocol, the BasicAccount
class, and the Dollars
typealias to this file.
Create one more source file and name it Checking.swift. Move CheckingAccount
into this file.
That’s it! The critical thing to note about the Sources folder is that Xcode treats the code in it as a separate module.
You can comment out the rest of the code in your playground for now. It won’t be able to “see” the code you just moved until later in this chapter.
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. You’ll use the two new files you just created to try this out!
Right now, nothing prevents a haphazard coder who doesn’t read the documentation from creating a Check
on their own. In your safe code, you want a Check
to only originate from CheckingAccount
so that it can keep track of balances.
In the Check
class, try adding the private
modifier to the initializer:
private init(amount: Dollars, from account: CheckingAccount) { //...
While this prevents incorrect code from creating a Check,
you’ll notice it also prevents CheckingAccount
from creating one as well. private
entities can be accessed from anything within lexical scope. Still, in this case, CheckingAccount
is one step outside the scope of Check
. Fortunately, this is where fileprivate
is very useful.
Replace the initializer instead with fileprivate
:
fileprivate init(amount: Dollars, from account: CheckingAccount) { //...
Great! Now CheckingAccount
can still write checks, but you can’t create them from anywhere else.
The fileprivate
modifier is ideal for code that is “cohesive” within a source file; that is, code that is closely related or serves enough of a common purpose to have shared but protected access. Check
and CheckingAccount
are examples of two cohesive types.
internal, public and open
With private
and fileprivate
, you could protect code from being accessed by other types and files. These access modifiers modified access from the default access level of internal.
The internal
access level means that an entity can be accessed from anywhere within the software module in which it’s defined. So far, you’ve written all of your code in a single playground file, which means it’s all been in the same module.
When you added code to the Sources directory in your playground, you effectively created a module that your playground consumed. The way playgrounds work in Xcode, all files in the Sources directory are part of one module, and everything in the playground is another module that consumes the module in the Sources folder.
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)
// ...
CheckingAccount
has no specified access modifier and is treated as internal
, making it inaccessible to the playground.
The result is that Swift displays a build error when trying to use the CheckingAccount
type.
To remedy this, you must learn about the public
and open
access modifiers.
Note: Because
internal
is the default access level, you never need to explicitly declare your codeinternal
. Whether you use theinternal
keyword in your definitions is a matter of style and preference.
public
To make CheckingAccount
visible to your playground, you must 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.
Add the public
modifier to class CheckingAccount
:
public class CheckingAccount: BasicAccount {
You’ll also need to add public
to BasicAccount
since CheckingAccount
subclasses it:
public class BasicAccount: Account
The playground will now recognize CheckingAccount
, yet you’re still unable to instantiate it.
While the type itself is now public, its members are still internal and unavailable outside the module. You’ll need to add public
modifiers to all the entities you want to be part of your module’s interface.
Start by adding a public initializer to BasicAccount
and CheckingAccount
:
// In BasicAccount:
public init() { }
// In CheckingAccount:
public override init() { }
Next, in BasicAccount
, add public
to balance
, deposit(amount:)
and withdraw(amount:)
. You’ll also need to make the Dollars
typealias public, as this typealias appears in public methods.
Finally, in CheckingAccount
, add public
to writeCheck(amount:)
, deposit(_:)
and class Check
. Save all files. You’ll find that everything builds and runs!
Note: Even though
BasicAccount
adoptsAccount
, you may notice that the playground can’t seeAccount
, nor does it know thatBasicAccount
conforms to it. Protocol conformance will be invisible to consuming modules if the protocol itself is not accessible.
open
Now that CheckingAccount
and its public members are visible to the playground, you can use your banking interface as designed.
Well — almost! The banking library should provide a set of standard accounts such as checking accounts and be open to extensibility for any special kind of account a bank may have.
In your playground, create an interest-accumulating SavingsAccount
that subclasses BasicAccount
:
class SavingsAccount: BasicAccount {
var interestRate: Double
init(interestRate: Double) {
self.interestRate = interestRate
}
func processInterest() {
let interest = balance * interestRate
deposit(amount: interest)
}
}
While BasicAccount
is declared public
and is accessible to the playground, Swift will show a build error when trying to subclass BasicAccount
:
You can override a class, method, or property from another model by declaring it open
. Go to the file Account.swift and replace the public
access modifier for class BasicAccount
with open
:
open class BasicAccount: Account { //..
Do you see it all coming together? The interfaces you’ve crafted using public
and open
permit subclassing of BasicAccount
to provide new types of accounts. withdraw(amount:)
and deposit(amount:)
, because they’re public, can be used by those subclasses. The implementations of withdraw(amount:)
and deposit(amount:)
are safe from being overridden because they’re only public, not open!
Imagine if you could override withdraw(amount:)
and deposit(amount:)
:
override func deposit(amount: Dollars) {
// LOL
super.deposit(amount: 1_000_000.00)
}
This capability presents a problem!
If you’re creating a library, you often want to restrict the ability to override methods and properties so you can avoid otherwise surprising behavior. The open
access modifier lets you explicitly control what other modules do to your code.
Mini-exercises
- Create a struct
Person
in a new Sources file. This struct should havefirst
,last
andfullName
properties readable but not writable by the playground. - Create a similar type, except make it a class and call it
ClassyPerson
. In the playground, subclassClassyPerson
with classDoctor
and make a doctor’sfullName
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, making different parts of your code less dependent on others. As you learned earlier, highly cohesive code helps closely related code work together to fulfill a task.
Swift features such as access modifiers, when used with extensions, can help you both organize your code and encourage good software design.
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 code sections as public
, internal
or private
.
Begin by adding some basic fraud protection to CheckingAccount
. Add the following properties to CheckingAccount
:
private var issuedChecks: [Int] = []
private var currentCheck = 1
These will keep track of checks written by the checking account.
Next, add the following private extension:
private extension CheckingAccount {
func inspectForFraud(with checkNumber: Int) -> Bool {
issuedChecks.contains(checkNumber)
}
func nextNumber() -> Int {
let next = currentCheck
currentCheck += 1
return next
}
}
CheckingAccount
can use these two methods to determine the check number and confirm that the account issued it.
Notably, this extension is marked private
. A private
extension implicitly denotes all its members as private
. These fraud protection tools are features of CheckingAccount
only — you don’t want other code incrementing the currentCheck
number! Putting these two methods together also connects two related, cohesive methods. It’s clear to yourself and anyone else maintaining the code that these two are cohesive and help solve a common task.
Extensions by Protocol Conformance
Another effective technique is to organize your extensions based on protocol conformance. You’ve already seen this technique used in “Swift Apprentice: Fundamentals — Chapter 17: Protocols”. As an example, make CheckingAccount
conform to CustomStringConvertible
by adding the following extension:
extension CheckingAccount: CustomStringConvertible {
public var description: String {
"Checking Balance: $\(balance)"
}
}
This extension implements CustomStringConvertible
and, more importantly:
- Makes it obvious
description
is part ofCustomStringConvertible
. - Doesn’t help conform to other protocols.
- Can easily be removed without doing collateral damage to the rest of
CheckingAccount
. - It’s easy to understand!
available()
If you look at SavingsAccount
, you’ll notice that you can abuse processInterest()
by calling it multiple times and repeatedly adding interest to the account. You can add a PIN to the account to make this function more secure.
Add a pin
property to SavingsAccount
, and ensure the initializer and processInterest()
method take this PIN as a parameter. The class should look like this:
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)
}
}
}
You’re thrilled with the new layer of security. However, after you send this updated code to the bank, you get angry phone calls. The bank’s code now doesn’t compile because it used your old SavingsAccount
class.
To prevent breaking code that uses the old implementation, you need to deprecate the code rather than replace it. Luckily, Swift has built-in support for this.
Bring back the old implementation of the initializer and processInterest()
, and add this line of code before the initializer:
@available(*, deprecated, message: "Use init(interestRate:pin:) instead")
And this line of code before processInterest()
:
@available(*, deprecated, message: "Use processInterest(pin:) instead")
Now, these methods still work as expected; however, Xcode generates a warning with your custom message when someone tries to use them:
The asterisk in the parameters denotes which platforms are affected by this deprecation. It accepts the values *
, iOS
, iOSMac
, tvOS
or watchOS
. The second parameter details whether this method is deprecated
, renamed
or unavailable
.
Opaque Return Types
Imagine you need to create a public API for users of your banking library. You must make a function called createAccount
to create and return a new account. One of the requirements of this API is to hide implementation details so that clients are encouraged to write generic code. It means you shouldn’t expose the type of account you’re creating, be it a BasicAccount
, CheckingAccount
or SavingsAccount
. Instead, you’ll return some instance that conforms to the protocol Account
.
To enable that, you must first make the Account
protocol public. Open Account.swift and add the public
modifier before protocol Account
. Now go back to your playground and insert this code:
func createAccount() -> Account {
CheckingAccount()
}
You’ll notice you get an error:
To solve this, you can add the keyword some
before the return type so that it would look like this:
func createAccount() -> some Account {
CheckingAccount()
}
This code is an opaque return type, and it lets the function decide what kind of Account
it wants to return without exposing the class type.
You’ll learn more about this feature in “Chapter 11: Advanced Protocols & Generics”.
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 easily use it in their code. For example, a module that implements the logic of downloading images from the web is useful in many projects. Instead of copying & pasting the code to all your projects needing image downloading functionality, you could import and reuse this module.
Swift Package Manager is out of scope for this book; however, you can read more about it here: https://swift.org/package-manager/
.
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 functionality of the code you’ve written. This change is risky since they’re unfamiliar with the code, and their changes might introduce bugs to the existing logic. An excellent way to prevent this from happening is to write unit tests.
Unit tests are pieces of code that test existing code to ensure it works as expected. For example, you might write a test that deposits $100 to a new account and then verifies the balance is indeed $100.
It might sound like overkill at first, but when many engineers are working on a codebase or going back to make changes to the code you wrote long ago, unit tests help you verify that you don’t break anything.
Creating a Test Class
To write unit tests, you first need to import the XCTest framework. Add this at the top of the playground:
import XCTest
Next, you need to create a new class that’s a subclass of XCTestCase
:
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 useful unit tests. Those criteria are:
- Fast: Tests should run quickly.
- Independent/Isolated: Tests should not share state.
- Repeatable: You should obtain the same results every time you run a test.
- Self-validating: Tests should be fully automated, and the output should be either “pass” or “fail”.
- Timely: Ideally, write tests before writing the code they test (Test-Driven Development).
Adding tests to a test class is super easy - just add a function that starts with the word test
, takes no arguments and returns nothing.
func testSomething() {
}
Congratulations! You’ve just written your first test.
To run your tests in the playground, add this at the bottom, outside of the BankingTests
class.
BankingTests.defaultTestSuite.run()
Now run the playground, and you’ll see something similar to this printed to the console:
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
The test passed, which is unsurprising since it currently does nothing.
XCTAssert
XCTAssert
functions ensure your tests meet certain conditions. 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 with a zero balance. Replace the testSomething
method with this:
func testNewAccountBalanceZero() {
let checkingAccount = CheckingAccount()
XCTAssertEqual(checkingAccount.balance, 0)
}
The method XCTAssertEqual
verifies that the two parameters are equal, or else it fails the test. Note how the name of the test explicitly states what it tests.
If you run your playground now, this should appear in your console:
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' started.
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' passed (0.030 seconds).
Awesome, your test is passing! If someone makes changes that inadvertently cause new accounts to start with a balance other than zero, the test will fail. Why not test it? Open the file Account.swift
and find this line:
public private(set) var balance: Dollars = 0.0
Replace the 0.0
with 1.0
. Now run the test in your playground, and you should see this printed to the console:
error: -[BankingTests testNewAccountBalanceZero] : XCTAssertEqual failed: ("1.0") is not equal to ("0.0")
You can see the test fails, and it even tells you why it failed! This functionality is the real power of unit tests. From now on, tests will protect your accounts code from this mistake.
Now go ahead and return the variable balance
to 0.0
and then add one more test:
func testCheckOverBudgetFails() {
let checkingAccount = CheckingAccount()
let check = checkingAccount.writeCheck(amount: 100)
XCTAssertNil(check)
}
Can you figure out what this test does? It creates a new account and then tries to write a check for $100. The account balance is zero, so this test verifies that writing a check fails and returns nil
.
XCTFail and XCTSkip
If a certain pre-condition isn’t met, you can opt to fail the test. For example, suppose you’re writing a test to verify an API only available on iOS 15 and above. In that case, you can fail the test for iOS simulators running older versions with an informative message:
func testNewAPI() {
guard #available(iOS 15, *) else {
XCTFail("Only available in iOS 15 and above")
return
}
// perform test
}
Alternatively, instead of failing the test, you can skip it. XCTSkip
is a type of Error
a test can throw.
func testNewAPI() throws {
guard #available(iOS 15, *) else {
throw XCTSkip("Only available in iOS 15 and above")
}
// perform test
}
Making Things @testable
When you import Foundation
, Swift brings in the public interface for that module. You might create a Banking
module for your banking app that imports the public interface. But you might want to check the 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
This attribute makes your internal interface visible. (Note: Private API remains private.) This technique is an excellent tool for testing, but you should never do this in production code. Always stick to the public API there.
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 executes before each test, and its purpose is to initialize the needed state for the tests to run.
Add this at the top of your BankingTests
class:
var checkingAccount: CheckingAccount!
override func setUp() {
super.setUp()
checkingAccount = CheckingAccount()
}
and remove the line let checkingAccount = CheckingAccount()
from both tests.
Just as setUp
executes before each test, tearDown
runs after every test. It doesn’t matter whether the test passes or fails. It’s good when you need to release resources you acquired or when you need to reset the state of an object. For example, you could reset the balance of the CheckingAccount
instance to zero. This code is unnecessary since setUp
will initialize new accounts, but you can add it for the sake of the example.
Add this below the setUp
method:
override func tearDown() {
checkingAccount.withdraw(amount: checkingAccount.balance)
super.tearDown()
}
You can read more about unit tests at https://developer.apple.com/documentation/xctest
.
Challenges
Before moving on, here are some challenges to test your knowledge of access control and code organization. It is best to 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.
Use access modifiers to create a singleton class Logger
. This Logger
should:
- Provide shared, public, global access to the single
Logger
object. - Not be able to be instantiated by consuming code.
- Have a method
log()
that will print a string to the console.
Challenge 2: Stack
Declare a generic type Stack
. A stack is a LIFO (last-in-first-out) data structure that supports the following operations:
-
peek
: returns the top element on the stack without removing it. Returnsnil
if the stack is empty. -
push
: adds an element on top of the stack. -
pop
: returns and removes the top element on the stack. Returnsnil
if the stack is empty. -
count
: returns the size of the stack.
Ensure that these operations are the only exposed interface. In other words, additional properties or methods needed to implement the type should not be visible.
Challenge 3: Character Battle
Utilize something called a static factory method to create a game of Wizards vs. Elves vs. Giants.
Add a file Characters.swift in the Sources folder of your playground.
To begin:
- Create an enum
GameCharacterType
that defines values forelf
,giant
andwizard
. - Create a protocol
GameCharacter
that inherits fromAnyObject
and has propertiesname
,hitPoints
andattackPoints
. Implement this protocol for every character type. - Create a struct
GameCharacterFactory
with a single static methodmake(ofType: GameCharacterType) -> GameCharacter
. - Create a global function
battle
that pits two characters against each other — with the first character striking first! If a character reaches 0 hit points, they have lost.
Hints:
- The playground should not be able to see the concrete types that implement
GameCharacter
. - Elves have 3 hit points and 10 attack points. Wizards have 5 hit points and 5 attack points. Giants have 10 hit points and 3 attack points.
- The playground should know none of the above!
In your playground, you should use the following scenario as a test case:
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
andopen
. Theinternal
access level is the default. - Modifiers control your code’s visible interface and can hide complexity.
-
private
andfileprivate
protect code from being accessed by code in other types or files, respectively. -
public
andopen
allow code access from another module. Theopen
modifier additionally lets you override from other modules. - When you apply access modifiers 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. - Adding methods beginning with
test
to yourXCTestCase
derived test class will automatically add the method to your suite of unit tests. -
XCTAssertEqual
lets you check invariants in your unit tests.