Русский
Русский
English
Статистика
Реклама

Visitor

Простой вариант разношерстного recycler view на шаблоне Посетитель

07.04.2021 16:22:59 | Автор: admin

Прошло пол года, как я с паскаля перекатился на kotlin и влюбился в android-разработку, и вот уже разрешаю себе публично лезть со своими идеями в чужой монастырь. Но причина на то есть. Понаблюдав в профильных чатах за тем, какие чаще всего возникают вопросы у android-разработчиков, и не только у новичков, я понял, что в большинстве случаев, когда человек сталкивается с ошибкой, которую не может понять, как не может понять объяснение коллег из чата или их наводящие вопросы, причиной является бездумное использование готовых кусков кода или библиотек. Однако, полагаясь на готовые примеры кода, которые у них не работают (а в этой сфере код, написанный больше года назад, по умолчанию требует обновления или вообще переработки, и это касается кода со stack overflow, библиотечных гайдов и даже гайдов от самого Google), они не понимают причин возникающих ошибок или же отличающегося поведения, поскольку полагаются на библиотеку как китайскую комнату, не пытаясь разобраться в её архитектуре и принципах работы.

Поскольку очень часто всплывают вопросы, связанные с recycler view, я бы хотел немного разобраться в том, как самостоятельно сделать расширяемый и чистый код для отображения мульти-элементного списка в приложении.


Изучая архитектурные шаблоны android-разработки, я приучил себя в первую очередь искать ответы на сервере Google developer guides. Но иногда там, особенно в обучающих codelabs, приводятся примеры кода больше упрощенного, чем рассчитанного на универсальность, чистоту и расширяемость.

В данном случае у меня возникла потребность использовать модный recycler view для отображения списка элементов с разной внутренней разметкой и логикой. На такой идее строятся все современные приложения - от мессенджеров и лент социальных сетей до банковских приложений. К тому же комбинирование на лету с использованием реактивного подхода разных визуальных элементов списка recycler view вместо ручной верстки разметки является мостиком в мир декларативно-функционального ui, который нам предлагают в Jetpack Compose, и на который рано или поздно Google мягко предложит переходить.

Codelab, посвященный включению в список recycler view элемента с другой разметкой, строится на оборачивании элемента списка внутрь sealed класса. Но это не главный недостаток. Главный, на мой взгляд,- помещение всего кода, обрабатывающего разные элементы списка, внутрь класса самого адаптера. Это заставит в будущем расти код адаптера как снежный ком, нарушая как принцип открытости/закрытости, так и принцип единственной ответственности (при желании, можно найти нарушения каждой буквы из акронима SOLID, но поэтому их и объединили).

Другим существенным минусом является то, что Google предложил вынести свойство id из data-классов в качестве дискриминатора двух типов элементов: для заголовка id будет равен Long.MIN_VALUE, а для данных id будет транзитом переходить из data-класса. И здесь полностью закрыта щелочка для дальнейшего расширения: у вас или data-класс, который для адаптера будет всегда одинаков, или заголовок. Вся архитектура могучего recycler view мгновенно сжалась до всего лишь двух вариантов.

Решением проблемы можно считать использование готовых библиотек. Я из самых актуальных и наиболее распространенных нашел adapter delegates, groupie и epoxy. По ним по всем написаны многие статьи как здесь, так и там. Самый базовый подход, который я собираюсь сейчас изложить, наиболее близко воплощен в первой библиотеке. Группи и эпокси гораздо мощнее, универсальнее, но при этом сложнее внутри и станут сложнее снаружи, если вдруг разработчику захочется использовать всю их мощь.

Любая библиотека всегда таит в себе две беды:

  • вам бывает лень разбираться в ее устройстве, в итоге вы не осознаете, что используете библиотеку всего лишь на 10%, прямо как мозг у некоторых млекопитающих;

  • вы зависите от библиотечных классов: вам или надо наследовать ваши данные от них, или еще как-то ломать свои представления об идеальных data-классах и их движении.

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

Но также не беда, если вам захочется понять принцип, на котором все перечисленные выше библиотеки основаны, а поняв, попробовать реализовать свой собственный способ работы с recycler view с именно той функциональностью, которая нужна вам в вашем конкретном приложении. Иногда пара своих дополнительных классов с десятком строк кода в каждом вместо зависимости от библиотеки делает то же самое, при этом не перегружая код и оставляя вам гораздо больше свободы.

Так вот, в самом широком смысле в этих библиотеках применяется паттерн Посетитель с менеджером.

