Swift multiple trackers architecture with SOLID – Part 2

In part 1, we developed code to implement an infrastructure that enables multiple trackers. In part 2, we will refactor our code based on SOLID principles to improve its design.

SOLID principles consist of five guidelines that help developers create code that is easy to maintain, extend and test. These principles include:

  • Single Responsibility Principle (SRP): Our code follows SRP as the TrackerStore class is responsible for storing and registering trackers, while the Tracker class is responsible for implementing tracking functions.
  • Open/Closed Principle (OCP): Our code follows OCP by allowing the system to be extended without modifying existing code, thanks to the TrackerConfigurable protocol that enables/disables trackers through configuration.
  • Liskov Substitution Principle (LSP): Our code follows LSP as the AdobeTracker, BrazeTracker, and SumoTracker classes implement their respective protocols without violating the behavior expected by their supertypes.
  • Interface Segregation Principle (ISP): Our code follows ISP by using separate protocols for each set of related functions (PlaybackTrackable, NavigationTrackable, OptionalNavigationTrackable) instead of one large protocol.
  • Dependency Inversion Principle (DIP): Our code follows DIP as the Tracker class depends on abstractions (protocols) rather than concrete types, allowing for easier testing and swapping of implementations.
enum TrackerType {
    case braze, adobe, sumo
}

extension TrackerType {
    var isEnabled: Bool {
        return TrackerConfig.enabledTrackers.contains(self)
    }
}

// DIP
// Client depends on abstractions
protocol Trackable {
    var type: TrackerType { get }
}

// OCP
// Enable/Disable trackers through configuration
protocol TrackerConfigurable {
    static var enabledTrackers: [TrackerType] { get }
}

struct TrackerConfig: TrackerConfigurable {
    static let enabledTrackers = [TrackerType.braze, TrackerType.adobe]
}


// SRP
// Tracker class is responsible for storing and registering trackers
final class TrackerStore {
    private(set) var trackers = [Trackable]()

    func registerTracker(tracker: Trackable) {
        if !trackers.contains(where: { $0.type == tracker.type }) {
            trackers.append(tracker)
        } else {
            print("πŸ”₯πŸ”₯πŸ”₯ We don't want duplicates πŸ”₯πŸ”₯πŸ”₯")
        }
    }
}

// Tracker class is responsible for implementing tracking functions
class Tracker {
    private let trackerStore: TrackerStore

    init(trackerStore: TrackerStore) {
        self.trackerStore = trackerStore
    }
}

extension Tracker: PlaybackTrackable {
    private var playbackTrackers: [PlaybackTrackable] {
        trackerStore.trackers
            .filter { $0.type.isEnabled }
            .compactMap { $0 as? PlaybackTrackable }
    }

    func trackPlay() {
        playbackTrackers
            .forEach { $0.trackPlay() }
    }

    func trackPause() {
        playbackTrackers
            .forEach { $0.trackPause() }
    }

    func trackStop() {
        playbackTrackers
            .forEach { $0.trackStop() }
    }

    func trackNext() {
        playbackTrackers
            .forEach { $0.trackNext() }
    }

    func trackPrevious() {
        playbackTrackers
            .forEach { $0.trackPrevious() }
    }
}

extension Tracker: NavigationTrackable {
    private var navigationTrackers: [NavigationTrackable] {
        return trackerStore.trackers
            .filter { $0.type.isEnabled }
            .compactMap { $0 as? NavigationTrackable }
    }

    func trackNavigate(toView: String, fromView: String) {
        navigationTrackers
            .forEach { $0.trackNavigate(toView: toView, fromView: fromView) }
    }

    func trackBackTap(fromView: String) {
        navigationTrackers
            .forEach { $0.trackBackTap(fromView: fromView) }
    }

    func trackSettingsOpen(fromView: String) {
        navigationTrackers
            .forEach { $0.trackSettingsOpen(fromView: fromView) }
    }
}

extension Tracker: OptionalNavigationTrackable {
    private var optionalNavigationTrackers: [OptionalNavigationTrackable] {
        return trackerStore.trackers
            .filter { $0.type.isEnabled }
            .compactMap { $0 as? OptionalNavigationTrackable }
    }

    func optionalTrackTap() {
        optionalNavigationTrackers
            .forEach { $0.optionalTrackTap() }
    }
}

protocol PlaybackTrackable {
    func trackPlay()
    func trackPause()
    func trackStop()
    func trackNext()
    func trackPrevious()
}

// ISP
// Separate protocols for each function
protocol NavigationTrackable {
    func trackNavigate(toView: String, fromView: String)
    func trackBackTap(fromView: String)
    func trackSettingsOpen(fromView: String)
}

// LSP
// Separate protocol for optional function
protocol OptionalNavigationTrackable {
    func optionalTrackTap()
}

class AdobeTracker: Trackable {
    let type = TrackerType.adobe
}

extension AdobeTracker: NavigationTrackable {
    func trackNavigate(toView: String, fromView: String) {
        // Specific implementation
    }

    func trackBackTap(fromView: String) {
        // Specific implementation
    }

    func trackSettingsOpen(fromView: String) {
        // Specific implementation
    }
}

extension AdobeTracker: OptionalNavigationTrackable {
    func optionalTrackTap() {
        // Specific implementation
    }
}

class BrazeTracker: Trackable {
    let type = TrackerType.braze
}

extension BrazeTracker: NavigationTrackable {
    func trackNavigate(toView: String, fromView: String) {
        // Specific implementation
    }

    func trackBackTap(fromView: String) {
        // Specific implementation
    }

    func trackSettingsOpen(fromView: String) {
        // Specific implementation
    }
}

class SumoTracker: Trackable {
    let type = TrackerType.sumo
}

extension SumoTracker: OptionalNavigationTrackable {
    func optionalTrackTap() {
        // Specific implementation
    }
}

By following SOLID principles, we have created a well-structured code that is maintainable, extensible, and testable.

Leave a Comment