Gustavo Guedes
Posted on September 20, 2024
A little context
Recently, I needed to use the Equatable
protocol in some entities within my application, and I encountered an interesting aspect of it: the requirement to manually compare the properties of a class
. So I thought: there must be a better way.
What does Equatable offer us?
Without diving too deep into its implementation, Equatable
forces us to create a method to compare objects.
Imagine the following class:
class PersonEntity {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
If we try to compare two instances of PersonEntity
, we will get an error like this:
Binary operator '==' cannot be applied to two 'PersonEntity' operands
By implementing the Equatable protocol, we can finally compare instances using the ==
operator:
class PersonEntity: Equatable {
static func == (lhs: PersonEntity, rhs: PersonEntity) -> Bool {
return lhs.name == rhs.name && rhs.age == rhs.age
}
Now the comparison is possible. But imagine a class with many properties, including other classes. Manually validating each property isn’t necessary in languages like Dart
.
Inspiration
In Dart/Flutter, we have the Equatable package, which automates comparisons.
The code that handles this works as follows:
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is Equatable &&
runtimeType == other.runtimeType &&
equals(props, other.props);
}
To summarize:
- First, it checks if instances "A" and "B" are identical using the identical method (similar to
AnyObject
inSwift
); - If they are not identical, it checks if the other instance (or
rhs
in Swift/Equatable) is of typeEquatable
; - Then, it compares the types of the instances using
type(of:)
; - Finally, it compares the properties of both instances.
But what are these properties? They’re what we will create on our side to simplify comparisons.
Let’s get to work
We will create an array of properties, where we will store all the props used for comparison. This eliminates the need for manual comparisons.
protocol CustomEquatable: Equatable {
var props: [Any?] { get }
}
I decided to extend Equatable to follow the language standard. Here's the implementation:
extension CustomEquatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.isEqual(to: rhs)
}
func isEqual(to other: any CustomEquatable) -> Bool {
return self.props.elementsEqual(other.props, by: { lhsElement, rhsElement in
switch (lhsElement, rhsElement) {
case let (lhsElement as any CustomEquatable, rhsElement as any CustomEquatable):
return lhsElement.isEqual(to: rhsElement)
case let (lhsElement as AnyHashable, rhsElement as AnyHashable):
return lhsElement == rhsElement
default:
return lhsElement == nil && rhsElement == nil
}
})
}
}
In summary:
- We implement the Equatable protocol;
- We check each element in the props array;
- If the element is of type CustomEquatable, we use recursion to validate complex objects;
- For simple types, we compare using
AnyHashable
; - And, of course, we handle
null
values.
Pretty cool, right? But does it work? Let’s test it!
Unit tests
Here are the classes we will use for testing:
import Foundation
class PersonEntity: CustomEquatable {
var props: [Any?] {
return [name, age, preferences, nickname, luckNumbers]
}
let name: String
let age: Int
let preferences: PersonPreferencesEntity?
let nickname: String?
let luckNumbers: [Int]
init(name: String, age: Int, preferences: PersonPreferencesEntity? = nil, nickname: String? = nil, luckNumbers: [Int]? = nil) {
self.name = name
self.age = age
self.preferences = preferences
self.nickname = nickname
self.luckNumbers = luckNumbers ?? []
}
}
class PersonPreferencesEntity: CustomEquatable {
var props: [Any?] {
return [darkmode]
}
let darkmode: Bool
init(darkmode: Bool = false) {
self.darkmode = darkmode
}
}
The first validation is to check if the object we are comparing implements CustomEquatable
. To do this, we instantiate PersonPreferencesEntity
inside PersonEntity
.
func testCompareCustomEquatableProps() {
let equalPreferencesOne = PersonPreferencesEntity()
let equalPreferencesTwo = PersonPreferencesEntity()
let personOne = PersonEntity(name: "John Doe", age: 25, preferences: equalPreferencesOne)
let personTwo = PersonEntity(name: "John Doe", age: 25, preferences: equalPreferencesTwo)
XCTAssertTrue(personOne == personTwo)
let diffPreferencesOne = PersonPreferencesEntity(darkmode: false)
let diffPreferencesTwo = PersonPreferencesEntity(darkmode: true)
let diffPersonOne = PersonEntity(name: "John Doe", age: 25, preferences: diffPreferencesOne)
let diffPersonTwo = PersonEntity(name: "John Doe", age: 25, preferences: diffPreferencesTwo)
XCTAssertFalse(diffPersonOne == diffPersonTwo)
}
With this test, we ensure that complex properties are compared correctly.
The second test validates values that do not implement CustomEquatable
.
func testCompareCommonProps() {
let personOne = PersonEntity(name: "John Doe", age: 25, luckNumbers: [1, 2, 3])
let personTwo = PersonEntity(name: "John Doe", age: 25, luckNumbers: [1, 2, 3])
XCTAssertTrue(personOne == personTwo)
let diffPersonOne = PersonEntity(name: "John Doe", age: 25, luckNumbers: [1, 2, 3])
let diffPersonTwo = PersonEntity(name: "John Doe", age: 25, luckNumbers: [4, 5, 6])
XCTAssertFalse(diffPersonOne == diffPersonTwo)
}
Now, for nullable values:
func testCompareNullablesProps() {
let diffPersonOne = PersonEntity(name: "John Doe", age: 25, nickname: "john")
let diffPersonTwo = PersonEntity(name: "John Doe", age: 25)
XCTAssertFalse(diffPersonOne == diffPersonTwo)
}
Finally, a test to ensure that we are not comparing the object to itself.
func testCompareInstances() {
let diffPersonOne = PersonEntity(name: "John Doe", age: 25)
let diffPersonTwo = PersonEntity(name: "John Doe", age: 25)
XCTAssertFalse(diffPersonOne === diffPersonTwo)
}
With this last test, we validate that our protocol works as expected.
Conclusion
With this solution, we avoid having to manually create a chain of comparisons inside the ==
method. Since the original Equatable
implementation required comparing each property manually, we maintain a time complexity of O(n).
Important: whenever you add a new property to your class, don’t forget to update the props array.
That’s it! I hope this was helpful. See you next time!
Posted on September 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.