Ⅰ.Introduction

In my recent iOS project ChatApp , I refactored the account creation functionality. The original code was overly long and lacked modularity, making it difficult to maintain and extend.

By refactoring the code, I improved its readability, maintainability, and reduced potential memory leak risks. In this blog, I will share a comparison of the code before and after the refactor, as well as the advantages of the refactor and the lessons learned.

Here is the whole code before the refactor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import UIKit
import FirebaseAuth
import FirebaseDatabase

class CreateAccountViewController: UIViewController {

// ...view...

@IBAction func createAccountButtonTapped(_ sender: Any) {


showLoadingView()

Database.database().reference().child("usernames").child(username).observeSingleEvent(of: .value) { snapshot in
guard !snapshot.exists() else {
self.presentErrorAlert(title : "Usename In Use",
message: "Please enter a different name to continue.")
self.removeLoadingView()
return
}

Auth.auth().createUser(withEmail: email, password: password) { result, error in
self.removeLoadingView()
if let error = error {
print(error.localizedDescription)
var errorMessage = "Something went wrong. Please try again later."

let errorCode = (error as NSError).code

if let authError = AuthErrorCode(rawValue: errorCode) {
// 可以 switch 来区分错误类型了
switch authError {
case .emailAlreadyInUse:
errorMessage = "Email already in use."
case .invalidEmail:
errorMessage = "Invalid email."
case .weakPassword:
errorMessage = "Weak password. Please use at least 6 characters."
default:
break // 使用默认的 errorMessage
}
}


self.presentErrorAlert(title : "Create Account Failed",
message: errorMessage )
return
}


guard let result = result else {
self.presentErrorAlert(title : "Create Account Failed",
message: "Something went wrong. Please try again later.")
return
}


let userId = result.user.uid
let userData: [String: Any] = [
"id": userId,
"username": username
]

Database.database().reference()
.child("users")
.child(userId)
.setValue(userData)

Database.database().reference()
.child("usernames")
.child(username)
.setValue(userData)

}

}
}

// ...function...
}

Ⅱ.Project Background & Problem

In the original code, the createAccountButtonTapped function handles multiple tasks, which makes the code overly long. User input validation, database operations, and UI updates are all performed within the same function, reducing the code’s readability and maintainability.

Additionally, the repeated error handling leads to code duplication.

Ⅲ.Refactor

The purpose of the refactor is to simplify the code structure and improve its maintainability and scalability.

Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) is one of the five SOLID principles of object-oriented design, introduced by Robert C. Martin. It states that:

“A class should have only one reason to change.”

In other words, a class (or function) should only have one job or responsibility.

If a class or function is responsible for more than one task, it becomes harder to maintain and modify, because changes in one area could affect the other areas, leading to bugs or unintended side effects.

Key Points of SRP:

  1. Separation of Concerns:
    SRP encourages separating different concerns or functionalities into distinct classes or methods. For example, if you have a class that handles both user authentication and database operations, it would violate SRP because it has more than one reason to change.
  2. Maintainability:
    When a class has a single responsibility, it’s easier to understand, test, and modify. If a change is needed in one area, only that class or function will be affected, making the process smoother and less risky.
  3. Extensibility:
    With SRP, extending functionality is simpler. If a class is focused on a single responsibility, you can easily add new features without impacting existing code.
  4. Example:
    If you have a UserManager class that handles both the registration of new users and email notifications, it should be refactored. The user registration logic could go into one class, and the email notification logic into another, as they represent different concerns.

Why SRP is Important:

  • Simplifies Code: Code is easier to understand when each class or function has a single responsibility.
  • Reduces Complexity: By isolating concerns, SRP reduces the complexity of code, making it easier to maintain and test.
  • Improves Flexibility: Since classes have a single responsibility, they are less likely to break when changes are made, improving the flexibility of your codebase.

By adhering to SRP, you ensure that your code is modular, maintainable, and easier to extend in the long term.

By applying the Single Responsibility Principle (SRP), we can break down the large function triggered by the button press into three main responsibilities.

  1. First, we check if the username is already registered, i.e., if the name already exists.
  2. Second, if the username is available, we proceed to send a request to the server to register the new user and write the data.

Here we go!

checkIfExists()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func checkIfExists(username: String, completion: @escaping (_ result: Bool) -> Void ) {

Database.database().reference().child("usernames").child(username)
.observeSingleEvent(of: .value) { snapshot in

guard !snapshot.exists() else {
completion(true)
return
}

completion(false)
}

}

This function, checkIfExists, checks whether a username already exists in the database. Its sole responsibility is to perform this task, which aligns with the Single Responsibility Principle. The expected outcome of this function is simply to tell us whether the username exists or not.

Now, regarding the concept of escaping closures, if you’re not familiar with it, I recommend checking out my articleChatApp . In simple terms, an escaping closure means that the closure’s parameter (in this case, the completion handler) might be used later, outside the scope of the current function. The parameter could be a string, a number, or even another function, indicating that this function might use it later.

