Skip to content

Instantly share code, notes, and snippets.

@ShahOdin
Last active February 12, 2019 11:58
Show Gist options
  • Save ShahOdin/b56d4a1ba2e8a9eb232b16bf67eee370 to your computer and use it in GitHub Desktop.
Save ShahOdin/b56d4a1ba2e8a9eb232b16bf67eee370 to your computer and use it in GitHub Desktop.
type Id = Int
type Film = String
type Series = String
type Episode = String
type Url = String
case class DataBaseClient(url: Url) {
def get(s:String, id: Id): String = "foo"
}
object Client1 {
val dataBaseUrl: Url = ???
val dataBaseClient = DataBaseClient(dataBaseUrl)
def getFilm(id:Id): Film = dataBaseClient.get(???, id)
def getSeries(id:Id): Series = dataBaseClient.get(???, id)
def getEpisode(id:Id): Episode = dataBaseClient.get(???, id)
}
//pros: quick to implement
//cons: not mockable as implementation is hard-coded in Client itself.
//cons: Client1 object can become too big and hard to scheme and navigate through.
class Client2(dataBaseUrl: Url) {
val dataBaseClient = DataBaseClient(dataBaseUrl)
def getFilm(id:Id): Film = dataBaseClient.get(???, id)
def getSeries(id:Id): Series = dataBaseClient.get(???, id)
def getEpisode(id:Id): Episode = dataBaseClient.get(???, id)
}
//pros: still quick to implement
//pros(cons): slightly better mockablity as client is parameterised now.
// can:
// - use wiremock to mock it.
// - override methods of interest.
// but implementation is still coupled with the interface.
//cons: Client2 class can become too big and hard to get a top-level view.
trait Client3 {
def getFilm(id:Id): Film
def getSeries(id:Id): Series
def getEpisode(id:Id): Episode
}
class Client3Impl(client: DataBaseClient) extends Client3{
def getFilm(id:Id): Film = client.get(???, id)
def getSeries(id:Id): Series = client.get(???, id)
def getEpisode(id:Id): Episode = client.get(???, id)
}
object Client3 {
def apply(dataBaseUrl: Url): Client3 = new Client3Impl(DataBaseClient(dataBaseUrl))
}
//cons: some boilerplate: interface, implementation layer + helper instantiation logic.
//pros: fully mockable as the implementation is decoupled
//pros: Client3 trait gives a clean view of the interface.
//cons: three layers to walk through, bit difficult to navigate.
//pros: instantiation layer clean and Client3 object is small.
trait Client4 {
def getFilm(id:Id): Film
def getSeries(id:Id): Series
def getEpisode(id:Id): Episode
}
object Client4 {
def apply(dataBaseUrl: Url): Client4 = new Client4 {
//instantiation layer
val dataBaseClient = DataBaseClient(dataBaseUrl)
//implementation layer
def getFilm(id:Id): Film = dataBaseClient.get(???, id)
def getSeries(id:Id): Series = dataBaseClient.get(???, id)
def getEpisode(id:Id): Episode = dataBaseClient.get(???, id)
}
}
//cons: quick to implement.
//pros: easily mockable
//pros: Client4 trait gives a clean view of the interface.
//pros: two layers to walk through, bit difficult to navigate.
//cons: instantiation layer and implementation layer coupled. could become too big.
//most times Client3 pattern is the most optimised solution.
//some times not, ie:
trait GetData {
def getFilm(id:Id): Film
def getSeries(id:Id): Series
def getEpisode(id:Id): Episode
}
case class AggregatedData(film: Film, series: Series, episode: Episode)
trait GetAggregatedData {
def getAggregatedData: AggregatedData
}
object GetDataImplNew {
def apply(url: Url): GetData = ???
}
object GetDataImplOld {
def apply(url: Url): GetData = ???
}
object GetAggregatedData {
def getAggregatedData(url1: Url, url2: Url): GetAggregatedData = new GetAggregatedData {
val getDataNew = GetDataImplNew(url1)
val getDataOld = GetDataImplOld(url2)
//the logic for combining/manipulating responses from clients only here.
def getAggregatedData: AggregatedData = {
val aggregatedDataNew: AggregatedData = AggregatedData(getDataNew.film,getDataNew.series,getDataNew.episodes)
val aggregatedDataOld: AggregatedData = AggregatedData(getDataOld.film,getDataOld.series,getDataOld.episodes)
if(???) aggregatedDataNew else aggregatedDataOld
}
}
}
//or even if we need to do some processing
trait GetData2 {
def getFilm(id:Id): Film
def getSeries(id:Id): Series
def getEpisode(id:Id): Episode
}
object GetData2Impl{
def apply(): GetData2 = ???
}
object GetData2 {
def apply(): GetData = new GetData {
val lowLevelImpl = GetData2Impl()
val defaultFilm = ???
def getFilm(id:Id): Film = {
val normalFilm = lowLevelImpl.getFilm(id)
if (???) normalFilm else defaultFilm
}
def getSeries(id:Id): Series = ???
def getEpisode(id:Id): Episode = ???
}
}
// here we separated the database layer from the business logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment