Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify API to be short in code #96

Open
levibostian opened this issue Jun 19, 2020 · 0 comments
Open

Simplify API to be short in code #96

levibostian opened this issue Jun 19, 2020 · 0 comments

Comments

@levibostian
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant