Прежде чем вы начнете читать, позвольте мне сделать здесь одно замечание. Очевидно, этого можно добиться и на Java (или другом языке со статической типизацией), но, поскольку мой пример взят из библиотеки Android, выбор Kotlin был наиболее подходящим вариантом.

Безопасность типа

Я уверен, что у большинства из вас есть ненулевой опыт работы с JavaScript или Python. Это потому, что они отлично подходят для интеллектуальных небольших инструментов и прототипирования. Но это имеет свою цену. О, как мы любим эти милые красные линии в консоли после того, как какая-то переменная волшебным образом меняет свой тип… верно? Нет. Чем больше проект, над которым вы работаете, тем больше недостатков и когтей языка с динамической типизацией вы начнете испытывать. Вот, взгляните:

И результат:

boolean false
boolean false
string true
boolean true
string false
boolean true

Вы видите здесь какую-то логику? И я нет. Особенно последнее не имеет никакого смысла.

Введите статически типизированный язык

Безопасность типов — отличная особенность всех статически типизированных языков. Это позволяет нам забыть хотя бы об одном источнике проблем. Если мы работаем с объектом какого-то типа, мы точно знаем, что он содержит поля и методы этого типа и можем смело на них ссылаться. Более того, наша IDE тоже это знает, и поэтому очень часто наше кодирование сводится к выбору подходящей подсказки из списка. Как будто этого мало, компилятор тоже это знает, благодаря чему мы получим программу однозначно более эффективную, чем ее аналоги, написанные на языках с динамической типизацией.

Общие коллекции

Общие типы, также известные как «Написать один раз — использовать в тысячах разных случаев». Еще одна фантастическая вещь, которую мы используем каждый день. Приложив немного усилий, мы можем обобщить код для теоретически бесконечного числа случаев, сделав нашу программу чистой, читабельной и гибкой. Однако, прежде всего, мы выигрываем время (меньше w̵r̵i̵t̵i̵n̵g̵ копипаст) и безопасность (не позволяет нам забыть об одной из тысяч клонированных строк при изменении одной ошибочной строки кода). Ярким примером являются различные типы коллекций. Благодаря дженерикам нам не нужно создавать такие классы, как StringList, BucketList, WishList, чтобы использовать безопасность типов. Достаточно одного класса: List<ItemType>.

Выбор ингредиентов (Проблема)

Хорошо, после этого длинного вступления, давайте перейдем к тому, что вы ожидали найти в этой статье — рецепту вкусного торта! Начнем с чего-нибудь тривиального. Представьте, что вам нужно создать коллекцию, которая будет содержать данные многих разных типов, но эти типы никак не связаны и не могут быть связаны между собой. Что ж, единственный выход — вернуться в давние времена, когда по улицам Нью-Йорка бегали динозавры и на Java не было дженериков. Создадим коллекцию типа Any.

I/System.out: I am ApplePie.
I/System.out: I am Cheesecake.
I/System.out: I am Coffee.

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

I/System.out: Yummy apple pie!
I/System.out: Yummy cheesecake!
I/System.out: Delicious coffee!

Что мы сделали, так это создали простую иерархию и использовали фундаментальную функцию ООП для заполнения списка связанных объектов и доступа к ним через общий интерфейс. И это довольно круто. Вот почему мы так часто используем этот механизм. Иногда, однако, этого недостаточно. Давайте сделаем еще один шаг, не так ли? Мы хотели бы объединить оба требования в одно. Имейте коллекцию произвольных, несвязанных типов, но сохраняйте доступ к полям и методам каждого из них. Если мы знаем все возможные типы, которые могут появиться в коллекции, это не должно вызывать больших проблем. Однако это приведет к относительно длинному, неэффективному и подверженному ошибкам коду.

I/System.out: Yummy apple pie!
I/System.out: Yummy cheesecake!
I/System.out: Delicious coffee!
I/System.out: Meow?
I/System.out: Hey! You forgot about ThisClass!

При таком подходе класс каждого элемента коллекции должен проверяться и обрабатываться соответствующим образом. Одним из многих минусов является то, что любое изменение набора поддерживаемых моделей должно затрагивать и все эти блоки when/switch и эта задача, к сожалению, ложится на плечи программиста.

Если вы посмотрите внимательно, то увидите фабрику стратегий. Делает ли использование двух шаблонов проектирования хорошим решением? Вы можете судить сами.

