Last active
February 12, 2019 11:58
-
-
Save ShahOdin/b56d4a1ba2e8a9eb232b16bf67eee370 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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