Адаптер recycler view, то есть обычный ListAdapter, уже имеет все необходимые методы, которые подталкивают к использованию Посетителя:

  • getItemType - функция, которая должна возвращать тип элемента (поскольку тип в данном случае всего лишь целое число, Google рекомендует использовать более понятные константы);

  • onCreateViewHolder - функция, которая возвращает ViewHolder такого класса, который реализован для требуемого типа элемента (тип передается в функцию параметром с помощью предыдущей функции);

  • onBindViewHolder - функция, которая осуществляет привязку конкретного элемента в списке (передается по номеру) и конкретного ViewHolder, который создан и возвращен предыдущей функцией.

С учетом специфики recycler view всегда следует помнить, что после пролистывания экрана для следующих элементов recycler view использует уже созданные места, поэтому если не хочется определять соответствие типа каждый раз, нужно внимательно следить за тем, что и в какие классы передается, а также ответственно отнестись к реализации DiffUtil-коллбэка.

Если вы реализуете стандартный шаблон DiffCallback,
class BaseDiffCallback : DiffUtil.ItemCallback<HasStringId>() {    override fun areItemsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem.id == newItem.id    override fun areContentsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem == newItem}

то всегда помните, что areContentsTheSame вызывается только тогда, когда areItemsTheSame возвращает true. В моем примере классы реализуют интерфейс HasStringId, в котором есть id типа String и метод equals, что позволяет использовать data-классы как для моделей данных, так и для моделей слоя view. Data-классы с данными из сети всегда имеют уникальный id, поэтому их отличие DiffUtil определяет максимально быстро, а для вспомогательных ui-классов с одинаковыми id вызываются оба метода.

Чтобы управлять набором посетителей, введем такое понятие, как менеджер. Это будет класс, реализующий следующий интерфейс:

interface ViewHoldersManager {    fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor)    fun getItemType(item: Any): Int    fun getViewHolder(itemType: Int): ViewHolderVisitor}

И определим для целей тестового примера набор типов для recycler view:

object ItemTypes {    const val UNKNOWN = -1    const val HEADER = 0    const val TWO_STRINGS = 1    const val ONE_LINE_STRINGS = 2    const val CARD = 3}

Собственно менеджер и будет тем "делегатом" в терминологии adapter delegates, которому адаптер делегирует функции по определению типа для отрисовки текущего элемента. Для этого в менеджере должны быть зарегистрированы все необходимые классы вью холдеров.

Я буду использовать hilt и data binding, поскольку с их помощью очень легко решить второстепенные задачи: внедрить менеджера вью холдеров и упростить отображение данных на ui. Бонусом будет точное знание того места, где именно и в какой момент инициализируется менеджер вью холдеров, а также какие вью холдеры регистрируются в нем:

@Module@InstallIn(FragmentComponent::class)object DiModule {    @Provides    @FragmentScoped    fun provideAdaptersManager(): ViewHoldersManager = ViewHoldersManagerImpl().apply {        registerViewHolder(ItemTypes.HEADER, HeaderViewHolder())        registerViewHolder(ItemTypes.ONE_LINE_STRINGS, OneLine2ViewHolder())        registerViewHolder(ItemTypes.TWO_STRINGS, TwoStringsViewHolder())        registerViewHolder(ItemTypes.CARD, CardViewHolder())    }}

Добавим верстку для всех элементов:

