2 moments happened recently that gave me inspiration.
- 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.
- 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.
2 moments happened recently that gave me inspiration.
DataSourcenot contain any of the actual database/networking logic inside of it. TheDataSourceshould just delegate to a true repository class.Here is what a
DataSourceshould look like with this delegation: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
TellerRepositoryhas always caused confusion because I now have to work withReposRepositoryandReposTellerRepository.When you look at the readme, they have taken advantage of this removing of boilerplate idea.
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
DataSourceandRepositoryAPI design right now is very unit testable in the dev's app code". Well, check out my latest test I made for aDataSource:Yup.
DataSourcetests are worthless. That means that theDataSourcebeing it's very own class is also worthless.