Чистый код под флагом АОП и ненавистный #ПредупреждаюНедвижимостьИзменился

в 15:53, , рубрики: .net, C#, inotifypropertychanged, Proxy pattern, Анализ и проектирование систем, метки: , ,

Поддавшись общей истерии на хабре,
Чистый код под флагом АОП и ненавистный #ПредупреждаюНедвижимостьИзменился - 1
(а именно «Предупреждаю Недвижимость Изменился» переводит Гуглекс всеми любимый «I Notify Property Changed») по поводу уведомлений об изменении. Я решил посмотреть на сколько далеко продвинулось человечество в изобретении велосипедов.

OnPropertyChanged(«Property»)
OnPropertyChanged(()=>Property)
SetProperty(ref storage, value)
[CallerMemberName]
[Magic]
АОП разных мастей
даже предлагают roslyn.codeplex.com/discussions/550266 почему бы м нет.
Круче всех все же nameof(Property) — осторожно си шарп 6.
Последствия истерии выражаются в следующих работах
habrahabr.ru/post/199378
habrahabr.ru/post/246469
habrahabr.ru/post/95211
Лично меня устраивает варианты OnPropertyChanged(nameof(Property)) и OnPropertyChanged(()=>Property), первый вариант работает быстрее.
Но чаще всего я использую SetProperty(ref storage, value), коробочный вариант BindableBase.
Хабражитель Scrooge2 запостил

Хватит изобреать велосипеды.
Использовать обычный INotifyPropertyChanged руки не отпадут, без всяких но.

Полностью поддерживаю, но… НЕТ.
Ну что же, напильник и кувалда.
Я за чистый код! Логирование, перехваты исключений, проверка прав доступа, всевозможные уведомления и т.д. — задымляют код.Очистить код позволят древние знания шаблона Proxy, например habrahabr.ru/post/88722.
Осложню себе жизнь и буду использовать классы а не интерфейсы.

public class Data
    {
        public virtual int Value { get; set; }

        public virtual string Source { get; set; }
    }

Уведомления, ничего личного

class BindableObject : BindableBase
    {
        public new bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            return base.SetProperty<T>(ref storage, value, propertyName);
        }
    }

И сам заместитель, взявший на себя всю грязную работу

public class ProxyData : Data, INotifyPropertyChanged
    {
        Data _target;
        BindableObject _notifier;

        public ProxyData(Data target)
        {
            _target = target;
            _notifier = new BindableObject();
        }

        public override int Value
        {

            set
            {
                int newValue = 0;

                if (_notifier.SetProperty(ref newValue, value))
                    base.Value = newValue;
            }
        }

        public override string Source
        {

            set
            {
                string newSource = null;

                if (_notifier.SetProperty(ref newSource, value))
                    base.Source = newSource;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _notifier.PropertyChanged += value; }
            remove { _notifier.PropertyChanged -= value; }
        }
    }

Создам консольное приложение, где как не в консоли проверять ПредупреждаюНедвижимостьИзменился

data = new ProxyData(new Data());
(data as INotifyPropertyChanged).PropertyChanged += (s, e) => { Console.WriteLine(string.Format("Property {0} changed!", e.PropertyName)); };
             

data.Value = 10;
data.Source = "List";

Плюсы: чистейший класс Data, все просто и понятно, можно создать кучу разных прокси logger, access и тд, скомбинировать, безопасно удалять и добавлять разный функционал не относящийся к непосредственной работе приложения.
Минусы: Бесполезно, это подмена понятий вьюмодель я практически превратил в модель, получив еще 1 звено шаблона Model -> ViewModel -> Proxy -> View, одно из назначений ViewModel — уведомление, конечно в vm можно оставить подготовку данных… Плюс ко всему кода стало еще больше, хотя вроде как ответственность vm снизилась, ох solid SOLID.
Истерия! Пришло время АОП, про аоп написано довольно много и на теории я останавливаться не буду.
я работаю с IUnityContainer, т.к. его можно считать коробочным, хорошо взаимодействует с Prism.
А вот и универсальное поведение уведомлятора, жуть

class NotifyPropertyChangedBehavior : IInterceptionBehavior, INotifyPropertyChanged
    {
        static readonly MethodBase _add;
        static readonly MethodBase _remove;

        static NotifyPropertyChangedBehavior()
        {
            var methods = typeof(INotifyPropertyChanged).GetMethods();
            _add = methods[0];
            _remove = methods[1];
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            IMethodReturn result = null;
            if (IsPropertyChanged(input))
                if (SubscribeUnsubscribe(input))
                    result = input.CreateMethodReturn(null);
                else
                {
                    PropertyInfo property;

                    if (IsSetMethodCalled(out property, input))
                        result = SetValue(property, input, getNext);
                } 
            return result ?? getNext()(input, getNext);
        }

        public bool WillExecute
        {
            get { return true; }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Проверка вызова мутатора
        /// </summary>
        /// <returns></returns>
        bool IsSetMethodCalled(out PropertyInfo property, IMethodInvocation input)
        {
            string propertyName = input.MethodBase.Name.TrimStart("set_".ToArray());
            property = input.Target.GetType().GetProperty(propertyName);

            return property != null;
        }

        /// <summary>
        /// Установить 
        /// </summary>
        /// <returns></returns>
        IMethodReturn SetValue(PropertyInfo property, IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            var oldValue = property.GetValue(input.Target, new object[0]);
            var newValue = input.Arguments[0];
            IMethodReturn result = null;
            //обновление только если пришло действительно новое значение
            if (!Equals(oldValue, newValue))
            {
                result = getNext()(input, getNext);
                if (PropertyChanged != null)
                    PropertyChanged(input.Target, new PropertyChangedEventArgs(property.Name));
            }
            else result = input.CreateMethodReturn(null);            

            return result;
        }

        /// <summary>
        /// Проверка вызова методов INotifyPropertyChanged
        /// </summary>
        bool SubscribeUnsubscribe(IMethodInvocation input)
        {
            if (input.MethodBase == _add)
            {
                PropertyChanged += (PropertyChangedEventHandler)input.Arguments[0];
                return true;
            }
            else if (input.MethodBase == _remove)
            {
                PropertyChanged -= (PropertyChangedEventHandler)input.Arguments[0];
                return true;
            }
            return false;
        }

        /// <summary>
        /// Вызов на экземпляре реализующим INotifyPropertyChanged
        /// </summary>
        bool IsPropertyChanged(IMethodInvocation input) { return input.Target is INotifyPropertyChanged; }
    }

ну и кусок кода, где все это связывается

IUnityContainer container = new UnityContainer();

container.AddNewExtension<Interception>();

container.RegisterType<Data>(new Interceptor<VirtualMethodInterceptor>(),
                new InterceptionBehavior<NotifyPropertyChangedBehavior>()
                , new AdditionalInterface<INotifyPropertyChanged>());
var data = container.Resolve<Data>();

(data as INotifyPropertyChanged).PropertyChanged += (s, e) => { Console.WriteLine(string.Format("Property {0} changed!", e.PropertyName)); };

             

data.Value = 10;
data.Source = "List";
data.Value = 10;

Console.ReadKey();

Result ----->
image
плюшка для особо ленивых

public static class Extensions
    {
        public static IUnityContainer RegisterViewModel<T>(this IUnityContainer container) where T : class
        {
            container.AddNewExtension<Interception>();

            return container.RegisterType<T>(new Interceptor<VirtualMethodInterceptor>(),
                new InterceptionBehavior<NotifyPropertyChangedBehavior>()
                , new AdditionalInterface<INotifyPropertyChanged>());
        }
    }

минимизация регистрации

container.RegisterViewModel<Data>();

Нельзя оставлять иллюзий, в основе все тот же ЗАМЕСТИТЕЛЬ, только мы о нем не знаем ТсссССсссссс.
Плюсы: теперь можно сделать VM из всего у чего есть виртуальные свойства, полная минимизация кода, чистый класс без ПредупреждаюНедвижимостьИзменился, всяких атрибутов и тому подобных техномагий.
Минусы: совершенно не просто, нужно разбираться в контейнере и его возможностях аоп, не явность в чистом виде, воспринимаемая как магия, куча рефликсии — это не для слабонервных, просев по производительности.
Проведя небольшой эксперимент на сотрудниках, результат плачевный, программисты испытывали боль когда я попросил разобраться как это работает.
Но оно работает.
В целом внедрять это не стали и чуть не придали праведному огню, аоп это круто, но все зависит от уровня команды и их стремлению к заморочкам.

P.S. Всем кого пугает эта жесть рекомендую OnPropertyChanged(nameof(Property)) — оптимально по всем показателям, кроме того что это нужно прописывать РУКАМИ. Бу!

Автор: InWake

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js