Сard item
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools">    <data>        <variable name="card" type="ru.alexmaryin.recycleronvisitor.data.ui_models.CardItem" />    </data>    <androidx.cardview.widget.CardView xmlns:card_view="http://personeltest.ru/away/schemas.android.com/apk/res-auto"        android:id="@+id/card_view"        android:layout_width="match_parent"        android:layout_height="200dp"        android:layout_margin="8dp"        card_view:cardBackgroundColor="@color/cardview_shadow_end_color"        card_view:cardCornerRadius="15dp">        <ImageView            android:id="@+id/card_background_image"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_gravity="center"            android:scaleType="centerCrop"            tools:ignore="ContentDescription"            tools:src="@android:mipmap/sym_def_app_icon" />        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:layout_gravity="bottom"            android:background="@android:drawable/screen_background_dark_transparent"            android:orientation="vertical"            android:padding="16dp">            <TextView                android:id="@+id/card_title"                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:ellipsize="end"                android:maxLines="1"                android:paddingTop="8dp"                android:paddingBottom="8dp"                android:textAllCaps="true"                android:textColor="#FFFFFF"                android:textStyle="bold"                tools:text="Cart title"                android:text="@{card.title}"/>            <TextView                android:id="@+id/txt_discription"                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:ellipsize="end"                android:maxLines="2"                android:textColor="#FFFFFF"                tools:text="this is a simple discription with losts of text lorem ipsum dolor sit amet,            consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."                android:text="@{card.description}"/>        </LinearLayout>    </androidx.cardview.widget.CardView></layout>
One line item
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools">    <data>        <variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.OneLineItem2" />    </data>    <androidx.constraintlayout.widget.ConstraintLayout        android:layout_width="match_parent"        android:layout_height="wrap_content">        <TextView            android:id="@+id/text1"            android:layout_width="0dp"            android:layout_height="wrap_content"            android:paddingStart="8dp"            android:text="@{model.left}"            android:textAlignment="textEnd"            android:textAppearance="?attr/textAppearanceListItem"            android:textColor="@color/cardview_dark_background"            app:layout_constraintEnd_toStartOf="@+id/divider"            app:layout_constraintHorizontal_bias="0.5"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintTop_toTopOf="parent"            tools:ignore="RtlSymmetry,TextContrastCheck"            tools:text="Left text" />        <ImageView            android:id="@+id/divider"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:alpha="0.6"            android:padding="5dp"            android:scaleType="center"            android:scaleX="0.5"            android:scaleY="0.9"            android:src="@drawable/ic_outline_waves_24"            android:visibility="visible"            app:layout_constraintBottom_toBottomOf="@+id/text1"            app:layout_constraintEnd_toStartOf="@+id/text2"            app:layout_constraintHorizontal_bias="0.5"            app:layout_constraintStart_toEndOf="@+id/text1"            app:layout_constraintTop_toTopOf="@+id/text1"            app:srcCompat="@drawable/ic_outline_waves_24"            tools:ignore="ContentDescription"            tools:visibility="visible" />        <TextView            android:id="@id/text2"            android:layout_width="0dp"            android:layout_height="wrap_content"            android:paddingEnd="8dp"            android:text="@{model.right}"            android:textAppearance="?attr/textAppearanceListItem"            app:layout_constraintBottom_toBottomOf="@+id/divider"            app:layout_constraintEnd_toEndOf="parent"            app:layout_constraintHorizontal_bias="0.5"            app:layout_constraintStart_toEndOf="@+id/divider"            app:layout_constraintTop_toTopOf="@+id/divider"            tools:ignore="RtlSymmetry"            tools:text="Right text" />    </androidx.constraintlayout.widget.ConstraintLayout></layout>
Two line item
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto">    <data>        <variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.TwoStringsItem" />    </data>    <androidx.constraintlayout.widget.ConstraintLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:minHeight="?attr/listPreferredItemHeight"        android:mode="twoLine"        android:paddingStart="?attr/listPreferredItemPaddingStart"        android:paddingEnd="?attr/listPreferredItemPaddingEnd">        <TextView            android:id="@+id/text1"            android:layout_width="0dp"            android:layout_height="wrap_content"            android:layout_marginTop="8dp"            android:text="@{model.caption}"            app:layout_constraintTop_toTopOf="parent"            app:layout_constraintStart_toStartOf="parent"            android:textAppearance="?attr/textAppearanceListItem" />        <TextView            android:id="@id/text2"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:text="@{model.details}"            app:layout_constraintTop_toBottomOf="@id/text1"            app:layout_constraintStart_toStartOf="parent"            android:textAppearance="?attr/textAppearanceListItemSecondary" />    </androidx.constraintlayout.widget.ConstraintLayout></layout>
Header item
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <data>        <variable            name="headerItem"            type="ru.alexmaryin.recycleronvisitor.data.ui_models.RecyclerHeader" />    </data>    <TextView        style="@style/regularText"        android:id="@+id/header"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:background="#591976D2"        android:textAlignment="center"        android:textStyle="italic"        android:text="@{headerItem.text}"/></layout>

Вью холдер будет классом, реализующим простой интерфейс Посетителя:

interface ViewHolderVisitor {    val layout: Int    fun acceptBinding(item: Any): Boolean    fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById)}

Здесь два стандартных для Посетителя метода (классически они называются acceptVisitor и execute, однако мы же пишем не абстрактного посетителя для реализации паттерна в вакууме, а весьма конкретное его приложение) - acceptBinding и bind, а также свойство layout, в которое конкретные вью холдеры будут записывать ссылку на ресурс своей разметки.

Роль функции accept заключается в следующем: когда адаптер просит (или требует) от менеджера выдать ему соответствие типа элемента тому объекту, который должен быть отрисован, менеджер пробегается по всем зарегистрировавшимся вью холдерам, вызывая их метод accept, и выдает тип первого, который вернет true. Таким образом, ни адаптер, ни менеджер не знают ничего о внутреннем устройстве и даже классе самих элементов, что позволяет бесконечно увеличивать их количество и изменять как угодно. Единственное требование - зарегистрировавшийся вью холдер должен честно признаваться (accept = true), что готов забиндить какой-то элемент, тип которого он указал при регистрации.

