Reputation: 921
I have a class that has a fixed set of filters and is responsible for fetching data from an search API (lets say "PERSON;CAR;BUILDING"). Now I want to wrap this search class inside an "Extended" search (by using something like an adapter pattern), and add an additional endpoint which fetches also ANIMALS (another endpoint). For the Extended class I have the Combined filter "PERSON;CAR;BUILDING;ANIMALS" where the first three still work for the one API and the last one (ANIMALS) works for the second API.
My question is, is there a nice approach to make these filters somehow compatible/dependant on each other. The best solution would be to extend the first filter with the items for "Combined" filter. I have been playing around with sealed interfaces/classes but so far I could not find a nice way to do that. My solution for now is to use enums (copy the values from all filters) and make a mapper from the combined filter to the real search filter. The issues with this approach are that I am duplicating all filter values and in case the search filter gets extended it could easily be forgotten to also extend the combined filter.
Edit: added code
enum class SearchFilter { PERSON, CAR, BUILDING }
class GeneralSearch {
fun search (filter: SearchFilter)
}
class AnimalSearch {
fun search ()
}
Combine both api-s:
enum class CombinedSearchFilter { PERSON, CAR, BUILDING, ANIMALS }
class CombinedSearch (
generalSearch: GeneralSearch,
animalSearch: AnimalSearch
) {
fun search (filter: CombinedSearchFilter)
}
The issue is that the CombinedSearchFilter
is duplicating the values from SearchFilter
without any constraint.
Upvotes: 1
Views: 312
Reputation: 273540
You can make SearchFilter
, CombinedSearch
and AnimalSearch
sealed interfaces. Each filter (the enum values) would then implement one or more of those interfaces. You can make SearchFilter
and AnimalSearch
inherit from CombinedSearch
as well.
sealed interface SearchFilter: CombinedFilter
sealed interface AnimalFilter: CombinedFilter
sealed interface CombinedFilter
object Person: SearchFilter
object Car: SearchFilter
object Building: SearchFilter
object Animal: AnimalFilter
or put the individual filters in other objects if you want code completion:
sealed interface CombinedFilter
sealed interface SearchFilter: CombinedFilter {
object Person: SearchFilter
object Car: SearchFilter
object Building: SearchFilter
}
sealed interface AnimalFilter: CombinedFilter {
object Animal: AnimalFilter
}
Because of this type hierarchy, CombinedSearch.search
would be able to take all 4 of Person
, Car
, Building
and Animal
.
The downside of this is that if you happen to have another combination of APIs, you would need to modify the declarations of all the involved filter types. For example, if there were a third API with a FooFilter
type:
sealed interface FooFilter
object Foo1: FooFilter
object Foo2: FooFilter
And you want a CombinedFilter2
that combines FooFilter
s and SearchFilter
s, you'd need to add:
sealed CombinedFilter2
sealed interface SearchFilter: CombinedFilter, CombinedFilter2
^^^^^^^^^^^^^^^^^
sealed interface FooFilter: CombinedFilter2
^^^^^^^^^^^^^^^
If you want code-completion on CombinedFilter
, then I can't think of any way unless you get rid of sealed
.
Represent each filter as anonymous object properties, and wrap object
s around them. The combined filters will have two properties, each initialised to the object
containing the constituent filters.
// this can still be sealed
sealed interface CombinedFilter
interface SearchFilter: CombinedFilter
object SearchFilters {
val person = object: SearchFilter {}
val car = object: SearchFilter {}
val building = object: SearchFilter {}
}
interface AnimalFilter: CombinedFilter
object AnimalFilters {
val animal = object: AnimalFilter {}
}
object CombinedFilters {
val search = SearchFilters
val animal = AnimalFilters
}
Another option is to keep using enums, but CombinedSearch
would take a Either<AnimalFilter, SearchFilter>
.
enum AnimalFilter { ANIMAL }
sealed interface Either<out L, out R> {
data class Left<L>(val value: L): Either<L, Nothing>
data class Right<R>(val value: R): Either<Nothing, R>
}
Example usage:
combinedSearch.search(Either.Right(AnimalFilter.ANIMAL))
combinedSearch.search(Either.Left(SearchFilter.CAR))
The downside of this is that it's more inconvenient on users' side - there is whole lot more "ceremony".
Upvotes: 1