Skip to content

Demo app to showcase how to apply SOLID principles and compose functionality based on generic interfaces

Notifications You must be signed in to change notification settings

ibru/composition-demo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Demo app showcasing SOLID principles

In this demo app I explore how SOLID principles can solve an issue of multiple teams working on same feature possibly creating merge conflict.

You can also read Medium article for deeper explanation.

The demo app exhibits a RegistrationViewController, which is supposed to perform a user registration action. On call ofregisterButtonTouched() IBAction it performs local email validation and sends API request to register new user.

Common implementation approach, not using SOLID

The typical implementation of the registration button logic can be seen in branch no-solid in func RegistrationViewController.registerButtonTouched() It makes the RegistrationViewController be directly dependent on EmailValidator and RegistrationAPI classes, which are stored in different modules.

RegistrationVC no solid

Then I showcase 4 different pull request, which are implementing 4 different codebase improvements.

  1. Refactoring of RegistrationViewController

PR registration VC refactor

  1. Enhance local email validation

PR email validation

  1. Migrate RegistrationAPI public interface to use async/await

PR async await

  1. Adding new functionality of checking for email domain

PR whitelisted domains

These improvements, even though they essentially happen inside different modules, all end up modifying same place - the method RegistrationViewController.registerButtonTouched(). And creating merge conflicts.

More over these improvements result in making LoginModule being dependent on even more external modules (adding dependency to Security) module which might lead to spaghetti dependencies, slow build times, harder reasoning about code.

Improved implementation following SOLID

Then, I refactor the RegistrationViewController logic to follow SOLID principles.

It seems like RegistrationViewController is getting too many responsibilities by directly depending on EmailValidator, RegistrationAPI, DomainManager, etc. It's definitely not a scalable solution for the future.

I solve it by expressing the act of registration by using protocol RegistrationService. So this is the only dependency the VC knows about. And does not care who and how exactly is it implemented.

public enum RegistrationError: Error {
    case invalidEmail
    case serverError(Error)
}

public protocol RegistrationService {
    func registerUser(withEmail emailAddress: String, completion: @escaping (Result<Void, RegistrationError>) -> Void)
}

Then I update RegistrationViewController accordingly

open class RegistrationViewController: UIViewController {
    
    private let registrationService: RegistrationService
    
    public init(registrationService: RegistrationService) {
        self.registrationService = registrationService
        super.init(nibName: nil, bundle: nil)
    }
    
    @IBAction func registerButtonTouched() {
        guard let email = emailTextField.text else { return }
        
        registrationService.registerUser(withEmail: email) { [weak self] result in
            switch result {
            case .success:
                self?.showRegistrationSuccess()
                
            case let .failure(error):
                switch error {
                case .invalidEmail:
                    self?.showInvalidEmail()
                
                case let .serverError(error):
                    self?.showError(error)
                }
            }
        }
    }
    ...
}

My registration logic requires that multiple components (EmailValidator, RegistrationAPI, DomainManager) work together to perform full functionality. But now I have only one protocol that represents all the logic. I can solve it by separating the logic into small pieces, per component, and compose it together as I need.

Email validator conforms to RegistrationService separately:

extension EmailValidator: RegistrationService {
    public func registerUser(withEmail emailAddress: String, completion: @escaping (Result<Void, RegistrationError>) -> Void) {
        guard isEmailValid(emailAddress, for: [.regex]) else {
            completion(.failure(.invalidEmail))
            return
        }
        completion(.success(()))
    }
}

API Service conforms to RegistrationService separately:

extension RegistrationAPI: RegistrationService {
    public func registerUser(withEmail emailAddress: String, completion: @escaping (Result<Void, RegistrationError>) -> Void) {
        Task { [weak self] in
            do {
                try await self?.registerEmail(emailAddress)
                completion(.success(()))
            } catch {
                completion(.failure(.serverError(error)))
            }
        }
    }
}

Now, to put it all together, I create helper types that peform composition of my needs. I create types AppendingRegistrationService and FallbackRegistrationService and compose the fuctionality inside my Factory type. By using these helper types, I was able to make multiple pieces of functionality work together, while still be able to maintain them in isolation.

final class RegistrationFactory {
    func makeViewController(emailValidator: EmailValidator, registrationAPI: RegistrationAPI) -> UIViewController {
        RegistrationViewController(
            registrationService: emailValidator
                .fallback(service: WhitelistedDomainsRegistrationAdapter())
                .appending(service: registrationAPI)
        )
    }
}

Then I redo the same PRs once again.

  1. Refactoring of RegistrationViewController

PR registration VC refactor

  1. Enhance local email validation

PR email validation

  1. Migrate RegistrationAPI public interface to use async/await

PR async await

  1. Adding new functionality of checking for email domain

PR whitelisted domains

Now the PRs, doing same changes as before, end up modifying different places in the codebase and do not create any merge conflicts. More over they tend to get simpler and more isolated, resulting in greater control over source code changes, predictability of development and less chances of unintentionally breaking unrelated part of the app.

SOLID principles applied in the codebase

S-ingle-responsibility principle

"There should never be more than one reason for a class to change."

  • the RegistrationViewController depending only on RegistrationService
  • Registration adapters adapting interfaces one one class into interfaces needed for other class

O-pen–closed principle

"Software entities ... should be open for extension, but closed for modification."

  • modifying existing/adding new functionality by appending new service inside RegistrationFactory

L-iskov substitution principle

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

  • Using AppendingRegistrationService, FallbackRegistrationService and composing multiple RegistrationService implementation into a place where RegistrationService instance is expected

I-nterface segregation principle

"Many client-specific interfaces are better than one general-purpose interface."

  • not being used in this example

D-ependency inversion principle

"Depend upon abstractions, not concretions".

  • making RegistrationViewController depend on abstraction - the protocol RegistrationService, instead of concrete implementations of EmailValidator, RegistrationAPI etc.

About

Demo app to showcase how to apply SOLID principles and compose functionality based on generic interfaces

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages