rimes
rimes

Reputation: 921

Extend set of fixed values (enum, sealed interface, sealed class...) without repeating them

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

Answers (1)

Sweeper
Sweeper

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 FooFilters and SearchFilters, 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 objects 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

Related Questions