Шаблон наблюдатель в Unity

Tulenber 26 June, 2020 ⸱ Intermediate ⸱ 4 min ⸱ 2019.4.0f1 ⸱

Ещё один важный(а какие они ещё бывают) шаблон в нашу копилку.

Наблюдатель - это незаменимый шаблон для построения лёгких малосвязанных систем, который может сильно упростить жизнь, с точки зрения архитектуры. Фактически мы уже сталкивались с применением наблюдателя, когда рассматривали новую систему ввода, но не заостряли на этом внимание. Очень часто его применяют при построении UI, подписывая обновление визуальной части на оповещения об изменениях в логике. В Unity есть несколько вариантов для реализации этого шаблона и, честно говоря, у неподготовленного пользователя они могут вызвать некоторую путаницу, так что рассмотрим все начиная с основ.

Теория

Грубо говоря, наблюдатель делит мир на два типа объектов: издатели и подписчики. Подписчики могут подписаться(и отписаться) на получение уведомления о событии от издателя, а издатель оповещает о событии всех подписавшихся. Таким образом, создаётся возможность взаимодействия между объектами без жёсткой связи между ними. Как всегда, больше теории можно найти в этой статье или этой главе книги Роберта Нистрома.

Реализация

Основным механизмом, реализующим наблюдателя, служит ключевое слово delegate, также для обеспечения большей безопасности Unity(если быть точнее в .Net) предоставляет объекты типа event.

Delegate

Можно сказать, что delegate это тип переменной, которая указывает на функцию, а не на значение переменной. Если вы когда-либо слышали о C++, то фактически это указатель на функцию.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DelegateTest {
    // Объявление указателя на функцию типа void с аргументом типа string
    public delegate void Print(string val);

    public DelegateTest()
    {
        // Присвоение переменной p функции, имеющей описанную ранее сигнатуру
        Print p = PrintValue;
        // Добавление ещё одного подписчика для выполнения
        p += PrintData;
        // Вызов всех подписчиков p
        p("Delegate");
    }

    private void PrintData(string s)
    {
        Console.WriteLine("PrintData " + s);
    }

    public void PrintValue(string s)
    {
        Console.WriteLine("PrintValue " + s);
    }
}

Результат выполнения:

1
2
PrintValue Delegate
PrintData Delegate

System.Action

Также следует познакомиться с классом System.Action, который является не чем иным, как определением public delegate void Action(); и позволяет обойтись без определения делегата на функцию без возвращаемых параметров. В случае необходимости аргументы функции можно задать с помощью дженериков Action<T, …>, это означает, что вы можете перечислить необходимое количество аргументов функции с заданными типами (см. код в следующем примере).

Event

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EventTest {
    // Объявление эвента
    // Action<string> - объявлен в пакете System как public delegate void Action<in T>(T obj);
    // Присвоение пустого делегата delegate { } позволит избежать дополнительной проверки на null при вызове эвента, так как при отсутствии слушателей вызов выкинет NullReferenceException
    public event Action<string> print = delegate { };

    public DelegateTest()
    {
        // Добавление подписчика
        print += PrintValue;
        // Добавление подписчика
        print += PrintData;
        // Вызов всех подписчиков
        p("Event");
    }

    private void PrintData(string s)
    {
        Console.WriteLine("PrintData " + s);
    }

    public void PrintValue(string s)
    {
        Console.WriteLine("PrintValue " + s);
    }
}

Результат выполнения:

1
2
PrintValue Event
PrintData Event

Static event

Нетрудно заметить, что использование комбинации public static event Action print = delegate { }; может стать хорошей альтернативой синглетону, в случае когда вам достаточно механизма оповещения подписчиков.

Замечание

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

Заключение

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



Privacy policyCookie policyTerms of service
Tulenber 2020