Реализация как менеджера, так и конкретных вью холдеров будет настолько короткой, насколько это возможно. Практически везде однострочные функции:

class ViewHoldersManagerImpl : ViewHoldersManager {    private val holdersMap = emptyMap<Int, ViewHolderVisitor>().toMutableMap()    override fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor) {        holdersMap += itemType to viewHolder    }    override fun getItemType(item: Any): Int {        holdersMap.forEach { (itemType, holder) ->             if(holder.acceptBinding(item)) return itemType        }        return ItemTypes.UNKNOWN    }    override fun getViewHolder(itemType: Int) = holdersMap[itemType] ?: throw TypeCastException("Unknown recycler item type!")}

И для примера вью холдер карточки (другие реализации очевидны и практически аналогичны):

class CardViewHolder : ViewHolderVisitor {      override val layout: Int = R.layout.card_item    override fun acceptBinding(item: Any): Boolean = item is CardItem    override fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById) {        with(binding as CardItemBinding) {            card = item as CardItem            Picasso.get().load(item.image).into(cardBackgroundImage)        }    }}

Не стоит бояться операторов явного приведения типов as в коде. Во-первых, реализуя интерфейс, вы заключаете определенный контракт: если функция accept согласна с тем, что Посетитель работает с элементами класса CardItem, в метод bind совершенно точно будет передан объект только этого класса и никакого другого. Это же касается и разметки: если вы однозначно определяете имя ресурса разметки в свойстве layout, именно для этой разметки в свойство binding будет передаваться сгенерированный data binding класс. Ну и во-вторых, если бы это не было безопасно, разве линтер idea или android studio не разразились ли громкими воплями на всю сборку?

Что касается самого главного, адаптера для recycler view,- он единственный и универсальный, поскольку все функции по различению разных элементов делегирует менеджеру, а тот в свою очередь выбирает одного единственного холдера, согласного посетить и прибиндить намертво элемент к экрану смартфона:

class BaseListAdapter(    private val clickListener: AdapterClickListenerById,    private val viewHoldersManager: ViewHoldersManager) : ListAdapter<HasStringId, BaseListAdapter.DataViewHolder>(BaseDiffCallback()) {    inner class DataViewHolder(        private val binding: ViewDataBinding,        private val holder: ViewHolderVisitor    ) : RecyclerView.ViewHolder(binding.root) {        fun bind(item: HasStringId, clickListener: AdapterClickListenerById) =            holder.bind(binding, item, clickListener)    }    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder =        LayoutInflater.from(parent.context).run {            val holder = viewHoldersManager.getViewHolder(viewType)            DataViewHolder(DataBindingUtil.inflate(this, holder.layout, parent, false), holder)        }    override fun onBindViewHolder(holder: DataViewHolder, position: Int) = holder.bind(getItem(position), clickListener)    override fun getItemViewType(position: Int): Int = viewHoldersManager.getItemType(getItem(position))}

Сам адаптер создается и наполняется элементами уже в слое view, в данном случае во фрагменте таким нехитрым способом:

// где-то выше во фрагменте:// private val viewModel: MainViewModel by viewModels()// private lateinit var recycler: RecyclerView// @Inject lateinit var viewHoldersManager: ViewHoldersManager// private val items = mutableListOf<HasStringId>()override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        recycler = requireActivity().findViewById(R.id.recycller)        val itemsAdapter = BaseListAdapter(AdapterClickListenerById {}, viewHoldersManager)        itemsAdapter.submitList(items)        recycler.apply {            layoutManager = LinearLayoutManager(requireContext())            addItemDecoration(DividerItemDecoration(requireContext(), (layoutManager as LinearLayoutManager).orientation))            adapter = itemsAdapter        }        populateRecycler()    }private fun populateRecycler() {     lifecycleScope.launch {        viewModel.getItems().flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)           .collect { items.add(it) }     }   }

Благодаря одному "лишнему" классу менеджера вью холдеров и паре интерфейсов мы получили простой, но очень функциональный recycler view с возможностью безболезненного расширения. К тому же в противовес библиотекам у нас остались следующие преимущества:

  • нет необходимости наследовать классы данных от библиотечных супер-классов;

  • нет необходимости оборачивать разные элементы в sealed класс;

  • один и тот же data-класс можно использовать для сериализации/десериализации данных из интернета, сохранения в локальной базе и в качестве модели для view или data биндинга;

  • изменения классов данных и вью-холдеров не влекут изменений кода адаптера;

  • все внутренности элементов инкапсулированы от адаптера, законы SOLID выполняются;

  • отсутствует избыточная функциональность, неизбежно приносимая библиотеками (YAGNI).

