Concept

Harmony repositories are responsible of managing the data business logic of the application data layer, similar to interactors being responsible of managing the business logic of the domain layer.

Following the interface definition of data source, a repository defines a generic interface representing the three main action groups:

  • Get is the responsible of all actions that fetch data from one or many data sources
  • Put is the responsible of all actions that modify and push data to one or many data sources
  • Delete is the responsible of all action that delete data from one or many data sources

Repositories can accomplish many different things. For example, handle retries of failed processes, perform object validations, handling caches, and more.

In an effort to decouple the business logic of the data layer from the business logic domain layer, repositoreis use the concept of Operation: an object that intrinsically defines how a query must be forwarded to a data source.

For more information, read the Operation reference.

Understanding the abstraction

It's a good idea to use repositories isntead of data sources directly becuase often you will want to do a more elaborated data management (aka. data business logic).

For example, we can think in the case of buidling a simple cache system. Typically, starting from a network API service class, we would write some code similar to:

class BookNetworkAPIService {
let books = Map<Int:Book>()
func getBook(id): Book {
let isCached = books.contains(id)
if (isCached) {
return books[id]
} else {
let book = getBookFromNetwork(id)
books[id] = book
return book
}
}
}

Obviously, the code above is coupling a cache system to a network class. A better option would be to create a cache class on top of the network one, which is what Repository proposes.

class BookNetworkDataSource : GetDataSource<Book> {...}
class BookLocalStorageDataSource : GetDataSource<Book>, PutDataSource<Book> {...}
// Fetches from cache if available, otherwise use network and udpate cache
class CacheSyncOperation
class BookRepository : GetRepository<Book> {
let network: BookNetworkDataSource
let cache: BookLocalStorageDataSource
func get(query, operation): Book {
if (operation istypeof CacheSyncOperation) {
let cachedBook = cache.get(query)
if (cachedBook) {
return cacheBook
} else {
let book = network.get(query)
cache.put(book, query)
return book
}
} else {
// Otherwise, return from network
return network.get(query)
}
}
}

As seen in the example, we are reusing the generic interface of Harmony data sources. This could lead to a generic implementation of a cache repository that can be reused for any kind of data types. (hint: see CacheRepository)

IMPORTANT

Each repository must represent an atomic behavior (keeping its testability). It's possible to compose multiple repositories to achieve a more complex logic.

Interfaces

The Repository functions replicate the DataSource public API, adding an extra parameter of type Operation on each function.

Get

interface GetRepository<V> : Repository {
fun get(query: Query, operation: Operation = DefaultOperation): Future<V>
fun getAll(query: Query, operation: Operation = DefaultOperation): Future<List<V>>
}

Put

Actions related functions.

interface PutRepository<V> : Repository {
fun put(query: Query, value: V?, operation: Operation = DefaultOperation): Future<V>
fun putAll(query: Query, value: List<V>? = emptyList(), operation: Operation = DefaultOperation): Future<List<V>>
}

Delete

Deletion related functions.

interface DeleteRepository : Repository {
fun delete(query: Query, operation: Operation = DefaultOperation): Future<Unit>
}

Extensions

Not all Harmony languages are capable of supporting all extensions. Find below the list of all extensions by supported platform.

Key Access

Instead of using IdQuery to interface with repositories, there are extensions to syntax sugar the creation of IdQuery.

This means that instead of calling a repository with a query new IdQuery('my-key'), it can be used directly the my-key identifier.

fun <K, V> GetRepository<V>.get(id: K, operation: Operation = DefaultOperation): Future<V> = get(IdQuery(id), operation)
fun <K, V> GetRepository<V>.getAll(ids: List<K>, operation: Operation = DefaultOperation): Future<List<V>> = getAll(IdsQuery(ids), operation)
fun <K, V> PutRepository<V>.put(id: K, value: V?, operation: Operation = DefaultOperation): Future<V> = put(IdQuery(id), value, operation)
fun <K, V> PutRepository<V>.putAll(ids: List<K>, values: List<V>? = emptyList(), operation: Operation = DefaultOperation) = putAll(IdsQuery(ids), values, operation)
fun <K> DeleteRepository.delete(id: K, operation: Operation = DefaultOperation) = delete(IdQuery(id), operation)

Find below examples by platform:

// Instead of:_
repository.get(IdQuery("myKey"), operation)
repository.put(IdQuery("myKey"), object, operation)
repository.delete(IdQuery("myKey"), operation)
// Use:
repository.get("myKey", operation)
repository.put("myKey", object, opeartion)
repository.delete("myKey", operation)

Default Implementations

Harmony provides multiple default implementations.

Find below a list of the most common ones: