ArcObjects e Class Extensions #2
Boa tarde pessoal,
Lembram do artigo #1 sobre as class extensions? Falei um pouco sobre como as CEs funcionam? Dê lida aqui antes de ler este aqui.
Bem, as class extensions são um pouco difíceis de se manter, pois elas geram muito código, são controladas de forma esotérica (através de IPropertySet’s) e podem controlar alguns eventos importantes, como OnChange, OnCreate e OnDelete bem como as validações de uma feição.
Portanto, qual é uma maneira um pouco mais sã de construí-las? Uma primeira tentativa que fiz, foi utilizar herança para construí-las. Uma classe base com algumas funcionalidades era herdada por classes mais “abaixo” na cadeia e ia herdando funcionalidade. Mas como dizem por aí, composição é melhor que herança para extensão de função.
Na figura acima, percebemos que toda a funcionalidade é herdada através de herança. Todas as operações em OldBaseClassExtension (a mãe de todas) são marcadas como virtual, dando a possibilidade de sobreescrevemos o método pela classe filha.
Em código temos algo assim:
public interface IOldClassExtension : IClassExtension, IObjectClassExtension, IObjectClassValidation, IObjectClassEvents
{
// aqui dizemos que todas as classes derivadas de IOldClassExtension
// devem implementar os métodos das interfaces correspondentes.
}
public class OldBaseClassExtension:IOldClassExtension
{
public virtual void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
throw new NotImplementedException();
}
public virtual void Shutdown()
{
throw new NotImplementedException();
}
public virtual string ValidateField(IRow Row, string FieldName)
{
throw new NotImplementedException();
}
public virtual string ValidateRow(IRow Row)
{
throw new NotImplementedException();
}
public virtual void OnChange(IObject obj)
{
throw new NotImplementedException();
}
public virtual void OnCreate(IObject obj)
{
throw new NotImplementedException();
}
public virtual void OnDelete(IObject obj)
{
throw new NotImplementedException();
}
}
public class OldConcreteClassExtensionA : OldBaseClassExtension
{
public override void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
base.Init(ClassHelper, ExtensionProperties);
// nova funcionalidade customizada
}
public override void Shutdown()
{
base.Shutdown();
// nova funcionalidade customizada
}
public override string ValidateField(IRow Row, string FieldName)
{
base.ValidateField(Row, FieldName);
// nova funcionalidade customizada
return "resultado de nova validação de campo";
}
public override string ValidateRow(IRow Row)
{
base.ValidateRow(Row);
// nova funcionalidade customizada
return "resultado de nova validação de linha";
}
public override void OnChange(IObject obj)
{
base.OnChange(obj);
// nova funcionalidade customizada
}
public override void OnCreate(IObject obj)
{
base.OnCreate(obj);
// nova funcionalidade customizada
}
public override void OnDelete(IObject obj)
{
base.OnDelete(obj);
// nova funcionalidade customizada
}
}
public class OldConcreteClassExtensionB : OldBaseClassExtension
{
public override void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
base.Init(ClassHelper, ExtensionProperties);
// nova funcionalidade customizada
}
public override void Shutdown()
{
base.Shutdown();
// nova funcionalidade customizada
}
public override string ValidateField(IRow Row, string FieldName)
{
base.ValidateField(Row, FieldName);
// nova funcionalidade customizada
return "resultado de nova validação de campo";
}
public override string ValidateRow(IRow Row)
{
base.ValidateRow(Row);
// nova funcionalidade customizada
return "resultado de nova validação de linha";
}
public override void OnChange(IObject obj)
{
base.OnChange(obj);
// nova funcionalidade customizada
}
public override void OnCreate(IObject obj)
{
base.OnCreate(obj);
// nova funcionalidade customizada
}
public override void OnDelete(IObject obj)
{
base.OnDelete(obj);
// nova funcionalidade customizada
}
}
public class OldConcreteClassExtensionC : OldConcreteClassExtensionA
{
public override void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
base.Init(ClassHelper, ExtensionProperties);
// nova funcionalidade customizada
}
public override void Shutdown()
{
base.Shutdown();
// nova funcionalidade customizada
}
public override string ValidateField(IRow Row, string FieldName)
{
base.ValidateField(Row, FieldName);
// nova funcionalidade customizada
return "resultado de nova validação de campo";
}
public override string ValidateRow(IRow Row)
{
base.ValidateRow(Row);
// nova funcionalidade customizada
return "resultado de nova validação de linha";
}
public override void OnChange(IObject obj)
{
base.OnChange(obj);
// nova funcionalidade customizada
}
public override void OnCreate(IObject obj)
{
base.OnCreate(obj);
// nova funcionalidade customizada
}
public override void OnDelete(IObject obj)
{
base.OnDelete(obj);
// nova funcionalidade customizada
}
}
Perceba como determinamos a funcionalidade nas classes filhas: além de herdarmos e sobreescrevermos o método, chamamos também o método base, através de base.OnChange(obj), por exemplo.
Ok, funciona, mas quando temos algumas Class Extensions e uma hierarquia com dois ou três níveis já se torna impossível de manter e debugar o código. Demora-se muito para atingir um resultado razoável.
Como faríamos se usássemos composição? Bem, vamos imaginar o seguinte: temos algumas tabelas que descrevem as class extensions de acordo com sua funcionalidade básica (nem todas as CEs precisam implementar IObjectClassValidation, caso não às usem) e uma forma de ler e instanciar estas CEs de acordo com esta tabela. O diagrama de classes seria basicamente este aqui:
Desta maneira temos um código muito mais simples e nenhuma dependência entre cada ClassExtension. Todas as CEs implementariam ICustomClassExtension, pois esta dá visibilidade para todos os eventos / validações (poderíiamos até ter implementado IObjectInspector para dar formas diferentes de edição à cada CE) que precisamos e o sistema faria o restante, consultado o banco de dados (poderia esta dentro de um próprio FileGeodatabase/ArcSDE) e descobrindo à ordem e quais CEs devem ser instanciadas.
O esquema em SQL ficaria parecido com:
CREATE TABLE ENTIDADES ( -- conterá uma lista de feature classes ou de tabelas do ArcGIS nome_entidade varchar(128) NOT NULL, -- Lote nome_feature_dataset varchar(128) NOT NULL DEFAULT '', -- nome do feature dataset, caso exista nome_feature_class varchar(128) NOT NULL, -- nome da feature class, ex: FLTE_LOTES CONSTRAINT entidades_pk PRIMARY KEY (nome_entidade) ) CREATE TABLE CLASS_EXTENSIONS ( -- conterá uma lista das class extensions disponíveis em determinada assembly. nome_class_extension varchar(128) NOT NULL, -- Validar Geometria de Lote descricao_namespace varchar(128) NOT NULL, -- ValidateParcelGeometryExtensions CONSTRAINT class_extensions_pk PRIMARY KEY (nome_class_extension), CONSTRAINT descricao_namespace_un UNIQUE (descricao_namespace) ) -- tabela de relacionamento entre entidades e class extensions -- é um relacionamento M:N CREATE TABLE ENTIDADE_POSSUI_CLASS_EXTENSION ( nome_entidade varchar(128) NOT NULL REFERENCES ENTIDADES on (nome_entidade), nome_class_extension varchar(128) NOT NULL REFERENCES CLASS_EXTENSIONS on (nome_class_extension), ordem_criacao integer NOT NULL DEFAULT 0, CONSTRAINT epc_pk PRIMARY KEY (nome_entidade,nome_class_extension,ordem_criacao) )
Através de um pouco de código C#, podemos descobrir se a classe implementada está disponível, instanciá-la dinamicamente e adicioná-la à uma lista de ClassExtensions (propriedade extensions em ICustomClassExtension). Aqui segue um pouco de código para vocês verem a diferença. Dessa forma cada classe cuida só do que ela precisa e podemos compor dinamicamente class extensions poderosas, apenas alterando os registros presentes no banco de dados e contando que os campos manipulados por cada uma exista na feature class.
public interface ICustomClassExtension:IClassExtension,IObjectClassExtension,IObjectClassEvents,IObjectClassValidation
{
List extensions { get; set; }
void CreateExtensions();
}
// esta é a classe mais importante. ela provê toda a funcionalidade
// básica de buscar o banco de dados e encontrar quem são as extensões para
// cada feature class e como usá-las
public class CustomClassExtension:ICustomClassExtension
{
public List extensions
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public void CreateExtensions()
{
// vá ao banco de dados e descubra quem são as class extensions aplicáveis à esta classe
/*
* foreach (Record r in Query)
* {
* Type t = Type.GetType(r.ClassExtensionAssemblyName) // ValidateParcelGeometry
* extensions.Add(Activator.CreateInstance(t));
* }
*/
}
public void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
foreach (IClassExtension classEx in this.extensions)
{
// todas as classes implementam init
classEx.Init(ClassHelper,ExtensionProperties);
}
}
public void Shutdown()
{
foreach (IClassExtension classEx in this.extensions)
{
classEx.Shutdown();
}
}
public void OnChange(IObject obj)
{
// so vou conseguir iterar sobre as classes que implementam IObjectClassEvents
foreach (IObjectClassEvents classEvents in this.extensions)
{
classEvents.OnChange(obj);
}
}
public void OnCreate(IObject obj)
{
// so vou conseguir iterar sobre as classes que implementam IObjectClassEvents
foreach (IObjectClassEvents classEvents in this.extensions)
{
classEvents.OnCreate(obj);
}
}
public void OnDelete(IObject obj)
{
// so vou conseguir iterar sobre as classes que implementam IObjectClassEvents
foreach (IObjectClassEvents classEvents in this.extensions)
{
classEvents.OnDelete(obj);
}
}
public string ValidateField(IRow Row, string FieldName)
{
string result = String.Empty;
// so vou conseguir iterar sobre as classes que implementam IObjectClassValidation
foreach (IObjectClassValidation validation in this.extensions)
{
result += validation.ValidateField(Row, FieldName);
}
return result;
}
public string ValidateRow(IRow Row)
{
string result = String.Empty;
// so vou conseguir iterar sobre as classes que implementam IObjectClassValidation
foreach (IObjectClassValidation validation in this.extensions)
{
result += validation.ValidateRow(Row);
}
return result;
}
}
public class ValidateParcelGeometryExtensions : IClassExtension, IObjectClassExtension, IObjectClassValidation
{
public void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
throw new NotImplementedException();
}
public void Shutdown()
{
throw new NotImplementedException();
}
public string ValidateField(IRow Row, string FieldName)
{
throw new NotImplementedException();
}
public string ValidateRow(IRow Row)
{
throw new NotImplementedException();
}
}
public class OnParcelEvents : IClassExtension, IObjectClassExtension, IObjectClassEvents
{
public void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
throw new NotImplementedException();
}
public void Shutdown()
{
throw new NotImplementedException();
}
public void OnChange(IObject obj)
{
throw new NotImplementedException();
}
public void OnCreate(IObject obj)
{
throw new NotImplementedException();
}
public void OnDelete(IObject obj)
{
throw new NotImplementedException();
}
}
public class ValidateDwgFile : IClassExtension, IObjectClassExtension, IObjectClassValidation
{
public void Init(IClassHelper ClassHelper, ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties)
{
throw new NotImplementedException();
}
public void Shutdown()
{
throw new NotImplementedException();
}
public string ValidateField(IRow Row, string FieldName)
{
throw new NotImplementedException();
}
public string ValidateRow(IRow Row)
{
throw new NotImplementedException();
}
}
As vantagens de algo assim são enormes do ponto de vista de manutenção. São três simples tabelas e uma mudança simples de estrutura de código que vai dar muito mais liberdade para os desenvolvedores e usuários. No momento em que construirmos uma nova extensão, se todos os usuários tiverem à mesma instalada no PC, é só associarmos a mesma à uma entidade que imediatamente deveria funcionar perfeitamente. Poderíamos até deixar à cargo do usuário ligar ou desligar as class extensions de seu interesse.
Entendo que este post é um ArcObjects um pouco mais avançado, mas é muito divertido ver como simples mudanças de arquitetura facilitam a vida dos desenvolvedores e transformar a forma como o usuário se relaciona com o software.
O que acharam?
Um abraço pessoal,
Related posts:
fevereiro 11th, 2011 - 21:17
Olá!
Parabéns pelo artigo! Fiquei com “inveja”
da qualidade das figuras. Qual software vc utilizou para representar as classes e objetos?
[Translate]
fevereiro 14th, 2011 - 12:39
Olá Rodrigo,
Utilizei o Visual Studio mesmo. Você pode criar diagramas de classes com ele de maneira bem simples. É só escrever o código e depois arrastar as classes para dentro do diagrama!
Obrigado pelo elogio e continue frequentando o blog!
[Translate]