Разумеется, моя реализация еще имеет пути для улучшения и расширения. Можно, как в groupie добавить группировку элементов и их визуальное сворачивание. Можно отказаться от data binding или дополнить адаптер вариантами для view binding или обычного инфлейта разметки со всеми любимыми findViewById во вью холдерах. И тогда код превратится в ту же самую библиотеку, которых уже вон сколько и так. Для моих же конкретных целей на тот момент, когда возникла необходимость, варианта с простым Посетителем более, чем достаточно:

Прошу не судить строго, поскольку в мире android это мое первое появление на свет. Полноценный код примера из текста статьи будет доступен в репозитории github.

Подробнее..

Из песочницы Enum и switch, и что с ними не так

04.09.2020 16:11:02 | Автор: admin

image


Часто ли у вас было такое, что вы добавляли новое значение в enum и потом тратили часы на то, чтобы найти все места его использования, а затем добавить новый case, чтобы не получить ArgumentOutOfRangeException во время исполнения?


Идея


Если проблема состоит только в switch операторе и отслеживании новых типов, тогда давайте избавимся от них!


Идея состоит в том, чтобы заменить использование switch паттерном visitor.


Пример 1


Предположим у нас есть какой-то API для работы с документами, от которого мы получаем необходимые данные и определяем его тип, а далее в зависимости от этого типа, необходимо делать различные операции.


Определим файл DocumentType.cs:


public enum DocumentType{    Invoice,    PrepaymentAccount}public interface IDocumentVisitor<out T>{    T VisitInvoice();    T VisitPrepaymentAccount();}public static class DocumentTypeExt{    public static T Accept<T>(this DocumentType self, IDocumentVisitor<T> visitor)    {        switch (self)        {            case DocumentType.Invoice:                return visitor.VisitInvoice();            case DocumentType.PrepaymentAccount:                return visitor.VisitPrepaymentAccount();            default:                throw new ArgumentOutOfRangeException(nameof(self), self, null);        }    }}

И да, я предлагаю определять все связанные типы в одном файле, что не является идиоматичным для .Net разработчика. Но иногда это очень ухудшает упрощает понимание кода.


Опишем visitor который будет искать в базе нужный документ DatabaseSearchVisitor.cs:


public class DatabaseSearchVisitor : IDocumentVisitor<IDocument>{    private ApiId _id;    private Database _db;    public DatabaseSearchVisitor(ApiId id, Database db)    {        _id = id;        _db = db;    }    public IDocument VisitInvoice() => _db.SearchInvoice(_id);    public IDocument VisitPrepaymentAccount() => _db.SearchPrepaymentAccount(_id);}

И потом его использование:


public void UpdateStatus(ApiDoc doc){    var searchVisitor = new DatabaseSearchVisitor(doc.Id, _db);    var databaseDocument = doc.Type.Accept(searchVisitor);    databaseDocument.Status = doc.Status;    _db.SaveChanges();}

Пример 2


У нас есть события, которые выглядят следующим образом:


public enum PurseEventType{    Increase,    Decrease,    Block,    Unlock}public sealed class PurseEvent{    public PurseEventType Type { get; }    public string Json { get; }    public PurseEvent(PurseEventType type, string json)    {        Type = type;        Json = json;    }}

Мы хотим отправлять уведомления пользователю на определенный тип событий. Тогда реализуем visitor:


public interface IPurseEventTypeVisitor<out T>{    T VisitIncrease();    T VisitDecrease();    T VisitBlock();    T VisitUnlock();}public sealed class PurseEventTypeNotificationVisitor : IPurseEventTypeVisitor<Missing>{    private readonly INotificationManager _notificationManager;    private readonly PurseEventParser _eventParser;    private readonly PurseEvent _event;    public PurseEventTypeNotificationVisitor(PurseEvent @event, PurseEventParser eventParser, INotificationManager notificationManager)    {        _notificationManager = notificationManager;        _event = @event;        _eventParser = eventParser;    }    public Missing VisitIncrease() => Missing.Value;    public Missing VisitDecrease() => Missing.Value;    public Missing VisitBlock()    {        var blockEvent = _eventParser.ParseBlock(_event);        _notificationManager.NotifyBlockPurseEvent(blockEvent);        return Missing.Value;    }    public Missing VisitUnlock()    {        var blockEvent = _eventParser.ParseUnlock(_event);        _notificationManager.NotifyUnlockPurseEvent(blockEvent);        return Missing.Value;    }}

Для примера не будем ничего возвращать. Для этого можно воспользоваться типом Missing из System.Reflection или же написать тип Unit. В реальном проекте возвращался бы Result, например, с информацией об ошибке, если такие имеются.


И пример использования:


public void SendNotification(PurseEvent @event){    var notificationVisitor = new PurseEventTypeNotificationVisitor(@event, _eventParser, _notificationManager);    @event.Type.Accept(notificationVisitor);}

Дополнение


Если нужно быстрее


Если нужно использовать такой подход там, где важна производительность, в качестве visitor можно использовать структуры. Тогда код изменится следующим образом.


Метод расширение:


public static T Accept<TVisitor, T>(this DocumentType self, in TVisitor visitor)    where TVisitor : IDocumentVisitor<T>    {        switch (self)        {            case DocumentType.Invoice:                return visitor.VisitInvoice();            case DocumentType.PrepaymentAccount:                return visitor.VisitPrepaymentAccount();            default:                throw new ArgumentOutOfRangeException(nameof(self), self, null);        }    }

Сам visitor остаётся прежним, только меняем class на struct.


И сам код обновления документа выглядит не так удобно, но работает быстро:


public void UpdateStatus(ApiDoc doc){    var searchVisitor = new DatabaseSearchVisitor(doc.Id, _db);    var databaseDocument = doc.Type.Accept<DatabaseSearchVisitor, IDocument>(searchVisitor);    databaseDocument.Status = doc.Status;    _db.SaveChanges();}

При таком использовании generic, необходимо уточнять типы самому, так как компилятор не хочет способен вывести их автоматически.


Читабельность и in-place реализация


Если нужно реализовать логику только в одном месте, то часто visitor громоздко и не удобно. Поэтому есть альтернативное решение match.


Сразу пример со структурой:


public static T Match<T>(this DocumentType self, Func<T> invoiceCase, Func<T> prepaymentAccountCase){    var visitor = new FuncVisitor<T>(invoiceCase, prepaymentCase);    return self.Accept<FuncVisitor<T>, T>(visitor);}

Сам FuncVisitor:


public readonly struct FuncVisitor<T> : IDocumentVisitor<T>{    private readonly Func<T> _invoiceCase;    private readonly Func<T> _prepaymentAccountCase;    public FuncVisitor(Func<T> invoiceCase, Func<T> prepaymentAccountCase)    {        _invoiceCase = invoiceCase;        _prepaymentAccountCase = prepaymentAccountCase;    }    public T VisitInvoice() => _invoiceCase();    public T VisitPrepaymentAccount() => _prepaymentAccountCase();}

Использование match:


public void UpdateStatus(ApiDoc doc){    var databaseDocument = doc.Type.Match(        () => _db.SearchInvoice(doc.Id),        () => _db.SearchPrepaymentAccount(doc.Id)    );    databaseDocument.Status = doc.Status;    _db.SaveChanges();}

Итог


При добавлении нового значения в enum необходимо:


  1. Добавить метод в интерфейс.
  2. Добавить его использование в метод расширение.

Для остальных мест компилятор подскажет нам, где необходимо реализовать новый метод.
Таким образом мы избавляемся от проблемы забытого case в switch.


Это все еще не серебряная пуля, но может здорово помочь в работе с enum.


Ссылки


Подробнее..

Обобщаем паттерн посетитель (С)

10.12.2020 20:17:14 | Автор: admin

Недостатки типичной реализации

В статье намеренно не приведен пример типичной реализации паттерна посетителя в C++.

Если вы не знакомы с этим шаблоном, то вот тут можно с ним ознакомиться.

А если в двух словах, то этот шаблон очень полезен если вам нужно обойти коллекцию из указателей на абстрактный базовый класс, применив к ним какую то операцию в зависимости от типа, который скрывается за абстракцией.

Поэтому перейдем сразу к недостаткам, которые хотели бы устранить.

  • При добавлении класса в иерархию (или удалении), приходится изменить много мест в коде. (Каждый раз лезть и добавлять чисто виртуальную функцию-член в абстрактный класс, и почти во все реализации.)

  • При добавлении нового посетителя, приходится наследоваться от абстрактного класса, даже если мы всего то хотим вызвать просто шаблон функции. При этом мы не можем сделать шаблон виртуальной функции-члена visit у посетителя (причина этого проста, шаблонов виртуальных функций-членов просто нет). Каждую реализацию функции-члена visit приходится делать отдельно.

  • Класс посетителя привязан к предметной области.

Что хотим получить?

  • Имея на руках указатель на абстрактный базовый класс, хотим сразу получать реальный тип объекта и отправлять его самого или его тип в какой-нибудь шаблон функции, не используя при этом никаких конструкций из dynamic_cast или static_cast и не создавая множество одинаковых переопределений виртуальных функций.

  • Простым способом добавлять или удалять классы, которые посетитель может обойти.

  • Не привязанный к предметной области посетитель.

Реализация

Начинаем с создания абстрактного посетителя.

