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

Піддавшись загальній істерії на хабре,

(а саме «Попереджаю Нерухомість Змінився» переводить Гуглекс всіма улюблений «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
{
new public 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)) — оптимально за всіма показниками, крім того що це потрібно прописувати РУКАМИ. Бо!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.