Wednesday, April 09, 2008
O livro Inside Microsoft Dynamics AX 4.0 está disponível para download em formato pdf. O livro é ótimo para desenvolvedores.
Os autores começarão em breve upgrade do livro para a versão Dynamics AX 2009.
Saturday, February 09, 2008
A serialização de objetos possibilita a persistência e/ou a transmissão de um objeto entre módulos fisicamente separados de uma aplicação. É um processo parecido com transformar o leite em pó e depois conseguir recriar o leite adicionando água.
Diversas linguagens oferecem funcionalidades de serialização, em X++ ela é feita através do padrão pack-unpack. Enquanto em .Net, por exemplo, o objeto pode ser serializado em xml ou outros formatos, no X++ o formato utilizado é o container. Vamos utilizar a classe Person para o nosso exemplo:
public class Person
{
str name;
date birthdate;
}
Mas em que consiste “empacotar” um objeto? Basicamente é guardar todas as variáveis deste objeto em um container. O processo inverso, “desempacotar” o objeto, é receber um container contendo os valores das variáveis e carregar as variáveis com estes valores.
Uma versão simplista, mas funcional, do pack-unpack seria assim:
public container pack()
{
return [name, birthdate];
}
public void unpack(container _packed)
{;
name = conpeek(_packed, 1);
birthdate = conpeek(_packed, 2);
}
Veja a utilização do padrão. Primeiro crio um objeto pessoaA e coloco um nome e data de nascimento neste objeto. Utilizando o pack, guardo este objeto serializado em um container e, em seguida, destruo o objeto. Depois crio um novo Person, pessoaB, e, utilizando o unpack, restabeleço os valores do pessoaA neste novo objeto.
Person pessoaA;
Person pessoaB;
container packed;
pessoaA = new Person();
pessoaA.parmName("Joao");
pessoaA.parmBirthdate(10\10\2000);
packed = pessoaA.pack();
pessoaA = null;
pessoaB = new Person();
pessoaB.unpack(packed);
print pessoaB.parmName();
print pessoaB.parmBirthdate();
pause;
Esta versão funciona, mas poderia ficar um pouco melhor. Preciso repetir cada variável nos dois métodos, pack e unpack, além disso preciso acertar a ordem do container, senão posso colocar o valor de uma variável em outra durante o unpack. Utilizando uma macro, que declaro na classe, consigo resolver estes problemas. Esta nova implementação fica assim:
public class Person
{
str name;
date birthdate;
#LOCALMACRO.CurrentList
name,
birthdate
#ENDMACRO
}
public container pack()
{
return [#CurrentList];
}
public void unpack(container _packed)
{;
[#CurrentList] = _packed;
}
Em CurrentList declaro as variáveis que desejo empacotar para esta classe. Depois o pack-unpack utilizam a lista, eliminando a duplicação de código.
Versionamento
Muitas vezes as classes evoluem, ganhando novas variáveis, dadas as necessidades do negócio. Neste caso teremos um problema, imagine que um container contendo um objeto serializado é persistido e depois de algum tempo a aplicação tenta “desempacotá-lo” na classe que agora contém novas variáveis. Não seria possivel, na nossa implementação.
Portanto precisamos de uma pequena mudança. Digamos que a classe Person agora também contém o sexo da pessoa como variável. Então tenho uma variável e um método parm para sexo:
public class Person
{
str name;
date birthdate;
Gender gender;
#LOCALMACRO.CurrentList
name,
birthdate
#ENDMACRO
}
public Gender parmGender(Gender _gender = gender)
{;
gender = _gender;
return gender;
}
Agora tenho que mudar a implementação do pack-unpack para que ele saiba diferenciar pacotes contendo um objeto da versão antiga, sem sexo, da nova, com sexo. Adiciono a variável da CurrentList, esta foi a parte fácil. Crio uma constante com a versão corrente, 2.
public class Person
{
str name;
date birthdate;
Gender gender;
#DEFINE.CurrentVersion(2)
#LOCALMACRO.CurrentList
name,
birthdate,
gender
#ENDMACRO
}
No pack a versão passa a ser a primeira variável empacotada
public container pack()
{
return [#CurrentVersion, #CurrentList];
}
No unpack, verifico qual a versão do pacote recebido, e implemento um swicth para cada versão que a classe já teve. O método RunBase::getVersion() é um helper que lê o container descobre sua versão.
public void unpack(container _packed)
{
int version;
;
version = RunBase::getVersion(_packed);
switch (version)
{
case #CurrentVersion:
[version, #CurrentList] = _packed;
break;
default:
[name, birthdate] = _packed;
}
}
Veja a utilização, consigo desempacotar tanto um container contendo somente nome e data de nascimento, quanto a versão mais nova. Lógico que a variável gender assume o valor defaul, Unknown, quando o container não a possui.
Person pessoaA;
Person pessoaB;
container packed;
container oldPacked = ["Joao", 10\10\2000];
pessoaA = new Person();
pessoaA.parmName("Joao");
pessoaA.parmBirthdate(10\10\2000);
pessoaA.parmGender(Gender::Male);
packed = pessoaA.pack();
pessoaA = null;
pessoaB = new Person();
pessoaB.unpack(oldPacked);
print pessoaB.parmName();
print pessoaB.parmBirthdate();
print pessoaB.parmGender();
pessoaB.unpack(packed);
print pessoaB.parmName();
print pessoaB.parmBirthdate();
print pessoaB.parmGender();
pause;
Herança
Uma nova classe, Employee, é criada herdando da classe Person, empregado é uma pessoa, que também possui data de contratação.
class Employee extends Person
{
date hiringDate;
}
public date parmHiringDate(date _hiringDate = hiringDate)
{;
hiringDate = _hiringDate;
return hiringDate;
}
Como é Employee filha de Person, posso utilizar os métodos pack-unpack, no entanto, como era de se esperar, a variável hiringDate não será empacotada. Se eu simplesmente sobrescrever o pack-unpack e implementar minha própria versão, estarei duplicando código. Preciso sim, estender os métodos para que o pacote contenha também as variáveis da classe filha.
O pack, portanto, vai pegar o container da classe mãe, e vai empacotar em um novo container, junto com suas próprias variáveis.
public container pack()
{
container base;
base = super();
return [#CurrentVersion, #CurrentList, base];
}
Já o unpack vai desempacotar o container em três partes: a versão, sempre a primeira variável, as variáveis da classe filha e um segundo container, contendo as variáveis da classe mãe. Este segundo container é passado para o unpack da classe mãe.
Veja sua utilização, funcionando perfeitamente:
Employee empregadoA;
Employee empregadoB;
container packed;
;
empregadoA = new Employee();
empregadoA.parmName("Jose");
empregadoA.parmBirthdate(1\5\1950);
empregadoA.parmHiringDate(10\5\1980);
packed = empregadoA.pack();
empregadoA = null;
empregadoB = new Employee();
empregadoB.unpack(packed);
print empregadoB.parmName();
print empregadoB.parmBirthdate();
print empregadoB.parmHiringDate();
pause;
Friday, January 18, 2008
SysDictCoder publicou um post sobre como utilizar melhor os containers: Dealing with containers. Como o próprio autor define "container é um poderoso, e um pouco estranho, tipo". Para mim pouco é gentileza dele, containers são muito estranhos. Um ponto que realmente me incomoda é a pouca legibilidade dos containers. É impossível saber o que tem dentro deles, quantos níveis, os tipos, nada. Você é obrigado a ler o código que criou um container para conseguir ler os dados dele.
Existem diversas estrutura de dados no X++ e podem ser ótimas alternativas: Arrays, Maps, List, Set, RecordSortedList, dentre outros. Ou seja, estamos bem servidos de estrutura de dados. Se, ainda assim você precisar utilizar containers, sugiro a leitura acima.
Variáveis de classe no X++ não aceitam “access modifiers” e são sempre privadas. Ou seja, uma variável declarada no class declaration só acessivel dentro da própria classe.
No C#, ou no VB, nós temos as propriedades para dar acesso externo as variáveis de classe. Já no Java é comum criar método GetVariavel e SetVariavel para isto. No X++ o padrão é criar um método parmVariavel que oferece ao mesmo tempo acesso de leitura e de escrita na variável.
A classe Person abaixo, por exemplo, tem duas variáveis de classe: name e birthDate
class Person
{
str name;
date birthdate;
}
Se preciso expor estas variáveis para o mundo externo, faço um método parm como este:
public str parmName(str _name = name)
{;
name = _name;
return name;
}
O método recebe como parâmetro opcional o nome. Cujo o valor default é o nome atual. Veja como é fácil, alterar e ler a variável:
Person person;
;
person = new Person('José', 21\2\1998);
person.parmName('João');
print person.parmName();
pause;
Mesmo inicializando o objeto com o nome ‘José’, consigo alterar o nome para ‘João’. Agora em alguns casos não quero permitir alteração da variável, apenas leitura. Veja por exemplo o método parmAge, ele retorna a idade, baseado na data de nascimento. Só que não recebe parâmetro, pois esta é uma variável que não quero alterar.
public int parmAge()
{;
return yearDiff(today(), birthDate);
}
A leitura do método continua normal.
static void Job1(Args _args)
{
Person person;
;
person = new Person('José', 21\2\1998);
person.parmName('João');
print person.parmName();
print person.parmAge();
pause;
}
Como boa prática só devemos expor as variáveis somente quando realmente precisamos usar. Neste post explico um pouco melhor o porque desta prática.
Wednesday, January 23, 2008
Algumas novidades recentes na web relacionadas ao AX:
Uma nova revista online sobre Microsoft Dynamics: MS Dynamics World
Uma série de webcasts sobre Dynamics AX (conteúdo exclusivo do PartnerSource, é necessário login)
Dois blogs brasileiros dedicados ao desenvolmento AX: Axapta Brasil e Dynamics AXBR
Wednesday, December 19, 2007
Para quem tem interesse em processo de desenvolvimento vale a pena dar uma lida neste artigo sobre práticas ágeis em soluções ERP. Apesar de ser baseado em um projeto com outra plataforma, o artigo discorre sobre problemas comuns de projetos de customização de ERP e possíveis soluções utilizando práticas ágeis.
Um dos autores, Gerard Meszaros, é bastante respeitado na comunidade ágil e autor de um ótimo livro sobre testes unitários: xUnit Test Patterns. Quem tiver interesse em começar a realizar testes unitários no AX, pode aprender sobre o framework disponível no AX lá no MSDN.
Tuesday, December 11, 2007
Seguem os links de apoio para a nossa palestra no Teched sobre Dynamics AX.
O Marcos Tito me mandou um e-mail pedindo material sobre a demo de integração com o Speech Server. Esta demo foi criada pelo Peter Villads, PM do time de X++. Ele já blogou sobre a demo aqui, com todos os detalhes para reproduzí-la. Basicamente a solução é um aplicativo Speech Server, com um workflow simples, que utiliza o .Net Business connector com dois objetivos:
- Buscar a semântica da aplicação nas tabelas de produtos e clientes. Desta forma, a aplicação conhece as palavras e frases que o usuário pode falar.
- Persistir a ordem de venda no AX. Após o processo finalizado o sistema precisa persistir o pedido do cliente no AX.
A outra demo que apresentei foi a integração com o MapPoint. O serviço é oferecido via web services, e pode ser facilmente utilizado em qualquer aplicativo. O retorno é uma figura que o aplicativo .Net grava em disco. O endereço local da figura é passado para o AX que coloca a imagem em um controle do formulário.
A outra demo que fiz foi uma simples de .Net Business connector. Só para mostrar a capacidade de debug simultâneo no AX e no Visual Studio.
O Edmar apresentou a solução de Mobile, no PartnerSource você pode buscar informações e downloads do Mobile solution kit: (Lembrando que o acesso ao site é restrito para parceiros MBS)
https://mbs.microsoft.com/partnersource/downloads/releases/mobilesolutions.htm?printpage=false
Obrigado pela audiência e paciência de todos.
Thursday, November 08, 2007
Joel está divulgando vagas para desenvolvedor para o time do Dynamics AX. Para saber mais detalhes veja o post.
Tuesday, October 16, 2007
Uma das grandes vantagens do X++ como linguagem para um ERP são suas funcionalidades de acesso a dados integradas a própria linguagem. Isto simplifica bastante o acesso à camada de dados. Mesmo assim existem muitas formas de alcançar os mesmos objetivos, umas mais adequadas que outras. Neste post vou analisar algumas destas alternativas.
A estrutura mais utilizada de acesso aos dados de uma tabela é o while select. N exemplo abaixo seleciono e processo todos os clientes, contidos na tabela CustTable, que pertencem ao grupo “INTER-PR”.
Estendendo este código posso trazer as vendas abertas para estes clientes. O exemplo abaixo mostra isto, porém de uma forma pouco performática. Para cada cliente você faz um novo select na tabela de pedidos. Para uma base de 500 clientes, são 501 acessos ao banco de dados.
O código abaixo mostra uma solução usando um join entre as duas tabelas. O resultado é o mesmo, mas o acesso ao banco de dados é feito de uma vez. O que torna a solução mais performática.
Outro problema muito comum é listar todos os clientes que têm ordens de venda em aberto. Uma solução que pode parecer razoável é esta abaixo. Para cada cliente verifico se existe uma ordem aberta para ele.
Existem dois pontos fracos nesta solução: O primeiro select traz todos os clientes, sem filtrar nada. Depois disto, para cada cliente, um select é realizado na tabela de ordens de venda. Novamente um join é o mais adequado para este caso. Porém, neste caso, o ideal é fazer um tipo especial de join, que o exists join.
Neste caso, o exists join tem uma grande vantagem sobre um join comum: Não traz todas as ordens de vendas de cada cliente. Com isto o resultado do select é menor (leia-se: quantas linhas retornam). Além disto, a instância da tabela de ordens de compras não é carregada, economizando memória.
Este tipo de join tem seu inverso, que é o notexists. O comando também é conhecido como “minus” em SQL, pois trazem as linhas da tabela de cliente MENOS os clientes que estão na tabela de ordens de venda. Este é o resultado do código abaixo.
Utilizando o notexists temos que tomar o seguinte cuidado. As cláusulas where referentes à tabela de clientes devem ser escritas logo abaixo do select. Já as cláusulas referentes ao join entre as tabelas ficam no final do comando. Se eu quiser, por exemplo, filtrar os clientes que estão em São Paulo, tenho a query abaixo. Dois where no mesmo select não é comum em SQL, mas a leitura é bem intuitiva: Quero buscar os clientes que estão em São Paulo (primeiro select) e que não têm ordens de venda (o join).
Se você tentar colocar a cláusula do cliente no final, junto com as outras, verá que o resultado é o inverso, a query trará os cliente que não estão em São Paulo, pois a cláusula está relacionada ao notexists.
Como conclusão, seguem algumas dicas para identificar qual a melhor solução:
- Se dentro do select while você está fazendo testes com campos da tabela ou buscando informações de outras tabelas, também usando informações da tabela, verifique se um join não pode resolver este problema.
- Se você está fazendo um join, mas a segunda tabela só é utilizada como filtro para o select, verifique se é possível utilizar o comando exists ou notexists
Monday, October 08, 2007
Dois novos blogs de AX que passei a acompanhar recentemente:
- Axapta Brasil, mantido por diversos autores com experiência em desenvolvimento no AX.
- Inside Dynamics AX 4.0, não conheço o autor (porque as pessoas não escrevem aquela página de "about"?!). Mas tem bons artigos técnicos.