Source:
template< class T >struct AbstractVisitor{    virtual ~AbstractVisitor() = default;    virtual void visit( T& ) = 0;};

(Пояснение: здесь виртуальная функция-член visit не является шаблоном, количество виртуальных функций при инстанцировании класса AbstractVisitor точно известно т.к. T является параметром шаблона класса, а не функции )

Инстанс конкретного посетителя сможет обойти только один тип объекта. Нам же необходимо чтобы посетитель мог обходить несколько различных типов объектов.

Для этого создадим простой список типов TypeList и класс агрегатор AbstractVisitors. Список у AbstractVisitors будет содержать все типы объектов, которые посетитель может обойти.

Source:
template< class ... T >struct TypeList{};template< class T >struct AbstractVisitor{    virtual ~AbstractVisitor() = default;    virtual void visit( T& ) = 0;};template< class ...T >struct AbstractVisitors;template< class ... T >struct AbstractVisitors< TypeList< T... > > : AbstractVisitor< T >...{};

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

Source:
template< class Functor, class ... T >struct Dispatcher;template< class Functor, class ... T >struct Dispatcher< Functor, TypeList< T... > > : AbstractVisitors< TypeList< T... > >{    Dispatcher( Functor functor ) : functor( functor ) {}    Functor functor;};

Теперь необходимо для всех типов из листа переопределить виртуальную функцию-член visit.

Для этого создадим класс Resolver, который этим и будет заниматься. А сам класс Dispatcher унаследуем от всех возможных типов Resolver-ов.

Дополнительно необходимо вызывать функтор в переопределенной функции, воспользуемся (CRTP) и передадим тип Dispatcher как аргумент шаблона во все Resolver.

(Подробнее о том что такое CRTP можно почитать тут).

Source:
template< class Dispatcher, class T >struct Resolver : AbstractVisitor< T >{    void visit( T& obj ) override     {        static_cast< Dispatcher* >( this )->functor( obj );    };};template< class Functor, class ... T >struct Dispatcher< Functor, TypeList< T... > > : AbstractVisitors< TypeList< T... > >, Resolver< Dispatcher< Functor, TypeList< T... > >, T >...{    Dispatcher( Functor functor ) : functor( functor ) {}    Functor functor;};

Вроде все в порядке.

Попробуем создать объект класс Dispatcher. Компилятор начинает ругаться на то, что объект класса Dispatcher абстрактный, как же так?

Причина этого в том, что мы переопределили виртуальные функции для Resolver, но для Dispatcher мы ведь ничего не переопределяли.

Чтобы этого избежать, необходимо сделать наследование от AbstractVisitor<T>виртуальным.(Подробнее о размещении объектов в памяти и виртуальном наследовании можно почитать тут.)

Source:
template< class ... T >struct AbstractVisitors< TypeList< T... > > : virtual AbstractVisitor< T >...{};template< class Dispatcher, class T >struct Resolver : virtual AbstractVisitor< T >{    void visit( T& obj ) override     {        static_cast< Dispatcher* >( this )->functor( obj );    };};

Создадим абстрактный базовый класс (AbstractObject) и какие-нибудь классы (Object1, Object2), которые хотели обойти.

Так же создадим функцию test и шаблон функции test, которые будут получать непосредственно ссылку на объект определенного типа.

Пример использования:

Source:
struct Object1;struct Object2;using ObjectList = TypeList< Object1, Object2 >;struct AbstractObject{    virtual void accept( AbstractVisitors< ObjectList >& visitor ) = 0; };struct Object1 : AbstractObject{    void accept( AbstractVisitors< ObjectList >& visitor ) override     {         static_cast< AbstractVisitor< Object1 >& >( visitor ).visit( *this );      };};struct Object2 : AbstractObject{    void accept( AbstractVisitors< ObjectList >& visitor ) override     {         static_cast< AbstractVisitor< Object2 >& >( visitor ).visit( *this );    };};void test( Object1& obj ){    std::cout << "1" << std::endl;}template< class T >void test( T& obj ){    std::cout << "2" << std::endl;}int main(){    Object1 t1,t2,t3,t4;    Object2 e1,e2,e3;    std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 };    auto l = []( auto& obj ){ test(obj); };    Dispatcher<decltype(l), ObjectList> dispatcher;      for( auto* obj : vector )    {        obj->accept( dispatcher );    }}

(Пояснение: мы не можем просто написать visitor.visit( *this ), это приведет к неоднозначности, если классов в иерархии будет больше двух.)

Строчки на которых создается обобщенная лямбда и объект класса Dispatchable какие-то то страшные и не удобные, спрятать бы все это от глаз.

Так же, хотелось бы спрятать функцию-член accept у AbstractObject, Object1 и Object2, т.к. тело функции для всех типов объектов будет одинаковое, различаться будет только тип объекта.