Теперь пришло время для самого сложного сценария. Мы ничего не знаем о точных типах… но мы по-прежнему хотим безопасно обращаться к их полям и методам. У тебя есть идеи? Это невозможно? Это парадокс? Мы точно знаем, что предыдущий способ не сработает, потому что мы не будем включать все стандартные классы и все классы из используемых нами библиотек. Не только потому, что это было бы пустой тратой жизни, но и потому, что это просто невозможно, поскольку мы не контролируем библиотеки, импортируемые нашими пользователями (при условии, что мы сами создаем библиотеку). Итак, что нам теперь делать? Сдаться? Уйти с опущенными головами? Конечно, нет!

Они учили нас, что нельзя получить свой пирог и съесть его. Но так ли это? Давайте посмотрим, возможно ли это, и если да, то как сохранить информацию о типах в коллекции, которая не возражает против хранения числа двойной точности с плавающей запятой 3.141593 рядом со строковым литералом "PI".

Выпечка торта (Решение)

Работая над Kandy List Views, библиотекой Android для быстрого и эластичного создания списков, я столкнулся именно с этой проблемой. Я хотел дать разработчикам возможность забыть обо всех этих многословных адаптерах, представлениях типов и т. д. Но как это сделать, когда любой класс может быть моделью элемента списка? Более того, теоретически каждый элемент списка может быть выражен разным классом с разным представлением и разным поведением. Однако это можно сделать. Используя этот шаблон, вот как:

I/System.out: Yummy apple pie!
I/System.out: Yummy cheesecake!
I/System.out: Delicious coffee!
I/System.out: Meow?

Пожалуйста, имейте в виду, что многие вещи были упрощены, чтобы подчеркнуть саму идею. Библиотека с открытым исходным кодом, поэтому вы можете увидеть этот паттерн во всей красе на GitHub.

Хорошо, позвольте мне рационализировать магию, стоящую за этим. У нас есть класс ListItem, который содержит объект любого типа, и контроллер (здесь AbstractItemPrinter), отвечающий за использование этого объекта в интерфейсе, который нам нужен в нашей коллекции. Для простоты этот интерфейс содержит только один метод — print. В то время как основная цель print — предоставить удобный способ наследования контроллера благодаря доступу к типу объекта (без необходимости каждый раз приводить его), метод delegatePrint используется вне и позволяет для передачи любого аргумента после потери информации о фактических типах в коллекции элементов любого типа. И действительно, эта информация теряется, как только используется подстановочный знак (ListItem<*> ). В Kotlin в этом сценарии общий тип просто становится Nothing, что делает все поля и методы, использующие ItemType, совершенно бесполезными. delegatePrint решает эту проблему.

Вуаля! У нас есть торт, и мы его съели. Можем ли мы вставить совершенно произвольный объект в коллекцию? Конечно. Завернутый в ListItem. Есть ли у нас доступ к фактическому классу каждого элемента? Конечно. Везде, где нам нужно в нашем пользовательском интерфейсе.

Приятного аппетита! (Заключительные заметки)

Этот шаблон безопасен и работает, но помните, что класс элемента списка (здесь ListItem) должен обеспечивать совместимость типов между хранимыми данными и общим параметром адаптера. Другими словами, вы должны убедиться, что приведение в методе делегата действительно всегда безопасно. Если вы не хотите форсировать его так явно, как в приведенном выше примере, вам придется проверить его другим способом и быть предельно осторожным, если модель или держатель могут изменяться.

Я понимаю, что именно в этом примере программа могла бы быть еще проще и безопаснее. Перемещение метода print в класс ListItem гарантирует, что передача аргумента неправильного типа будет не только нелогичной, но и невозможной. Однако это был не вариант для библиотеки, из которой была взята эта концепция. RecyclerView.Holder не может хранить информацию о модели, и поэтому, применяя ее к текущему примеру, невозможно было иметь и item, и printer в одном классе.

Что мы могли бы сделать здесь по-другому, так это создать иерархию на основе ListItem, а не Printer (т.е. ConsumableListItem, CatListItem и т. д.). это лучше. Первое вполне очевидно. Как упоминалось ранее, RecyclerView.Holder просто не может хранить модель. Второй резюмируется известной фразой: «Предпочитайте композицию наследованию».

Я очень рада видеть вас здесь! Означает ли это, что вы прочитали всю статью или просто пропустили ее? 😏 Если вы хорошо провели время за чтением, буду очень признательна за любой отзыв. Оставьте комментарий или просто похлопайте в ладоши, чтобы другим было легче найти этот контент. 🙂

Если вам интересна библиотека, которую я упомянул, вы можете найти ссылку ниже. Надеемся, это сэкономит вам драгоценное время. 😉



Что дальше