In this case, the completion handler returns a Bool, which tells us whether the username exists or not. By using this escaping closure, we simplify the function and enhance its readability, allowing us to delegate the responsibility of checking the username’s existence to this isolated function. This improves the clarity and maintainability of our code.

This function is dedicated solely to checking whether a username exists, and by using an escaping closure, we make the process more modular and the code cleaner.

createUser()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Function to create a user using Firebase Authentication
func createUser(username: String, email: String, password: String, completion: @escaping (_ result: AuthDataResult?, _ error: String?) -> Void) {

// Attempt to create a user with email and password using Firebase
Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
guard let strongSelf = self else { return }

strongSelf.removeLoadingView() // Hide loading view after the operation

// Handle errors from Firebase authentication
if let error = error {
var errorMessage = "Something went wrong. Please try again later."
let errorCode = (error as NSError).code

if let authError = AuthErrorCode(rawValue: errorCode) {
switch authError {
case .emailAlreadyInUse:
errorMessage = "Email already in use."
case .invalidEmail:
errorMessage = "Invalid email."
case .weakPassword:
errorMessage = "Weak password. Please use at least 6 characters."
default:
break
}
}
completion(nil, errorMessage)
return
}

// Return the result if user creation is successful
guard let result = result else {
completion(nil, "Something went wrong. Please try again later.")
return
}

completion(result, nil) // Pass the result to the completion handler
}
}
1
func createUser(username: String, email: String, password: String, completion: @escaping (_ result: AuthDataResult?, _ error: String?) -> Void) { ...code... }

This function, responsible for creating the user, has parameters that include both a result and an error.
This indicates that the function’s purpose is to return two pieces of information: the result and the error message.

1
Auth.auth().createUser(withEmail: email, password: password) {[weak self] result, error in ...code...}

Essentially, this function is used for user creation. After uploading the data to the server, if the user is successfully created, the function returns the result.
However, if an error occurs, instead of returning the result, it returns the error message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

[weak self] result, error in
guard let strongSelf = self else { return }

strongSelf.removeLoadingView() // Hide loading view after the operation


// Handle errors from Firebase authentication
if let error = error {
var errorMessage = "Something went wrong. Please try again later."
let errorCode = (error as NSError).code

if let authError = AuthErrorCode(rawValue: errorCode) {
switch authError {
case .emailAlreadyInUse:
errorMessage = "Email already in use."
case .invalidEmail:
errorMessage = "Invalid email."
case .weakPassword:
errorMessage = "Weak password. Please use at least 6 characters."
default:
break
}
}
completion(nil, errorMessage)
return
}

// Return the result if user creation is successful
guard let result = result else {
completion(nil, "Something went wrong. Please try again later.")
return
}

completion(result, nil) // Pass the result to the completion handler
}

The function’s main role is to interact with the server, upload the user information, and provide feedback in the form of either a success result or an error message.

If you’re a beginner developer, you might have some confusion about weak self and the concepts above. Don’t worry!

The purpose of using [weak self] is to avoid retain cycles, which can lead to memory leaks. It ensures that self (the view controller) is not strongly captured by the closure, allowing it to be deallocated if needed.

The guard let strongSelf = self else { return } statement is used to safely unwrap self inside the closure. If self has been deallocated, the closure will return early, preventing any crashes.

For a more detailed explanation of these concepts, check out my other article.

So, the purpose of this function is simply to determine the result and error based on the upload status and uploading.

createAccountButtonTapped

We have already defined the responsibilities of the two functions: one checks if the username exists, and the other checks if the user creation was successful. Now, we need to connect these two functions.
For example, when the button is pressed, they should start working together.

First, we need to check if the username already exists. This is done by calling the checkIfExists function.