Для этого создадим абстрактный класс Dispatchable. Cделаем у него чисто виртуальную функцию-член accept и шаблон функции-члена который будет принимать функтор. В нем собственно и будем создавать наш Dispatcher.

Помимо этого создадим макрос DISPATCHED, он понадобится чтобы спрятать переопределение функции-члена accept у Object1 и Object2.

Source:
template< class TypeList >struct Dispatchable{    virtual ~Dispatchable() = default;    virtual void accept( AbstractVisitors< TypeList >& ) = 0;    template< class Functor >    void dispatch( Functor functor )    {        static Dispatcher< decltype(functor), TypeList > dispatcher( functor );        accept( dispatcher );    };};#define DISPATCHED( TYPE, TYPE_LIST ) \    void accept( AbstractVisitors< TYPE_LIST >& visitor ) override \    { \        static_cast< AbstractVisitor< TYPE >& >( visitor ).visit( *this );  \    }

Затем наследуем AbstractObject от класса Dispatchable. А в классы Object1 и Object2 добавляем макрос DISPATCHED.

Source:
struct Object1;struct Object2;using ObjectList = TypeList< Object1, Object2 >;struct AbstractObject : Dispatchable< ObjectList >{};struct Object1 : AbstractObject{    DISPATCHED( Object1, ObjectList )};struct Object2 : AbstractObject{    DISPATCHED( Object2, ObjectList )};

Отлично, мы спрятали все функции-члены accept и вынесли общий код. Вот теперь все готово.

Пример использования:

Source:
void test( Object1& obj ){    std::cout << "1" << std::endl;}template< class T >void test( T& obj ){    std::cout << "2" << std::endl;}int main(){    Object1 t1,t2,t3,t4;    Object2 e1,e2,e3;    std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 };    for( auto* obj : vector )    {        obj->dispatch( []( auto& obj ) { test(obj); } );    }}
Output:

1

2

1

1

2

2

1

Заключение

  • Чтобы добавить или удалить класс, который будет обрабатываться посетителем, достаточно просто изменить список типов.

  • Посетитель не привязан к предметной области, т.к. является шаблоном класса.

  • Можем в полной мере пользоваться шаблонами функций.

Какие недостатки?

  • Дополнительная косвенность, т.к. Dispatcher содержит функтор.

Ссылка на код в compiler explorer.

Full source:
#include <type_traits>#include <iostream>#include <vector>template< class ... T >struct TypeList{};template< class T >struct AbstractVisitor{    virtual ~AbstractVisitor() = default;    virtual void visit( T& ) = 0;};template< class ...T >struct AbstractVisitors;template< class ... T >struct AbstractVisitors< TypeList< T... > > : virtual AbstractVisitor< T >...{};template< class Dispatcher, class T >struct Resolver : virtual AbstractVisitor< T >{    void visit( T& obj ) override     {        static_cast< Dispatcher* >( this )->functor( obj );    };};template< class Functor, class ... T >struct Dispatcher;template< class Functor, class ... T >struct Dispatcher< Functor, TypeList< T... > > : AbstractVisitors< TypeList< T... > >, Resolver< Dispatcher< Functor, TypeList< T... > >, T >...{    Dispatcher( Functor functor ) : functor( functor ) {}    Functor functor;};template< class TypeList >struct Dispatchable{    virtual ~Dispatchable() = default;    virtual void accept( AbstractVisitors< TypeList >& ) = 0;    template< class Functor >    void dispatch( Functor functor )    {        static Dispatcher< decltype(functor), TypeList > dispatcher( functor );        accept( dispatcher );    };};#define DISPATCHED( TYPE, TYPE_LIST ) \    void accept( AbstractVisitors< TYPE_LIST >& visitor ) override \    { \        static_cast< AbstractVisitor< TYPE >& >( visitor ).visit( *this );  \    }struct Object1;struct Object2;using ObjectList = TypeList< Object1, Object2 >;struct AbstractObject : Dispatchable< ObjectList >{};struct Object1 : AbstractObject{    DISPATCHED( Object1, ObjectList )};struct Object2 : AbstractObject{    DISPATCHED( Object2, ObjectList )};void test( Object1& obj ){    std::cout << "1" << std::endl;}template< class T >void test( T& obj ){    std::cout << "2" << std::endl;}int main(){    Object1 t1,t2,t3,t4;    Object2 e1,e2,e3;    std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 };    for( auto* obj : vector )    {        obj->dispatch( []( auto& obj ) { test(obj); } );    }}
Подробнее..
Категории: C++ , Crtp , Visitor , Lambda functions , Generics , Templates , Pattern

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru