Наследование классов и параметризованные типы в Kotlin: возможен ли полиморфизм подтипов с помощью дженериков?

Я изо всех сил пытаюсь понять и / или заставить генерики и полиморфизм Kotlin работать на меня. Рассмотрим этот код:

class Item<T: BaseAttributes> {
    var id: Long = -1L
    lateinit var type: String
    lateinit var attributes: T
}

open class BaseAttributes {
    lateinit var createdAt: String
    lateinit var updatedAt: String
}

open class BaseResponseList<T : BaseAttributes> {
    lateinit var items: List<Item<T>> // the collection of items fetched from an API 
}

class FruitAttributes(val id: Long, val color: String /* ... */) : BaseAttributes()

class FruitResponseList: BaseResponseList<FruitAttributes>()

// base service for all types of items
interface ApiService {
    fun getItems(): BaseResponseList<BaseAttributes>
    // fun getItemById(itemId: Long): BaseResponse<BaseAttributes>
    /* other CRUD functions here ... */
}

// service for fruits
interface FruitService: ApiService {
    override fun getItems(): FruitResponseList // get fruit items
}

Я озадачен этой ошибкой компилятора, которая предполагает, что FruitResponseList не является подтипом параметризованного базового класса (BaseResponseList<FruitAttributes>):

Return type of 'getItems' is not a subtype of the return type of the overridden member 'public abstract fun getItems(): BaseResponseList<BaseAttributes> defined in ApiService'

Я пытаюсь использовать ковариацию сайта объявления в BaseAttributes, чтобы сообщить компилятору о своем намерении, что FruitResponseList является подклассом базового списка ответов следующим образом:

open class BaseResponseList<out T : BaseAttributes> {
    lateinit var items: List<Item<T>> // the collection of items fetched from an API 
}

приводит к этой ошибке:

Type parameter T is declared as 'out' but occurs in 'invariant' position in type List<Item<T>>

Как я могу добиться соотношения типа и подтипа между списками ответов Fruit и Base?

Контекст

Я реализую сетевой код для выполнения операций CRUD с API, основанным на формате Спецификация JSON API, поэтому я создал классы атрибутов и данных (Item) для представления объектов ответа json.

Моя цель - уменьшить количество дублированного кода, чтобы мне нужно было только один раз написать декларации службы API для каждой сущности в моем приложении (фрукты, поставщики, покупатели и т. д.). Я также хочу избежать написания дублированных / шаблонных реализаций уровней репозитория данных для каждого объекта в моем приложении (в контексте чистой архитектуры). Я должен иметь возможность просто указать типы, специфичные для бизнес-сущностей (модели / сущности), и позволить одной общей реализации выполнять работу по выборке сетевых данных.

Я подумал, что для этого имеет смысл использовать дженерики и наследование. В этом конкретном примере идея состоит в том, что GET, специфичный для фруктов, будет возвращать список ответов фруктов, который является подтипом базового списка ответов. Будем очень признательны за любые рекомендации по этому поводу или альтернативные подходы к этой проблеме.

Предлагаемое чтение: docs.oracle.com/javase/tutorial/java/generics/inheritance.ht‌ ml

m0skit0 14.05.2018 22:42
3
1
696
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Почему не что-то вроде этого:

// base service for all types of items
interface ApiService<T> {
    fun getItems(): T
    // fun getItemById(itemId: Long): BaseResponse<BaseAttributes>
    /* other CRUD functions here ... */
}

// service for fruits
interface FruitService: ApiService<FruitResponseList> {

    override fun getItems(): FruitResponseList
}

или это :

// base service for all types of items
interface ApiService<T : BaseAttributes> {
    fun getItems() : BaseResponseList<T>
    // fun getItemById(itemId: Long): BaseResponse<BaseAttributes>
    /* other CRUD functions here ... */
}

// service for fruits
interface FruitService: ApiService<FruitAttributes> {

    override fun getItems(): FruitResponseList
}

Вы будете привязаны к BaseResponseList<BaseAttributes> в вашей текущей реализации.

Ответ принят как подходящий

I'm stumped by this compiler error that suggests FruitResponseList is not a subtype of the parametrized base class (BaseResponseList<FruitAttributes>):

Это подтип BaseResponseList<FruitAttributes>, который не является подтипом BaseResponseList<BaseAttributes>.

I try to use declaration-site covariance in BaseAttributes to tell the compiler my intention that a FruitResponseList is a subclass of the base response list like this:...

Это может быть правильным подходом, но проблема в том, что Item не является ковариантным (и этого не может быть, потому что attributes - это var, а его установщик принимает параметр T). Если Item можно изменить, чтобы избежать этого, хорошо.

Другой подход - добавить параметр типа в ApiService:

// base service for all types of items
interface ApiService<T: BaseAttributes> {
    fun getItems(): BaseResponseList<T>
    // fun getItemById(itemId: Long): BaseResponse<T>
    /* other CRUD functions here ... */
}

// service for fruits
interface FruitService: ApiService<FruitAttributes> {
    override fun getItems(): FruitResponseList // get fruit items
}

Ответы Марка и Алексея оказались очень полезными. В частности, параметризация ApiService на BaseAttributes была более гибким вариантом, поскольку она позволяла функциям принимать или возвращать оба типа BaseResponseList и BaseResponse [под].

Однако, что бы это ни стоило, оказывается, что библиотека Retrofit не позволяет своим объявлениям служб расширять другие интерфейсы или даже параметризоваться, предположительно в пользу композиции, а не наследования. По поводу их решения по этому проблема здесь ведется много споров. В итоге я создал отдельные интерфейсы для каждой из моих моделей: /

Другие вопросы по теме