1
2
3
4
5
6
7
8
checkIfExists(username: username) { [weak self] usernameExists in
guard let strongSelf = self else { return }
if !usernameExists { ...code... } else {
strongSelf.presentErrorAlert(title : "Usename In Use",
message: "Please enter a different name to continue.")
strongSelf.removeLoadingView()
}

Inside this function, if the username does not exist, we proceed to create the user.
If the user is successfully created, we can move forward with the next steps.

However, if there is an error during user creation, we handle it by displaying an error message. If there is no error, the user’s data will be uploaded successfully.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
strongSelf.createUser(username: username, email: email, password: password) { result, error in

if let error = error {

DispatchQueue.main.async {
strongSelf.presentErrorAlert(title: "Create Account Failed", message: error)
}

return
}

guard let result = result else {
strongSelf.presentErrorAlert(title: "Create Account Failed", message: "Please try again later.")
return
}
}

Upload Firebase Realtime Database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Firebase Realtime Database
let userId = result.user.uid
let userData: [String: Any] = [
"id": userId,
"username": username
]

Database.database().reference()
.child("users")
.child(userId)
.setValue(userData)

Database.database().reference()
.child("usernames")
.child(username)
.setValue(userData)

So, this part of the code is written like this in the end:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

@IBAction func createAccountButtonTapped(_ sender: Any) {

checkIfExists(username: username) { [weak self] usernameExists in
guard let strongSelf = self else { return }
if !usernameExists {
strongSelf.createUser(username: username, email: email, password: password) { result, error in

if let error = error {

DispatchQueue.main.async {
strongSelf.presentErrorAlert(title: "Create Account Failed", message: error)
}

return
}

guard let result = result else {
strongSelf.presentErrorAlert(title: "Create Account Failed", message: "Please try again later.")
return
}
// Firebase Realtime Database
let userId = result.user.uid
let userData: [String: Any] = [
"id": userId,
"username": username
]

Database.database().reference()
.child("users")
.child(userId)
.setValue(userData)

Database.database().reference()
.child("usernames")
.child(username)
.setValue(userData)
}
} else {
strongSelf.presentErrorAlert(title : "Usename In Use",
message: "Please enter a different name to continue.")
strongSelf.removeLoadingView()
}
}

}
}

Ⅳ.Summary

After the refactor, our code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@IBAction func createAccountButtonTapped(_ sender: Any) {
showLoadingView()
checkIfExists(username: username) { [weak self] usernameExists in
guard let strongSelf = self else { return }
if !usernameExists {
strongSelf.createUser(username: username, email: email, password: password) { result, error in

if let error = error {

DispatchQueue.main.async {
strongSelf.presentErrorAlert(title: "Create Account Failed", message: error)
}

return
}

guard let result = result else {
strongSelf.presentErrorAlert(title: "Create Account Failed", message: "Please try again later.")
return
}

let userId = result.user.uid
let userData: [String: Any] = [
"id": userId,
"username": username
]

Database.database().reference()
.child("users")
.child(userId)
.setValue(userData)

Database.database().reference()
.child("usernames")
.child(username)
.setValue(userData)
}
} else {
strongSelf.presentErrorAlert(title : "Usename In Use",
message: "Please enter a different name to continue.")
strongSelf.removeLoadingView()
}
}

}


func checkIfExists(username: String, completion: @escaping (_ result: Bool) -> Void ) {

Database.database().reference().child("usernames").child(username)
.observeSingleEvent(of: .value) { snapshot in

guard !snapshot.exists() else {
completion(true)
return
}

completion(false)
}

}

func createUser(username: String, email: String, password: String, completion: @escaping (_ result: AuthDataResult?, _ error: String?) -> Void) {

Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
guard let strongSelf = self else { return }
strongSelf.removeLoadingView()
if let error = error {
print(error.localizedDescription)
var errorMessage = "Something went wrong. Please try again later."

let errorCode = (error as NSError).code

if let authError = AuthErrorCode(rawValue: errorCode) {

switch authError {
case .emailAlreadyInUse:
errorMessage = "Email already in use."
case .invalidEmail:
errorMessage = "Invalid email."
case .weakPassword:
errorMessage = "Weak password. Please use at least 6 characters."
default:
break
}
}
completion(nil, errorMessage)
return
}
guard let result = result else {
completion(nil, "Something went wrong. Please try again later.")
return
}
completion(result, nil)
}
}

The refactor improves the code by making it more modular and maintainable.
Each function now has a single responsibility, making the code easier to understand and extend.
It also reduces redundant code, improves error handling, and ensures better memory management, leading to more efficient and robust user account creation.

Ⅴ.Postscript

When I was learning about modularization and refactoring functions, I honestly felt very anxious because the new approach seemed more complicated than the old one. I was overwhelmed at first, and I’m sure if you were in my shoes, you’d feel anxious too. Of course, if you’re already an experienced professional, you might think this is simple and wonder why I’m struggling with it. But when I asked several AIs for answers, they all gave me similar advice, which made me feel empowered.

AI:

Don’t Worry About Complexity – It’s Part of the Learning Process

It’s completely normal to feel like the refactored code is more complex, especially when you’re first learning about modularization and design principles like the Single Responsibility Principle (SRP). While breaking down a large function into smaller ones makes the code seem more spread out, it actually improves maintainability, extensibility, and readability in the long term.

The perceived complexity comes from separating concerns and organizing code in a more modular way. This makes it easier to test, debug, and extend, even if it feels like there’s more to understand at first.

Remember, it’s not about writing less complex code immediately, but about building better, more flexible structures that can handle future changes more easily.

As you continue learning and practicing, this kind of modular thinking will become second nature. Start small, refactor when necessary, and trust that this approach will lead to cleaner, more manageable code in the long run.

If you’re also a beginner, I want to share this with you: it’s okay to feel confused at first, and this is part of the learning process.