Skip to content

Simplify API to be short in code #96

@levibostian

Description

@levibostian

2 moments happened recently that gave me inspiration.

  1. I was refactoring my iOSBlanky project. During this refactor, I realized that a better design pattern when using Teller is to have your DataSource not contain any of the actual database/networking logic inside of it. The DataSource should just delegate to a true repository class.

Here is what a DataSource should look like with this delegation:

import Foundation
import RxSwift
import Teller

typealias ReposTellerRepository = TellerRepository<ReposDataSource>
// sourcery: InjectRegister = "ReposTellerRepository"
// sourcery: InjectCustom
extension ReposTellerRepository {}

extension DI {
    var reposTellerRepository: ReposTellerRepository {
        TellerRepository(dataSource: inject(.reposDataSource))
    }
}

class ReposDataSourceRequirements: RepositoryRequirements {
    let githubUsername: String
    var tag: RepositoryRequirements.Tag {
        "Repos for GitHub username: \(githubUsername)"
    }

    init(githubUsername: String) {
        self.githubUsername = githubUsername
    }
}

enum ResposApiError: Error, LocalizedError {
    case usernameDoesNotExist(username: String)

    var errorDescription: String? {
        switch self {
        case .usernameDoesNotExist(let username):
            return "The GitHub username \(username) does not exist." // comment: "GitHub returned 404 which means user does not exist.")
        }
    }
}

// sourcery: InjectRegister = "ReposDataSource"
class ReposDataSource: RepositoryDataSource {
    typealias Cache = [Repo]
    typealias Requirements = ReposDataSourceRequirements
    typealias FetchResult = [Repo]
    typealias FetchError = HttpRequestError

    fileprivate let reposRepository: ReposRepository

    init(reposRepository: ReposRepository) {
        self.reposRepository = reposRepository
    }

    var maxAgeOfCache: Period = Period(unit: 3, component: .day)

    func fetchFreshCache(requirements: ReposDataSourceRequirements) -> Single<FetchResponse<[Repo], FetchError>> {
        reposRepository.getUserRepos(username: requirements.githubUsername)
    }

    func saveCache(_ fetchedData: [Repo], requirements: ReposDataSourceRequirements) throws {
        try reposRepository.replaceRepos(fetchedData, forUsername: requirements.githubUsername)
    }

    func observeCache(requirements: ReposDataSourceRequirements) -> Observable<[Repo]> {
        reposRepository.observeRepos(forUsername: requirements.githubUsername)
    }

    func isCacheEmpty(_ cache: [Repo], requirements: ReposDataSourceRequirements) -> Bool {
        cache.isEmpty
    }
}

When looking at this code, I see lots of boilerplate. The true logic is really just 5 lines of code - all of the code that delegates to the repository. All of the rest is just boilerplate.

Not only is it boilerplate, but introducing a TellerRepository has always caused confusion because I now have to work with ReposRepository and ReposTellerRepository.

  1. I was looking at this very similar project recently. It's trying to solve the same problem as teller is.

When you look at the readme, they have taken advantage of this removing of boilerplate idea.

StoreBuilder
    .from(
        fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
        sourceOfTruth = SourceOfTruth.of(
            reader = db.postDao()::loadPosts,
            writer = db.postDao()::insertPosts,
            delete = db.postDao()::clearFeed,
            deleteAll = db.postDao()::clearAllFeeds
        )
    ).build()

Now, note that this code above is Kotlin but you get the idea. It's building a repository by just delegation functions.


I was thinking about doing this idea recently but I decided not to because I thought to myself, "The current DataSource and Repository API design right now is very unit testable in the dev's app code". Well, check out my latest test I made for a DataSource:

import Foundation
import RxSwift
import XCTest

class ReposDataSourceTests: UnitTest {
    var dataSource: ReposDataSource!

    var reposRepositoryMock: ReposRepositoryMock!

    var disposeBag: DisposeBag!

    let defaultRequirements = ReposDataSource.Requirements(githubUsername: "username")

    override func setUp() {
        super.setUp()

        reposRepositoryMock = ReposRepositoryMock()

        disposeBag = DisposeBag()

        dataSource = ReposDataSource(reposRepository: reposRepositoryMock)
    }

    // At this time, there is little to no logic in the datasource. It's just a wrapper around the repository. Therefore, we are not testing it.
}

Yup. DataSource tests are worthless. That means that the DataSource being it's very own class is also worthless.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions