Olá pessoal, tudo certo?
Depois de alguns dias falando sobre conceitos, escrevendo muito texto e pouco código, resolvi fazer algo diferente. O post de hoje será predominantemente código. É simples e mais direto. Espero que gostem.
O que proponho hoje é uma abstração. Estou considerando a possibilidade de “extrair” comportamentos dos objetos, deixando-os mais simples e diretos. Todos os comportamentos são escritos em classes “utilitárias” que geram objetos que se “anexam” nos objetos básicos. Todo o conceito que apresento hoje é uma abstração dos Behaviors do Silverlight (criados para o Expression Blend).
Sem mais delongas…
Escrevendo objetos com pontos de extensão
Minha proposta, para o post de hoje, é escrever classes de modelo com pouca ou nenhuma validação. Em minha proposta de hoje, todo comportamento “extendido” é anexado aos objetos do modelo.
Escrever código “extensível” é, de algumas formas, trabalhoso. O exemplo que apresento hoje dá uma boa idéia do que considero necessário para escrever objetos que aceitem extensão de comportamento. Observe:
public class Size : INotifyPropertyChanged, INotifyPropertyChanging { double WidthField = 0d; public double Width { get { return this.WidthField; } set { if (value == this.WidthField) return; this.RaisePropertyChanging("Width", this.WidthField, value); this.WidthField = value; this.RaisePropertyChanged("Width"); } } double HeightField = 0d; public double Height { get { return this.HeightField; } set { if (value == this.HeightField) return; this.RaisePropertyChanging("Height", this.HeightField, value); this.HeightField = value; this.RaisePropertyChanged("Height"); } } double DepthField = 0d; public double Depth { get { return this.DepthField; } set { if (value == this.DepthField) return; this.RaisePropertyChanging("Depth", this.DepthField, value); this.DepthField = value; this.RaisePropertyChanged("Depth"); } } public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string propertyName) { this.OnPropertyChanged(propertyName); if (this.PropertyChanged == null) return; this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } protected virtual void OnPropertyChanged (string propertyName) {} public event PropertyChangingEventHandler PropertyChanging; private void RaisePropertyChanging< T >(string propertyName, T oldValue, T newValue) { this.OnPropertyChanging(propertyName, oldValue, newValue); if (this.PropertyChanging == null) return; this.PropertyChanging(this, new PropertyChangingEventArgs< T >(propertyName, oldValue, newValue)); } protected virtual void OnPropertyChanging< T > (string propertyName, T oldValue, T newValue) {} public double GetValue(string propertyName) { if (propertyName == "Width") return WidthField; if (propertyName == "Height") return HeightField; if (propertyName == "Depth") return DepthField; throw new ArgumentOutOfRangeException(); } public void SetValue(string propertyName, double value) { if (propertyName == "Width") Width = value; else if (propertyName == "Height") Height = value; else if (propertyName == "Depth") Depth = value; else throw new ArgumentOutOfRangeException(); } public override string ToString() { return string.Format("{0};{1};{2}", this.WidthField, this.HeightField, this.DepthField); } }
Observe como todas as propriedades disparam eventos de início e fim de alteração. Esses eventos permitem a objetos externos saibam o que está acontecendo e, eventualmente, façam alguma interferência.
Repare que o próprio framework oferece interfaces públicas para esse propósito. Pessoalmente, consideraria interessante cirar métodos “OnXChanging” e “OnXChanged” para cada propriedade, além de eventos adequados.
Outro artifício que considero bacana é a criação de métodos genéricos para Get e Set.
Para contornar uma limitação que considero grave do framework, extendo a classe “args” com propriedades que indicam melhor o estado do objeto e as alteraçãoes que estão sendo efetuadas. Observe:
public class PropertyChangingEventArgs< TProperty > : PropertyChangingEventArgs { public TProperty OldValue { get; private set; } public TProperty NewValue { get; private set; } public PropertyChangingEventArgs( string propertyName, TProperty oldValue, TProperty newValue) : base (propertyName) { this.OldValue = oldValue; this.NewValue = newValue; } }
Perceba como esse tipo simples pode colaborar em “objetos externos”.
Reproduzindo o conceito de Behaviors do Silverlight (um belo conceito)
Todo modelo desenvolvido pode ser, potencialmente, extendido por comportamentos. Considero muito importante “desenhar” um mecanismo padrão para “plugar” comportamentos a objetos.
Eis minha proposta: Tudo começa com uma interface simples que define as operações de “ligar/desligar” um comportamento. Observe:
public interface IAttachedObject< T > { void Attach(T obj); void Dettach(T obj); }
Essa interface define o fundamento para um objeto de comportamento.
public abstract class Behavior< T > : IAttachedObject< T > { public void Attach(T obj) { if (obj == null) return; this.OnAttach(obj); } protected virtual void OnAttach(T obj) { return; } public void Dettach(T obj) { if (obj == null) return; this.OnDettach(obj); } protected virtual void OnDettach(T obj) { return; } }
Essa é, essencialmente, a idéia desenvolvida para os Behaviors que vemos no Silverlight. Ou seja, nada aqui é novidade.
Repare como foi simples reproduzir o modelo.
Escrevendo um comportamento simples
A partir do modelo simples que desenvolvemos, escrevemos um comportamento, também, muito simples.
public class AcceptsOnlyPositiveValues : Behavior< Size > { protected override void OnAttach(Size obj) { obj.PropertyChanging += PropertyChanging; } void PropertyChanging(object sender, PropertyChangingEventArgs e) { var eT = (PropertyChangingEventArgs< double >)e; if (eT.NewValue <= 0) throw new InvalidOperationException( e.PropertyName + " accepts only positive values"); } }
Repare como o modelo é fácil “anexar” esse comportamento a nosso modelo:
var s = new Size(); var b = new AcceptsOnlyPositiveValues(); b.Attach(s);
Nosso modelo simples, original, passou a não aceitar mais valores negativos em nenhuma de suas propriedades.
Um comportamento mais “complexo”
Para fechar nosso post de hoje, mostro um comportamento mais complexo. A idéia é fazer com que, ao alterar uma medida, as demais sejam atualizadas proporcionalmente. Observe:
public class MantainAspectRatioBehavior : Behavior< Size > { string[] Properties = { "Width", "Height", "Depth" }; private int GetPropertyIndex(string propertyName) { for (int i = 0; i < Properties.Length; i++) if (propertyName == Properties[i]) return i; return -1; } public Size TargetObject { get; private set; } protected override void OnAttach(Size obj) { if (this.TargetObject != null) throw new InvalidOperationException(); this.TargetObject = obj; obj.PropertyChanging += PropertyChanging; obj.PropertyChanged += PropertyChanged; base.OnAttach(obj); } protected override void OnDettach(Size obj) { if (this.TargetObject != obj) throw new InvalidOperationException(); obj.PropertyChanging -= PropertyChanging; obj.PropertyChanged -= PropertyChanged; base.OnDettach(obj); } int WorkingOn = -1; double oldvalue; void PropertyChanging(object sender, PropertyChangingEventArgs e) { if (!this.Properties.Contains(e.PropertyName)) return; bool busy = !( WorkingOn == -1 || WorkingOn == GetPropertyIndex(e.PropertyName) ); if (busy) return; WorkingOn = GetPropertyIndex(e.PropertyName); oldvalue = GetValue(sender as Size, WorkingOn); } void PropertyChanged(object sender, PropertyChangedEventArgs e) { if (!this.Properties.Contains(e.PropertyName)) return; if (GetPropertyIndex(e.PropertyName) != WorkingOn) return; var s = sender as Size; var newValue = s.GetValue(e.PropertyName); var ratio = newValue / oldvalue; var p = WorkingOn; SetValue(s, (p + 1) % 3, GetValue(s, (p + 1) % 3) * ratio); SetValue(s, (p + 2) % 3, GetValue(s, (p + 2) % 3) * ratio); WorkingOn = -1; } double GetValue(Size s, int propertyIndex) { return s.GetValue(Properties[propertyIndex]); } void SetValue(Size s, int propertyIndex, double value) { s.SetValue(Properties[propertyIndex], value); } }
Perceba que esse comportramento não suporta mais de um objeto “target” sendo tratado ao mesmo tempo.
Behaviors podem ser mesmo divertidos, não acha?!
Por hoje, era isso
Márcio Fábio Althmann
março 9, 2011
Cara sensacional o post, como conversamos no msn e colocou no Twitter poderia chamar “Fechado para modificações, aberto para extensões” hehe, e os Behaviors do Silverlight são ótimos.
Parabéns pelo post.
Eleriane Cristina costa
março 9, 2011
Muito show.. e divertido..rs
Anotado para olhar com carinho
Eleriane