This article shows how we can leverage the power of Swift value types to encapsulate domain data, logic and business rules. It keeps classes focused on maintaining the identity of entities in the domain and managing state changes through their life cycles, helping to deal effectively with domain complexity and facilitating efficient, easily testable and concurrency-friendly programs.
Related articles:
- When and How to Use Value and Reference Types in Swift
- When and How to Use the Equatable and Identifiable Protocols in Swift
Contents
Representing immutable state
Dealing with mutable state
1. Swift value types and mutability
2. Controlling mutability of value types
The interplay of value and reference types
Usage considerations
Benefits
1. Separation of concerns
2. Testability
3. Efficiency
4. Concurrency
Conclusion
Designing with value and reference types
Swift offers a rich collection not just of reference types, in the form of classes and closures, but also of value types, in the form of structs, enums and tuples. Enums, tuples and closures are more specialized in what they do, so structs and classes are the most commonly used value and reference types respectively. To be able to program effectively with value and reference types, it is vitally important to understand the difference between value and reference semantics.
Values are used in almost all programs. Usually, these are basic values, like numbers and strings. Swift uses structs to represent numbers and strings. But structs in Swift can do a lot more than just represent simple data values. They can define properties and methods, specify initializers and conform to protocols, enabling abstraction and polymorphism. Since basic Swift data types (like numbers and strings) and basic container types (like arrays) are structs, even their functionality can be extended.
The real power of value types comes from composition, where we combine relatively simple value types to build value types capable of encapsulating the data, logic and business rules related to the various responsibilities of entities in the domain. The entities themselves are represented using classes because they have identity, a life cycle, and state that needs to be managed through that life cycle.
As an example, let’s consider a bank account as an entity that we need to model. A bank account has an identity in the domain, a life cycle and state that needs to be preserved. It is appropriate, therefore, to use a class to model a bank account. However, rather than burdening the class with the data, logic and business rules related to all the responsibilities of a bank account, we can design domain-specific value types to encapsulate the data, logic and business rules related to each of the major responsibilities of the class. This way, the class will be directly responsible only for maintaining the identity of the bank account and preserving its state.
To keep things manageable, let’s look at the two basic attributes of a bank account – account number and balance. Our first instinct may be to model the account number as a String and the account balance using a basic value type, like the Decimal type from the Standard Library, or a general-purpose Money type. However, once we start to get a deeper insight into the domain, we realize that there is more to it than that. A bank account number is not just a simple number. It is usually composed of components, which may need to be combined in different ways, often with other static elements, for different purposes, according to specific rules. Likewise for the account balance. There is usually a ledger balance and an available balance. There could also be an overdraft limit and there are specific rules governing when certain operations are allowed.
Representing immutable state
Not all state of an entity is meant to be mutable. Some attributes are set when an instance is created and do not change over its lifetime. For a bank account, the unique identifier that each account is assigned when it is created would be such an attribute.
As a first step in modeling a bank account, therefore, we create a value type that will allow each account to be uniquely identified. Note that we have not called this type ‘account number’. The account number that we commonly associate with each account is not the only way to identify a bank account, as we will see below. We assume we have already defined a Money type to represent monetary values and perform basic operations on such values.
struct AccountIdentifier {
var accountNumber: String {
return branchCode + base + suffix
}
var iban: String {
var iban = ""
// Logic to build IBAN
return iban
}
let branchCode: String
let base: String
let suffix: String
}
The bank account number that we normally see and use is typically a derived value. It is composed from sub-identifiers. Which identifiers are used, and in what order, may vary from case to case. For the purposes of our example, we have used a branch code, a base number unique to each customer, and a suffix which allows the same customer to hold multiple accounts linked to the same base number.
We have given our AccountIdentifier type a computed property, which concatenates the three sub-identifiers to form an account number that can be used to identify the account in normal use, such as on bank statements. This number would, however, only be unique for accounts within the same bank. Most bank accounts need other identifiers that will be unique in a broader context. For example, in order to clear cheques and process domestic fund transfers, withdrawals from other banks’ ATMs, etc., we may need a reference which will be unique across all banks that participate in the relevant clearing system. Similarly, when sending or receiving cross-border fund transfers, some sort of international identifier is often required. The most common such identifier is the International Bank Account Number (IBAN).
These identifiers can constructed by combining the sub-identifiers with static information, like standard bank and country codes. There are rules governing these combinations, such as how long an identifier must be, and how and where to insert padding. To keep the example simple and focused, we have just added an iban computed property to our AccountIdentifier type, without going into details of how an IBAN is actually constructed.
The point of the foregoing discussion is that real-world domains are usually complex, in ways that we can only begin to appreciate once we start to get an in-depth understanding. Trying to get one class to deal with all the data, logic and business rules related to a real-world entity is likely to make the class unwieldy and a nightmare to maintain. Using domain-specific value types enables us to break down the complexity into manageable chunks, which can be encapsulated behind APIs rich in the language of the domain.
Dealing with mutable state
Entities usually have mutable state. The classes we use to model them are responsible for keeping track of that state through the life cycle of the entity. The state is usually composed of values. Let’s consider the bank account example introduced earlier in this article. Unlike the account identifier, which is fixed for the life of the account, the account balance needs to change as money is deposited into and withdrawn from the account. Before we model this behaviour, let’s take a look at how mutation relates to values and value types.
As the state of a class mutates, the values themselves don’t change. They are just replaced by new ones. When we add a number to another, for instance, we don’t really change either number. We just create a new number equal to the sum of the two numbers. We can assign the sum to a new variable or to one of the variables holding the original numbers. Doing the latter may create the impression that we have modified the number. In fact, we have just discarded the old value in the variable and replaced it with a new one.
1. Swift value types and mutability
On the surface, Swift seems to allow mutation of the state of values by letting us define structs with variable stored properties and mutating methods. If you look under the hood, however, this is just sleight of hand. What Swift actually does when we change the value of a stored property of a struct, either directly or by using a mutating method, is to create a new instance of the struct and assign it to the variable to which the original instance was assigned, discarding the original instance in the process.
Here is a quick test to prove this.
struct TestStruct {
mutating func changeValue(to newValue: Int) {
value = newValue
}
var value: Int
}
var testVar = TestStruct(value: 5) {
didSet {
print("New instance created, with value: \(testVar.value)")
}
}
We define a struct with a single variable stored property, and a mutating method that assigns a new value to the stored property. We then declare a new variable and assign an instance of the struct to the variable. We also give the variable a didSet observer, which will print a message whenever a new value gets assigned to the variable.
Let’s see what happens when we assign a new value to the stored property of the instance, directly as well as by using the mutating method.
testVar.value = 6 // New instance created, with value: 6
testVar.changeValue(to: 7) // New instance created, with value: 7
In both cases, we get a message in the console confirming that, rather than modifying the same instance, Swift has assigned a new instance with the new value to the variable.
To obtain further confirmation, we assign a new instance of the struct to a constant. We then try to modify the stored property in the same ways as above.
let testConst = TestStruct(value: 5)
testConst.value = 6
// Error: Cannot assign to property: 'testConst' is a 'let' constant
testConst.changeValue(to: 7)
// Error: Cannot use mutating member on immutable value: 'testConst' is a 'let' constant
Sure enough, both our attempts to mutate the value of the stored property are met with errors because Swift is unable to assign the new instance it has created to a constant.
This shows that whenever we change the value of a stored property of a value type instance, directly or through a mutating method, it has the same effect as creating a new instance and assigning it to the variable holding the original instance, which gets discarded in the process.
2. Controlling mutability of value types
We have seen that Swift can only mutate a value type instance by assigning a new value to the variable that holds the instance. This means that we can control when a value type instance can be mutated, and when it cannot be, simply by assigning the instance to a variable (declared with the var keyword) or to a constant (declared with the let keyword) respectively. When we assign a value type instance to a constant, we are in effect guaranteeing that the instance can never be mutated, even if it has variable stored properties and mutating methods.
If we take a value type instance assigned to a constant and assign it to a variable, we can mutate the instance assigned to the variable. It is important to note, however, that the instance that gets mutated is a copy of the instance assigned to the constant, which itself remains unchanged. This is because value type instances in Swift automatically get copied on assignment, with no data sharing between an instance and its copy. It is noteworthy that this guarantee of no data sharing is true by definition only for ‘pure’ values, i.e., value type instances all of whose stored properties are also instances of value types. The picture can get a bid muddled if we create value types with stored properties which are instances of reference types, as we will see a bit later.
For now, we demonstrate how pure values guarantee automatic copying on assignment with no data sharing.
struct PureValue {
var value: Int
}
let pureValueConst = PureValue(value: 5)
var pureValueVar = pureValueConst
pureValueVar.value = 10
print(pureValueConst.value) // 5
print(pureValueVar.value) // 10
The struct PureValue has a mutable property of type Int. We assign a new instance of this type to the constant pureValueConst. We then assign the value of the constant to the variable pureValueVar. When we modify the value assigned to the variable, we see that the value assigned to the constant remains unchanged.
An instance of a value type assigned to a constant cannot be changed directly and will remain unaltered regardless of how many times we assign its value to a variable or pass it as an argument to a function, which also creates a copy of the instance and does not affect the original instance. This can give us fine-grained control over how certain parts of the code will behave, making code more predictable and easier to reason about.
We continue with the bank account example introduced earlier in this article and define a value type to model the account balance. As already noted, the account balance is not a single number. There is a ledger balance, which may or may not be the same as the available balance. There could be various reasons for a difference between the two, including a delay between authorization and posting of transactions, presence of a hold on a certain amount of funds, availability of an overdraft limit, etc. For the purposes of this example, we consider just one scenario, an optional overdraft limit.
struct AccountBalance {
init(_ ledger: Money, overdraftLimit: Money? = nil) {
self.ledger = ledger
self.overdraftLimit = overdraftLimit
}
var available: Money {
return ledger + (overdraftLimit ?? Money(0))
}
var ledger: Money
var overdraftLimit: Money?
}
The ledger balance, the actual recorded balance of the account, and the overdraft limit, are stored properties. The latter is an optional since there may or may not be an overdraft allowed on a particular account. The available balance is computed from the stored properties.
Next, we add the following mutating method to the AccountBalance type, which can be used to increase the balance of the account.
mutating func increase(by amount: Money) {
ledger = ledger + amount
}
Before we add a method to decrease the balance, we need to think about what should happen if the amount to be decreased exceeds the available balance of the account. This depends on the domain business rule, which could be to disallow such requests, or to allow an unauthorized overdraft, which could attract a higher rate of interest.
For our example, we assume that the account balance cannot be decreased below the available balance and the following error should occur if such an attempt is made.
enum AccountError: Error {
case insufficientBalance
}
Having agreed the business rule to be used, we add the following method to the AccountBalance type, to decrease the balance of an account, subject to the business rule.
mutating func decrease(by amount: Money) throws {
guard amount <= available else {
throw AccountError.insufficientBalance
}
ledger = ledger - amount
}
Note the throws keyword in the signature of the decrease(by:) method, which makes it a throwing function.
The interplay of value and reference types
We have already seen that we can stop the mutation of a value type instance that has variable stored properties, simply by assigning it to a constant. But this guarantee holds only for pure values, i.e., value type instances all of whose stored properties also hold values. Things may be different if the value type instance holds an instance of a reference type. This is because, unlike a variable holding a value type instance, where the variable and the instance assigned to it are logically unified and the value held by a variable cannot be manipulated independently of the variable, a variable holding a reference type instance is distinct from the instance assigned to it. A variable holding a reference type instance holds only a reference to the actual instance, which is allocated on the heap. There may be one or more other references to the same instance, any of which may be used to mutate the instance.
If a value type instance, which holds a reference type instance, is assigned to a constant, the only guarantee we get is that the reference held by the value type instance will not change, i.e., it will point to the same location in memory. This, however, cannot stop the actual reference type instance at that memory location from being modified. In fact, we can do this ourselves by assigning the value type instance to a variable, declared with the var keyword. Since values get copied on assignment, the new value will have a property holding a copy of the reference to the same memory location. We can use the new value to modify the reference type instance, which is now implicitly shared between the two values, because they both holds references to it.
class ReferenceType {
init(value: Int) {
self.value = value
}
var value: Int
}
struct ValueWithReference {
var reference: ReferenceType
}
let valueWithRefConst = ValueWithReference(reference: ReferenceType(value: 5))
var valueWithRefVar = valueWithRefConst
valueWithRefVar.reference.value = 10
print(valueWithRefConst.reference.value) // 10
print(valueWithRefVar.reference.value) // 10
We define a class called ReferenceType, which holds a value of type Int. We then define a struct called ValueWithReference and give it a property called reference of type ReferenceType.
We assign a new instance of ValueWithReference to a constant. We then assign the value of this constant to a variable and mutate the reference property of the instance assigned to the variable. When we print the Int values held by both the instances, we find that the value assigned to the constant has been modified, although we never touched it. This is because the ReferenceType instance is shared between the two instances of ValueWithReference and modifying it in one place affects the other as well.
One way to control such mutation could be to perform a deep copy each time a reference type instance is copied. But this can potentially have materially negative performance implications, owing to the heap memory allocation and reference counting involved in making copies of reference type instances, in particular if we end up having to create deep copies of reference type instances holding instances of other reference types. Another way to deal with the problem is to make all reference type instances used to represent values immutable, but this would still require a lot of copying, which is not an inherent strength of reference types.
There is a strong case, therefore, to use pure values to model the state of entities. This not only makes code more predictable and less prone to errors, it can also significantly improve performance characteristics. This is because value types are ‘cheaper’ to copy as they do not incur performance penalties due to allocation of memory on the heap and the overhead of reference counting. Pure values also facilitate concurrency since each thread can use its own copy of the data held by value type instances with no shared state.
There is no magic formula for how to mix value and reference types in designing applications, and different situations may demand different strategies. It stands to reason, however, to use value and reference types in ways that play to their strengths and not otherwise.
Given below are some guidelines, which could help manage the interplay of value and reference types:
- Use classes to model entities while using pure values to encapsulate as much of the domain data, logic and business rules as possible.
- Carefully consider how much of the state of a class needs to be mutable.
- For mutable parts of the state of a class that need to be accessible to code outside the class, make the properties only privately settable. This will ensure that, while the mutable values can be accessed by those on the outside, the class remains solely in charge of mutating its state.
- When any values that form the accessible state of a class need to be used outside the class, to the extent possible, assign them to let constants. This way, any code that uses these values can be confident that they were not modified since being copied from an instance of the class.
We now return to our bank account example. Having defined value types to encapsulate the data, logic and business rules related to the relevant aspects of a bank account, we define the following class to model a bank account.
class Account {
init(identifier: AccountIdentifier, balance: AccountBalance) {
self.identifier = identifier
self.balance = balance
}
func credit(_ amount: Money) {
balance.increase(by: amount)
}
func debit(_ amount: Money) throws {
try balance.decrease(by: amount)
}
let identifier: AccountIdentifier
private(set) var balance: AccountBalance
}
The Account class exposes the API to be used to perform operations on the account, leaving the heavy lifting to the value types already defined. This includes enforcing the business rule that an account cannot be debited for an amount exceeding its available balance. Accordingly, the debit(_:) method above simply propagates any errors that may be thrown by the decrease(by:) method of the AccountBalance type.
Note the private(set) modifier to the declaration of the balance property. There is no access level specified for the property, which means it has the default internal access level, enabling it to be used by code in any source file in the same module. However, the private(set) modifier makes the property settable only by code in the enclosing declaration and any extensions of that declaration in the same source file.
This has the effect of permitting the class, including any of its extensions in the same source file, to set the value of the balance property, while providing read-only access to the property to all other code.
Usage considerations
Value type instances in Swift are always copied on assignment. We have already noted how constants declared with the let keyword can be used to ensure that value type instances, even those with variable properties and mutating methods, are not modified after they are copied from a class instance. Care also needs to be taken to ensure that any client code does not rely on outdated information. Ideally, rules should be established regarding when a value obtained from a class should be discarded immediately after use and when it can be stored, and for how long. What makes sense in each scenario will depend on how often the state of the class instance is updated and the recency requirement of the use case of the client code.
For instance, in the case of a bank account:
- Code that needs to report real-time balance information should discard the value immediately after use.
- Code that generates statements as of the close of the previous day can store values obtained during the day and discard them at the end of each day.
- Code that needs access to static information such as account identifiers can refresh its values each time a batch run takes place, which would reflect updates for new accounts opened, accounts closed, etc.
Similar rules can be established on a case by cases basis, depending on the requirements and workflow of the domain.
Benefits
1. Separation of concerns
What we have seen is just a subset of the responsibilities of a class used to model a bank account. Imagine such a class in a real-world system and the amount of logic and number of business rules it would contain. This could lead to a significantly bloated class with a mixed bag of related but separate responsibilities. With all state of the entity modeled using value types, each value type takes responsibility for managing its own data, logic and business rules. The class maintains the identity of the entity, preserves its state, and exposes the API to be used by client code, but leaves all operations and validation to the respective value types.
2. Testability
Code that deals with pure values is inherently more easily testable because of the defining characteristics of values – attribute-based equality, lack of identity or life cycle, and substitutability. There is no need to use mocking frameworks or elaborate setup code to test value types, as often required for testing classes, which maintain state over their life cycles and their behaviour can change with changes in state. For testing a value type instance, all we need to do is create a new value with the expected attributes and perform an equality comparison.
3. Efficiency
It is considerably more efficient in Swift to work with value types compared to reference types, since the latter must be allocated on the heap and involve significant overhead to manage reference counting. Although there is more copying involved when dealing with values, copies of value type instances in Swift are generally ‘cheap’ and many copies can simply be optimized away by the compiler.
4. Concurrency
Since value type instances are always copied on assignment, and on being passed as arguments to a function, with no data sharing between copies, we can safely hand over values to processes running on different threads, without having to worry about synchronization, race conditions, deadlocks, etc. With the state of the domain entities modeled using pure values, any client code that simply needs to query the state of the entity can be handed a value that represents the relevant aspect of the state of the entity. These operations can run in parallel without any conflicts.
Only when an operation that may mutate the state of the class is to be executed do we need direct access to an instance of the class. In practice, such operations are typically executed much less frequently than queries. For a bank account, for instance, the most common operations are checking the balance, generating statements, generating IBANs, etc. All such operations can safely proceed concurrently, which could lead to significant gains in efficiency and response times.
Conclusion
We could construct our programs only using the value types provided by the Standard Library and simple general-purpose value types of our own, letting classes directly manage the detailed logic and myriad business rules related to all their responsibilities. But this would mean not using the real power of Swift value types in managing complexity and making code simpler and more efficient. We can keep classes lean and focused by creating domain-specific values types that encapsulate domain data, logic and business rules.
Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on Twitter.
Leave a Reply