Design Pattern Visitor — Посетитель

Одиннадцатый шаблон проектирования в серии.

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

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

От слов к делу.

Наше приложение будет чем-то наподобие интернет магазина, где можно класть в корзину продукты и в конечном итоге выписать некий чек с детальной информацией по каждому продукту и количеству.

Для начала выделим интерфейс продукта

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

interface Good {

    var quantity: Int

    fun getName(): String

    fun getPrice(): Int

    fun getDescription(): String
}

Создадим 3 товара, которые реализуют данный интерфейс. Здесь для упрощения работы выделим абстрактный класс, который по умолчанию хранит 0 штук товара.

abstract class AbstractGood(override var quantity: Int = 0) : Good
class Telephone : AbstractGood() {

    override fun getName(): String {
        return "Telephone N1"
    }

    override fun getPrice(): Int {
        return 10
    }

    override fun getDescription(): String {
        return "Simple telephone for home and office"
    }
}
class MediaPlayer : AbstractGood() {

    override fun getPrice(): Int {
        return 5
    }

    override fun getDescription(): String {
        return "MediaPlayer with USB, supports all well-known audio formats"
    }

    override fun getName(): String {
        return "MediaPLayer N2"
    }
}
class Radio : AbstractGood() {

    override fun getName(): String {
       return "Radio N3"
    }

    override fun getPrice(): Int {
        return 2
    }

    override fun getDescription(): String {
        return "Portable radio station, supports all MHz"
    }
}

Определив наименования, цену и описание товара нашишем интерфейс посетителя, который будет иметь методы под конкретные виды товара.

interface Visitor {

    fun visitTelephone(telephone: Telephone): String

    fun visitMediaPlayer(mediaPlayer: MediaPlayer): String

    fun visitRadio(radio: Radio): String

    fun incrementQuantity(good: Good)
}

Здесь для упрощения работы выделили метод для увеличения количества товара, можно также добавить метод уменьшения или же выделить в иной интерфейс и класс для большей чистоты.

Взглянем на реализацию интерфейса.

class GoodVisitor : Visitor {
    
    override fun incrementQuantity(good: Good) {
        good.quantity++
    }

    override fun visitTelephone(telephone: Telephone): String {
        return "Telephone. \n" + toString(telephone)
    }

    override fun visitMediaPlayer(mediaPlayer: MediaPlayer): String {
        return "MediaPlayer. \n" + toString(mediaPlayer)
    }

    override fun visitRadio(radio: Radio): String {
        return "Radio. \n" + toString(radio)
    }

    private fun toString(good: Good): String {
        val price: Int = good.getPrice()
        return "Name: " + good.getName() +
                ", description: " + good.getDescription() +
                ", quantity: " + good.quantity +
                ", price: $" + price +
                ", total: $" + good.quantity * price
    }
}

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

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/goodsRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/calculateCheckButton"
     app:layoutManager="android.support.v7.widget.LinearLayoutManager" />

    <Button
        android:text="show check"
        android:id="@+id/calculateCheckButton"
        style="@style/Base.Widget.AppCompat.Button.Borderless"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true" />

</RelativeLayout>

Накидаем адаптер и вьюхолдер для отображения товаров.

class GoodViewHolder(view: View, private val clickListener: GoodClickListener) : RecyclerView.ViewHolder(view) {

    private val nameTextView = view.goodNameTextView
    private val addButton = view.addGoodButton

    fun bind(good: Good) {
        val text = "${good.getName()}\n$${good.getPrice()}\n${good.getDescription()}"
        nameTextView.text = text
        addButton.setOnClickListener { clickListener.onGoodClick(good) }
    }
}

interface GoodClickListener {

    fun onGoodClick(good: Good)
}
class GoodsAdapter(private val goods: List<Good>, private val clickListener: GoodClickListener) : RecyclerView.Adapter<GoodViewHolder>() {

    override fun onCreateViewHolder(viewGroup: ViewGroup, position: Int): GoodViewHolder {
        val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.good_layout, viewGroup, false)
        return GoodViewHolder(view, clickListener)
    }

    override fun getItemCount(): Int {
        return goods.size
    }

    override fun onBindViewHolder(holder: GoodViewHolder, position: Int) {
        holder.bind(goods[position])
    }
}

Здесь все просто как 2*2. Текстовое поле, кнопка для добавления и разделитель на дне каждого элемента.

И приступим к самому важному. Коду главной страницы.

class MainActivity : AppCompatActivity(), GoodClickListener {

    private val telephone = Telephone()
    private val mediaPlayer = MediaPlayer()
    private val radio = Radio()

    private val goodVisitor = GoodVisitor()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        goodsRecyclerView.adapter = GoodsAdapter(getGoods(), this)

        calculateCheckButton.setOnClickListener {
            val check = goodVisitor.visitTelephone(telephone) + "\n" +
                    goodVisitor.visitMediaPlayer(mediaPlayer) + "\n" +
                    goodVisitor.visitRadio(radio)
            Toast.makeText(this, check, Toast.LENGTH_LONG).show()
        }
    }

    override fun onGoodClick(good: Good) {
        goodVisitor.incrementQuantity(good)
        Toast.makeText(this, "Good added", Toast.LENGTH_SHORT).show()
    }

    private fun getGoods(): List<Good> {
        return listOf(telephone, mediaPlayer, radio)
    }
}

Здесь мы используем котлин синтетик для простого доступа к вью элементам. Создаем категории товаров простым путем (здесь не будем углубляться в чистоту решения, без репозиториев и т.д.).

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

Главный экран с товарами.
Нажаие на кнопку +
Нажатие на кнопку отображения чека.

В данном случае можно было бы сделать абстрактный класс, указав ему поля и переопределив например метод toString(). Но опять же, цель у нас была показать как обойти множественное наследование применив для этого шаблон проектирования Посетитель.

Запись опубликована в рубрике Программирование Android, Программирование Kotlin. Добавьте в закладки постоянную ссылку.