Académique Documents
Professionnel Documents
Culture Documents
Introdução
Vamos ver um pouco sobre Bibliotecas de Funções e Framework, com destaque para as
funcionalidades em implementação no projeto ZLIB.
Bibliotecas e Frameworks
Com as funções básicas da linguagem, conseguimos criar qualquer programa. Alguns programas
podem dar mais trabalho que outros, tudo depende de quantas funcionalidades serão
implementadas. Porém, quando você precisa implementar muitas funcionalidades parecidas, é mais
eficiente isolar o código comum em classes ou funções parametrizáveis, para não ter que escrever
tudo de novo ou copiar-e-colar, replicando código desnecessariamente. Neste ponto, começa o
nascimento de uma Biblioteca de funções.
Quando falamos em Framework, não apenas estamos usando funções genéricas de uma biblioteca,
mas sim uma abstração de nível mais alto, que impõe um fluxo de controle na aplicação.
Um framework em desenvolvimento de software, é uma abstração que une códigos comuns entre
vários projetos de software provendo uma funcionalidade genérica. Um framework pode atingir
uma funcionalidade específica, por configuração, durante a programação de uma aplicação. Ao
contrário das bibliotecas, é o framework quem dita o fluxo de controle da aplicação, chamado de
Inversão de Controle.[1]
Projeto ZLIB
A ideia — necessidade — de uma LIB (Biblioteca) de componentes surgiu com os posts da série do
CRUD em AdvPL, que acabou virando uma Agenda de Contatos, feita originalmente atrelada a
interface do SmartClient, e depois implementada em uma interface WEB/HTTP.
Muito daquele código é comum a aplicações de mesma funcionalidade — cadastro simples.
Inclusão, Alteração, Exclusão, Consulta ordenada, consulta por filtro. Outras funcionalidades, como
exibição e cadastro de imagem, envio de email e mapa do endereço não necessariamente são
usadas em todos os cadastros, mas podem ser colocadas em componentes de uma biblioteca para
reaproveitamento.
A ideia da ZLIB é ser uma Biblioteca de Funções, que vai servir de base para construir um Framework.
Ela já está versionada no GITHUB, mas ainda em desenvolvimento e com pouca (nenhuma)
documentação, e como os componentes ainda estão nascendo, muitas alterações drásticas estão
sendo feitas a cada atualização.
Por exemplo, as classes implementadas para acesso a arquivos DBF e arquivos em memória. Ambas
possuem a mesma declaração de métodos para implementar as suas funcionalidades. Logo, o
mesmo programa que insere um registro em uma tabela da classe ZDBFFILE pode realizar a mesma
operação usando um objeto da ZMEMFILE.
Uma classe de geração de LOG de operação ou execução não precisa saber onde o log será gravado,
ou mesmo conhecer a interface de gravação. Ela pode receber como parâmetro um objeto de uma
classe de gravação de LOG. Ele pode ser de uma classe que grave os registros emitidos de log em
um arquivo TXT, ou em um banco de dados, ou ainda seja um encapsulamento de uma interface
“client” de log, que envia os dados gerados para serem gravados remotamente por um Log Server.
Criação de Componentes
Como a implementação está por baixo da abstração, eu posso por exemplo criar uma abstração de
exportação de arquivo, e implementar uma exportação para cada formato, a mesma coisa para
importação.
Objetivo Final
Conclusão
Por hora, a primeira missão das funções em desenvolvimento é permitir a reescrita do programa de
Agenda para SmartClient, usando componentes destacados, que permitam um elevado índice de
reaproveitamento de código, e uma forma de declarar e executar as validações e procedimentos de
cada operação que torne a codificação mais fácil e rápida, usando uma abordagem que permita
aproveitar o CORE de cada componente em integrações encapsuladas por APIs (RPC Advpl, REST,
SOAP) para serem consumidas por interfaces criadas em AdvPL ou qualquer outra linguagem ou
plataforma.
Referências
FRAMEWORK. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2018. Disponível
em: <https://pt.wikipedia.org/w/index.php?title=Framework&oldid=53678305>. Acesso em: 25
nov. 2018.
2 Comentários
Introdução
No post anterior (Abstração de Acesso a Dados e Orientação a Objetos – Parte 04), foram
implementadas algumas opções de exportação de tabelas, para os objetos ZDBFFILE e ZMEMFILE.
Enquanto isso, foi implementado na classe ZISAMFILE a importação dos formatos SDF e CSV. Agora,
para compatibilizar alguns recursos do AdvPL já existentes com estas classes, vamos partir para as
novas classes ZTOPFILE e ZQUERYFILE.
Classe ZQUERYFILE
Embora todas as funções ISAM possam ser usadas em uma WorkArea aberta sob um ALIAS, para
um cursor ou result-set de Query, aplicam-se apenas as seguintes funções:
DBGoTOP() — Se você ainda está no primeiro registro da Query, a função é ignorada. Se você já leu
um ou mais registros, ou mesmo todos os registros da Query, como não tem como “voltar” registros,
a função fecha e abre a query novamente no Banco de Dados, recuperando um novo result set.
Logo, se no intervalo de tempo entre a primeira abertura do cursor e o DBGoTOP(), registros foram
acrescentados ou tiveram algum campo alterado, a quantidade de registros retornada pela Query
pode ser diferente.
TCSetField() — Se no banco de dados existem um campo “D” Data em AdvPL, ele é gravado e
retornado pela Query como um campo “C” Caractere de 8 posições. Usando a função TCSetField()
podemos informar ao Protheus que esta coluna deve ser retornada no campo do ALIAS j;a
convertida para Data. O mesmo se aplica a precisão de campos numéricos, e a campos do tipo “L”
lógico — que no Banco de Dados são gravados com um caractere “T” indicando verdadeiro ou “F”
indicando falso.
Quaisquer outras instruções são ignoradas, ou retornam algum tipo de erro. DBRLOCK() sempre
retorna .F., pois um alias de Query é READ ONLY — Somente leitura. Nao é possível setar filtros,
nem para registros deletados. O filtro de registros deletados deve ser uma condição escrita de forma
literal na Query, usando o campo de controle D_E_L_E_T_ .
A ideia da classe ZQUERYFILE é encapsular o alias retornado da Query, e implementar as mesmas
funcionalidades que um objeto de acesso a dados da ZLIB foram concebidos — como por exemplo
o ZDBFFILE e o ZMEMFILE.
Desta forma, uma rotina escrita para trabalhar com um tipo de arquivo que herda desta classe ou
que possui os métodos mínimos de navegação necessários possa utilizá-la também, onde colocamos
um overhead mínimo apenas criando métodos para encapsular os acessos aos dados.
Classe ZTOPFILE
Dentro da classe — que vai herdar a ZISAMFILE — implementamos os métodos para usar os recursos
nativos, e alguns adicionais para permitir o uso de recursos implementados na ZLIB, como por
exemplo o índice em memória do AdvPL — ZMEMINDEX.
Alguns métodos das classes ZISAMFILE já foram concebidos para trabalhar nativamente com um
objeto de dados, ou o ALIAS de uma WorkArea. A ideia de utilização do modelo de abstração é
permitir flexibilizar a manutenção e persistência de dados — permanente ou temporária — usando
instruções de alto nível, dentro de fronteiras e comportamentos pré-definidos para viabilizar
escalabilidade, desempenho e resiliência para a aplicação.
Quer ver uma coisa deveras interessante que pode ser feita com, esta infraestrutura ? Então,
imagine um operador de sistema realizando por exemplo uma inclusão de dados com muitas
informações, em um terminal com conexão síncrona, como o SmartClient. Em determinado
momento, algo horrível aconteceu com a rede entre a estação (SmartClient) e o Servidor (Protheus
Server) que ela estava conectada. Vamos ao pior cenário, houve um problema de Hardware (no
servidor, na rede, na própria estação — acabou a luz. e o terminal em uso não têm No-Break. Adeus
operação, dados já digitados, foi pro beleléu.
Bem, usando um cache — por exemplo o ZMEMCACHED — a aplicação poderia gravar no cache, em
intervalos de tempo pré-definidos ou mesmo a cada informação significativa digitada, um registro
com os dados já preenchidos até aquele momento, atrelado ao usuário que está realizando a
operação ou a operação em si. No término da operação, esta informação é removida do cache. Caso
o processo seja interrompido, no momento que o usuário fizer o login, ou quando ele entrar na
rotina novamente na rotina, o programa verifica se tem alguma informação no cache daquele
usuário, e em caso afirmativo, permita a informação ser recuperada, e o usuário conseguiria
continuar a operação do ponto onde foi feito o último salvamento.
Para isso ser feito de forma simples, a operação deve ser capaz de serializar — representar em um
formato armazenável e recuperável — o seu estado no momento, para tornar fácil a operação de
salvamento e recuperação de estado. Para isso, poderemos usar — assim que estiver pronta — a
classe ZSTREAM.
Com ela, a ideia é ser possível inclusive salvar não apenas valores, mas um objeto. Isso mesmo,
pegar as propriedades de um objeto automaticamente e salvar o estado do objeto em um cache,
no disco ou onde você quiser. Porém, para restaurar o estado do objeto, você deverá recria-lo, e
fazer os métodos Save e Load nele, para ser possível ajustar as propriedades necessárias, lembrando
que CodeBlock não dá para ser salvo e restaurado, o bloco de código pode depender de variáveis
do ambiente e referências de onde ele foi criado.
Neste cenário, o mundo ideal seria criar uma classe apenas para ser um agrupador de propriedades
e ser o container de contexto de dados de uma operação. Neste caso, poderíamos construir um
método NEW() na estrutura, retornando self e populando as propriedades com seus valores default.
e os métodos SAVE e LOAD para salvar ou recuperar o estado atual a partir de uma Binary String —
ou Stream.
Conclusão
15/01/2019 ADVPL, Banco de Dados, DBF / ISAM, Orientação a Objeto Classes, Programação
Introdução
Continuando a mesma linha dos posts anteriores, vamos ver agora como exportar um arquivo de
dados — das classes ZMEMFILE e/ou ZDBFFILE — para os formatos SDF , CSV e JSON 😀
Formato SDF
O formato SDF é um arquivo texto com linhas de tamanho fixo (SDF = System Data Format, fixed
length ASCII text). Cada linha do arquivo é composta pelo conteúdo de um registro da tabela, onde
cada campo é gravado sem separador ou delimitador, na ordem de colunas da estrutura da tabela,
onde o formato de gravação de cada campo depende do tipo.
------------------------------------------------------------------------
------------------------------------------------------------------------
Logical fields T or F
------------------------------------------------------------------------
Campos Caractere são gravados com espaços a direita para preencher o tamanho do campo da
estrutura, campos do tipo “D” data são gravados no formato ANSI (AAAAMMDD), uma data vazia é
gravada com 8 espaços em branco, campos “L” Lógicos são gravados como “T” ou “F”, campos
numéricos usam o tamanho especificado na estrutura com espaços a esquerda, e “.” ponto como
separador decimal. A quebra de linha é composta pela sequência de dois bytes CRLF — chr(13) +
chr(10) — e ainda têm um último byte indicando EOF (Final de Arquivo). Campos “M” memo não
são exportados.
Este formato é muito rápido de ser importado, pois uma vez que eu trabalho com os tamanhos dos
campos da estrutura, eu não preciso fazer um “parser” de cada linha para verificar onde começa e
onde termina cada campo e informação. Porém, o formato SDF não leva junto a estrutura da tabela,
portanto uma importação somente será efetuada com sucesso caso o arquivo de destino já exista,
e tenha a mesma estrutura do arquivo de origem — estrutura idêntica, tipos e tamanhos de campos,
na mesma ordem.
Formato CSV
Este formato é muito comum em integrações entre sistemas, definido pela RFC-4180 (CSV = Comma
Separated Values). Os campos caractere são gravados desconsiderando espaços à direita, e
delimitados por aspas duplas. Caso exista uma aspa dupla dentro do conteúdo do campo a aspas é
duplicada. As colunas são separadas por vírgula, valores numéricos são gravados sem espaços e
usando o separador decimal “.” (ponto), valores booleanos são gravados como “true” ou “false”,
campos do tipo “D” Data são gravados entre aspas no formato AAAAMMDD, data vazia é uma string
vazia. Campos “M” memo também não são exportados. A primeira linha do CSV possui a lista de
campos da tabela — apenas o nome do campo.
Um arquivo CSV tende a ser menor que um SDF, quando os campos do tipo Caractere possuem
espaços a direita — que são desconsiderados ao gerar o CSV. Porém, com campos de tamanho
variável, é necessário tratar linha a linha para verificar onde começa e termina cada informação a
ser recuperada.
FORMATO JSON
Existem várias formas de se especificar uma tabela neste formato ( JSON = JavaScript Object
Notation). A forma mais econômica e segura de exportar uma tabela de dados neste formato é
representá-la como um objeto de duas propriedades: “header”, composta de um array
multidimensional de N linhas e 4 colunas, representando a estrutura da tabela (Campo, Tipo,
Tamanho e Decimais), e a propriedade “data”, composta de um array multi-dimensional de X linhas
por N colunas, onde cada linha representa um registro da tabela, na forma de um array com os
dados das colunas, na ordem de campos especificada no header. O formato de representação dos
dados é praticamente o mesmo do CSV, exceto o campo “C” Caractere, que caso tenha aspas duplas
em seu conteúdo, esta aspa é representada usando a sequencia de escape \” (barra inversa e aspa
dupla). Por hora, campos “M” Memo não são exportados. A diferença de tamanho dos mesmos
dados exportados para CSV ou JSON é mínima.
Até agora tudo é lindo, então vamos ver como fazer a exportação, usando o método — ainda em
desenvolvimento — chamado Export(). Primeiramente, partimos de um exemplo onde eu abri uma
Query e criei um arquivo em memória com os registros retomados da Query, usando a classe
ZMEMFILE. Então, eu chamo o método EXPORT() deste objeto, informando o formato a ser gerado
e o arquivo em disco com o resultado a ser criado.
Local cQuery
Local nH
nH := tclink("MSSQL/DBLIGHT","localhost",7890)
IF nH < 0
Return
Endif
cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"
TCSetField("QRY","VALOR","N",12,2)
oMemory := ZMEMFILE():New('qrymemory')
oMemory:CreateFrom("QRY",.T.)
// Fecha a Query
USE
oMemory:Export("SDF",'\temp\tstexport.sdf' )
oMemory:Export("CSV",'\temp\tstexport.csv' )
oMemory:Export("JSON",'\temp\tstexport.json' )
oMemory:Close()
FreeObj(oMemory)
Return
Método EXPORT()
E, para apreciação, vamos ver por dentro o método de exportação de dados. Ele ainda está em
desenvolvimento, estou estudando como parametrizar algumas características específicas dos
formatos suportados. Por hora, ele está assim:
Local nHOut
Local nPos
IF !::lOpened
Endif
cFormat := alltrim(Upper(cFormat))
If cFormat == "SDF"
// Formato SDF
nHOut := fCreate(cFileOut)
If nHOut == -1
Return .F.
Endif
::GoTop()
While !::Eof()
cRow := ""
cTipo := ::aStruct[nPos][2]
nTam := ::aStruct[nPos][3]
nDec := ::aStruct[nPos][4]
If cTipo = 'C'
cRow += ::FieldGet(nPos)
cRow += Str(::FieldGet(nPos),nTam,nDec)
cRow += DTOS(::FieldGet(nPos))
cRow += IIF(::FieldGet(nPos),'T','F')
Endif
Next
cRow += CRLF
cBuffer += cRow
fWrite(nHOut,cBuffer)
cBuffer := ''
Endif
::Skip()
Enddo
cBuffer += Chr(26)
fWrite(nHOut,cBuffer)
cBuffer := ''
fClose(nHOut)
// Formato CSV
nHOut := fCreate(cFileOut)
If nHOut == -1
Return .F.
Endif
If nPos > 1
cBuffer += ','
Endif
cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'"'
Next
cBuffer += CRLF
::GoTop()
While !::Eof()
cRow := ""
cTipo := ::aStruct[nPos][2]
nTam := ::aStruct[nPos][3]
nDec := ::aStruct[nPos][4]
If nPos > 1
cRow += ","
Endif
If cTipo = 'C'
// Numero trimado
cRow += cValToChar(::FieldGet(nPos))
cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'
cRow += IIF(::FieldGet(nPos),'true','false')
Endif
Next
cRow += CRLF
cBuffer += cRow
If len(cBuffer) > 32000
fWrite(nHOut,cBuffer)
cBuffer := ''
Endif
::Skip()
Enddo
If len(cBuffer) > 0
fWrite(nHOut,cBuffer)
cBuffer := ''
Endif
fClose(nHOut)
/*
{
"header": [
],
"data": [
*/
nHOut := fCreate(cFileOut)
If nHOut == -1
Return .F.
Endif
If nPos = 1
cBuffer += "["
Else
cBuffer += '],'+CRLF+'['
Endif
cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'","'+;
::aStruct[nPos][2]+'",'+;
cValToChar(::aStruct[nPos][3])+','+;
cValToChar(::aStruct[nPos][4])
Next
cBuffer += ']'+CRLF
::GoTop()
While !::Eof()
if lFirst
cRow := "["
lFirst := .F.
Else
cRow := "],"+CRLF+"["
Endif
cTipo := ::aStruct[nPos][2]
nTam := ::aStruct[nPos][3]
nDec := ::aStruct[nPos][4]
If nPos > 1
cRow += ","
Endif
If cTipo = 'C'
// Numero trimado
cRow += cValToChar(::FieldGet(nPos))
cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'
cRow += IIF(::FieldGet(nPos),'true','false')
Endif
Next
cBuffer += cRow
fWrite(nHOut,cBuffer)
cBuffer := ''
Endif
::Skip()
Enddo
// Termina o JSON
fWrite(nHOut,cBuffer)
cBuffer := ''
// Fecha o Arquivo
fClose(nHOut)
Else
Endif
Return
Otimizações
Uma otimização interessante é usar a variável de memória cBuffer para armazenar os dados que
devem ser gravados no arquivo, e apenas fazer a gravação caso ela tenha atingido ou ultrapassado
32000 (32 mil) bytes. É muito mais rápido fazer a variável de memória aumentar de tamanho, do
que o arquivo no disco. Logo, é mais eficiente gravar um bloco de 32000 bytes no arquivo, do que
gravar 32 blocos de 1000, pois a cada bloco gravado o sistema operacional aloca mais espaço para
o arquivo, e é mais rápido alocar um espaço maior de uma vez do que várias chamadas de alocações
menores.
Porém, não é por isso que eu vou deixar a minha String em AdvPL chegar a 1 MB de tamanho para
fazer a gravação, pois com strings muito grandes na memória, as operações de alocar mais espaço
em memória vão ficar mais pesadas. Para mim, algo perto de 32 KB é um “número mágico” bem
eficiente e sem desperdício.
Conclusão
Bem, por hora foi feita a exportação… agora, vou queimar mais alguns neurônios pra fazer a
importação destes dados nestes formatos 😀
Referências
https://www.json.org/
https://www.itlnet.net/programming/program/reference/c53g01c/ngc5219.html
https://www.webopedia.com/quick_ref/fileextensionsfull.asp
https://tools.ietf.org/html/rfc4180
1 comentário
14/01/2019 ADVPL, Banco de Dados, DBF / ISAM, Orientação a Objeto Classes, Tabelas, ZLIB
Introdução
Nos posts anteriores (Abstração de Acesso a Dados e Orientação a Objetos – Parte 02,Abstração de
Acesso a Dados e Orientação a Objetos), vimos a montagem de um encapsulamento de acesso a
dados usando orientação a objetos com herança em AdvPL. Agora, vamos integrar esse mecanismo
com um Alias / WorkArea do AdvPL.
Usando as classes de arquivo em memória (ZMEMFILE) ou arquivo DBF (ZDBFFILE), podemos criar
uma tabela (em memória ou no disco) com a mesma estrutura de uma outra tabela de uma destas
classes, informando como parâmetro o objeto. Que tal ela também receber um ALIAS como
parâmetro ? Pode ser de uma tabela qualquer, não importa. E, melhor ainda, que tal este método
receber um segundo parâmetro, que caso seja especificado .T. (verdadeiro), já abre a tabela criada
em modo exclusivo e copia todos os dados do ALIAS informado como parâmetro? Veja o exemplo
abaixo:
#include 'protheus.ch"
Local cQuery
Local nH
nH := tclink()
IF nH < 0
Return
Endif
cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"
// Ajusta um campo
TCSetField("QRY","VALOR","N",12,2)
oMemory := ZMEMFILE():New('QRYINMEMORY')
oMemory:CreateFrom("QRY",.T.)
While !oMemory:Eof()
oMemory:Skip()
Enddo
oMemory:Close()
FreeObj(oMemory)
// fecha a query
USE
return
Agora sim a coisa ficou prática. E, usando esta abordagem, eu tenho algumas vantagens incríveis.
Primeira, com a Query copiada para a memória, usando o arquivo em memória eu posso mexer nos
dados, eu posso inserir novos registros, posso navegar para frente e para trás ( Skip -1 ), e tudo o
mais que com um ALIAS a partir de uma Query, nada disso é possível de ser feito.
O método CreateFrom() trabalha em conjunto com o AppendFrom(), ambos do ZISAMFILE. Uma vez
determinado que eles receberam uma string ao invés de um Objeto como parâmetro, eles assumem
que a string contém um ALIAS de uma WorkArea aberta, e fazem a leitura de estrutura e dados do
ALIAS informado.
Local aStruct := {}
lFromAlias := .T.
cAlias := alltrim(upper(_oDBF))
If Select(cAlias) < 1
Endif
aStruct := (cAlias)->(DbStruct())
Else
aStruct := _oDBF:GetStruct()
Endif
If !::Create(aStruct)
Return .F.
Endif
IF lAppend
If !::Open(.T.,.T.)
Return .F.
Endif
// Apenda os dados
IF !::AppendFrom(_oDBF)
Return .F.
Endif
::GoTop()
Endif
Return .T.
Local aFromTo := {}
Local aFrom := {}
IF !::lOpened
Return .F.
Endif
IF !::lCanWrite
Return .F.
Endif
If valtype(_oDBF) == 'C'
lFromAlias := .T.
cAlias := alltrim(upper(_oDBF))
If Select(cAlias) < 1
Endif
aFrom := (cAlias)->(DbStruct())
Else
aFrom := _oDBF:GetStruct()
Endif
For nI := 1 to len(aFrom)
cField := aFrom[nI][1]
nPos := ::FieldPos(cField)
If nPos > 0
Endif
Next
IF lFromAlias
If lAll
(cAlias)->(DbGoTop())
Endif
While !(cAlias)->(EOF())
::Insert()
For nI := 1 to len(aFromTo)
// Atualiza os valores
::Update()
(cAlias)->(DbSkip())
Enddo
Else
If lAll
_oDBF::GoTop()
Endif
While !_oDBF:EOF()
::Insert()
For nI := 1 to len(aFromTo)
Next
// Atualiza os valores
::Update()
_oDBF:Skip()
Enddo
Endif
Return .T.
O CreateFrom() permite criar a tabela apenas com a estrutura, porém se parametrizado com .T. no
segundo parâmetro, já abre a tabela atual e importa os dados do objeto ou ALIAS de origem
especificado.
Próximos passos
Criando mais alguns encapsulamentos, será possível colocar de modo mais prático uma tabela em
Cache. Eu posso criar uma tabela temporária em memória com o resultado de uma Query, e colocar
esta tabela no cache, para ser recuperada conforme a necessidade. Lembrando que esta tabela não
deve ser monstruosa, mas ter um número de registros que isole um contexto. Senão você armazena
um caminhão de tijolos no Cache, mas quando você resgata o cache para uso, você usa apenas
alguns tijolos.
Conclusão
Nada a declarar. Fontes da zLib atualizados no GITHUB, agora é só bolar um encapsulamento neste
mesmo padrão para uma tabela ISAM do DBAccess e para uma Query, depois fazer Export e Import
para outros formatos (JSON, TXT, SFD, CSV , XML, “Socorro”).
Deixe um comentário
Introdução
No post anterior (MemCached Client em AdvPL – Parte 01) vimos a implementação de uma classe
nativa em AdvPL para fazer o papel de API Client do MemCached. Agora, vamos ver um pouco de
como usar esta classe — agora na versão 1.01, suportando todos os tipos simples do AdvPL para
armazenamento e recuperação do cache, inclusive Array.
No post anterior, vimos um fonte de testes da classe zMemCached, apenas para teste de
funcionalidade. Agora, vamos ver onde poderíamos usar esta classe em algumas situações.
Antes de mais nada, vamos conceituar o objetivo de um cache: Um cache normalmente é criado
para tirar o peso de um processamento usado para obter um resultado, quando o mesmo resultado
será muitas vezes , por um ou mais partes de um programa, por um ou múltiplos usuários, desde
que a informação seja a mesma para todos.
O MemCached foi criado para ser um cache versátil — armazena qualquer coisa — e rápido.
Normalmente é usado em aplicações WEB para reduzir a quantidade de requisições ao banco de
dados, quando um mesmo resultado de uma Query ou de uma página renderizada será muito
requisitada e possui um baixo índice de alteração — o que evita invalida e realimentar o cache
constantemente.
Por ser um cache que pode atender múltiplas conexões de múltiplos usuários de um sistema,
qualquer parte comum a todos eles, e constantemente requisitada, poderia ser colocado em cache.
Porém, precisamos tomar cuidado com esta afirmação: PODER, tudo pode, mas nem tudo DEVE ser
feito. Senão, dobra a memória da máquina e coloca tudo em cache. Pronto, seu cache vai comer
memória com farina, você vai ter um volume estrelar de dados da memória, aqueles dados que não
são usados frequentemente somente ocupam espaço e oneram as demais operações que realmente
ganhariam com o uso de um cache.
Logo, a premissa numero um é: Use um cache onde é necessário e adequado. Comece com aquilo
que realmente “mata” o seu banco de dados, faça um Log Profiler primeiro, não saia usando o cache
por que é “legal” 😉
Implementação
O segundo mandamento é: Salvo raras exceções, como o uso de uma chave com um valor para
incrementou ou decremento, use o cache para colocar um agrupamento de dados com contexto.
Por exemplo, se você têm uma Query que retorna os 10 produtos mais vendidos no mês, não
armazene cada linha da query em uma chave, coloque os dados em um array e guarde o array.
Quase todas as consultas feitas a esta chave vão querer, via de regra, os 10 produtos.
Quer fazer uma atualização automática dos resultados a cada 60 minutos? Uma forma elegante e
sob demanda é armazenar o resultado em cache com expire time de 3600 segundos (ou 1 hora).
Com isso, passou uma hora, o valor é apagado do cache, o próximo processo que pedir este valor
vai ver que ele não está cacheado, roda a Query, e alimenta o cache novamente com o valor mais
novo.
Vale lembrar que a responsabilidade de atualizar o cache é da aplicação que o consome, Ela sempre
deve buscar primeiro a informação em Cache, e caso não a encontre, ela deve gerar a informação e
alimentar o cache. Por isso, mesmo que o custo de processamento de consultar e armazenar no
cache seja relativamente baixo, fazer isso para uma informação com baixa incidência de busca (low
hit count) é desperdiçar recurso.
Colocando uma Query em Cache
Vamos partir de uma Query qualquer, onde desejamos guardar seu resultado em Cache. Por hora,
partindo de um programa já existente, primeiro ele deve ser alterado para trabalhar com um Result
Set em Array. Desse modo, podemos armazenar e recuperar o Array com o resultado da Query no
Cache.
cQuery := "SELECT CAMPO1, CAMPO2, CAMPO3 FROM TABELA WHERE CAMPO1 = '001' ORDER BY
1,2"
While !eof()
QRY->(DbSkip())
Enddo
USE
oMemCache := ZMEMCACHED():New("localhost",11211)
// Conecta no Cache
If !oMemCache:Connect()
conout("oMemCache:Connect() FAILED")
conout(oClient:GetErrorStr())
return .F.
Endif
Antes de rodar a Query, verificamos se a informação está no cache. Vamos dar um nome para a
chave, um identificador desta Query para o cache.
oMemCache:Get("TEST_QRYINCACHE",@aQryData)
Se o conteudo de aQryData estiver NIL após a chamada, este array não está no cache. Neste caso,
rodamos a Query, e colocamos o array em cache, usando:
oMemCache:Set("TEST_QRYINCACHE",aQryData)
Se o array não foi recuperado, roda a Query no banco, cria o array e coloca ele em cache
A exceção são os casos onde uma ou mais sub-rotinas durante o processamento também vão fazer
requisições ao cache. Neste caso, podemos encapsular o objeto de cache usando uma classe de
controle, armazenando o objeto client do cache em uma variável STATIC, e controlando o acesso a
este objeto usando métodos GetCache() e ReleaseCache().
Neste caso, as rotinas e sub-rotinas que usam o cache poderiam buscar o objeto usando o
GetCache(), que incrementaria um contador interno de uso, e ao terminar o processamento, chamar
a ReleaseCache(), que decrementa a referência, anula a variável em uso, e caso a referência esta
chegue em 0 (zero), desconecta e limpa o objeto armazenado, enquanto quem consome o cache
apenas atribui NIL para a variável usada para armazenar o objeto de cache daquela rotina. Inclusive,
podemos nos aproveitar de algumas características interessantes das classes em AdvPL. Vejamos:
#include "protheus.ch"
/* ==============================================================================
Classe ZMEMCACHEDPOOL
Data 01/2019
Descrição Encapsula objeto client do MemCache, usando contador de referencias.
ZMEMCACHEDPOOL():ReleaseCache( @oMemCache )
==============================================================================*/
METHOD GetCache()
METHOD ReleaseCache()
METHOD RefCount()
ENDCLASS
// ----------------------------------------------------
oCache := NIL
cError := ""
IF _oMemCache != NIL
IF _oMemCache:IsConnected()
// e retorno
_nRefCount++
oCache := _oMemCache
Else
FreeObj(_oMemCache)
_oMemCache := NIL
Endif
Endif
IF _oMemCache == NIL
_oMemCache := ZMEMCACHED():New("localhost",11211)
IF _oMemCache:Connect()
_nRefCount++
oCache := _oMemCache
Else
cError := _oMemCache:GetErrorStr()
FreeObj(_oMemCache)
_oMemCache := NIL
Endif
Endif
Return
// ----------------------------------------------------
IF oCache != NIL
oCache := NIL
_nRefCount--
IF _nRefCount < 1
_oMemCache:Disconnect()
FreeObj(_oMemCache)
_oMemCache := NIL
Endif
Endif
Return
// ----------------------------------------------------
// do objeto do Cache
Return _nRefCount
Dessa forma, você centraliza a conexão com o MemCached, e no seu processo obtém uma instância
única em uso. Basta tomar cuidado para não fazer mais Release() do que Get(), senão você acaba
matando a instância e todas as referências que ainda podem fazer uso dele. Da mesma forma, não
esqueça de fazer um Release() após usar, senão você larga o objeto na memória e deixa a conexão
dele lá até que o processo termine. Vamos ver como ficariam os fontes que consomem o cache:
oMemCache := ZMEMCACHED():New("localhost",11211)
If !oMemCache:Connect()
conout("oMemCache:Connect() FAILED")
conout(oClient:GetErrorStr())
return .F.
Endif
oMemCache:Disconnect()
FreeObj(oMemCache)
oMemCache := NIL
oMemCache := NIL
cError := ''
conout("ZMEMCACHEDPOOL:GetCache() FAILED")
conout(cError)
return .F.
Endif
// (...)
// (...)
ZMEMCACHEDPOOL():ReleaseCache( @oMemCache )
Conclusão
Deixe um comentário
Introdução
O MemCached é um aplicativo que provê um cache de objetos em memória, do tipo chave/valor de
alto desempenho. Ele possui APIs Client para várias linguagens de mercado, e agora também terá a
sua API Client em AdvPL.
O MemCached
Open Source, Free , originalmente desenvolvido para Linux, ele também têm porte para Windows,
sobe originalmente com um limite de 64 MB de memória para uso com elementos em cache, aceita
conexões na porta 11211, em todas as interfaces de rede da máquina. Existe parametrização para
permitir mudar a porta e colocar mais de uma instância de MemCached na mesma máquina, definir
uso de mais memória, e inclusive permitir conexões apenas em uma interface de rede ou IP.
Recomendo a leitura da documentação — pelo menos a abordagem inicial — disponível no site
MemCached.ORG
Classe ZMEMCACHED
Como o mecanismo do MemCached trabalha com TCP/IP, a classe implementa a API de acesso para
as funcionalidades do MemCached. Damos o nome de Chave a um identificador de conteúdo, uma
string com até 150 bytes, e associamos a esta chave um conteúdo a ser colocado em cache. Este
conteúdo pode ser colocado no cache com ou sem um tempo de vida (expires). As funcionalidades
básicas incluem acrescentar, atualizar e remover uma tupla chave/valor do cache, incrementar ou
decrementar um valor em cache — neste caso o conteúdo em cache deve ser um número —
representado como string, obter status do cache, e limpar todo o cache. Vamos ao fonte: (fonte
zMemCached,prw)
#include "protheus.ch"
#include "zLibStr2HexDmp.ch"
METHOD Add() // Acrescenta uma chave / valor ( apenas caso nao exista )
METHOD _GetTCPError()
ENDCLASS
Método NEW
::cMemCacheIP := cIp
::nMemCachePort := nPorta
::nRvcTimeOut := 1000
::oTCPConn := tSocketClient():New()
::cError := ''
::cResponse := ''
::lVerbose := .F.
Return self
Método CONNECT
Local iStat
::cError := ''
::cResponse := ''
IF ::lVerbose
Endif
If ::oTCPConn:Isconnected()
Return .F.
Endif
::_GetTCPError()
Return .F.
Endif
Método DISCONNECT
Local nSend
::cError := ''
::cResponse := ''
If ::oTCPConn == NIL
Return .F.
Endif
if( ::oTCPConn:IsConnected() )
IF ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
Endif
::oTCPConn:CloseConnection()
::oTCPConn := NIL
Return .T.
Método GETVERSION
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
::cError := "Memcached client not connected."
Return .F.
Endif
IF ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
IF ::lVerbose
Endif
If Left(cRecvBuff,8)!='VERSION '
Return .F.
Endif
cVersion := ::cResponse
Return .T.
O método GetVersion() deve passar a variável cVersion por referência — prefixada com “@” na
chamada da função. Em caso de sucesso, a função retorna .T., e o valor da variável será atualizado.
Caso contrário, a variável será NIL, e a mensagem de erro correspondente está na propriedade
::cError
Método GETSTATS
Local nI , nT , aTmp
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
Return .F.
Endif
IF ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
If nRecv == 0
::_GetTCPError()
Return .F.
Endif
IF ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
// Recupera estatisticas
nT := Len(aTmp)
For nI := 1 to nT
If Left(aTmp[nI],5)=='STAT '
aadd(aStats , substr(aTmp[nI],6) )
Endif
Next
aSize(aTmp,0)
Return .T.
Cada instância do MemCached têm seus mecanismos internos de controle. Usando o método
GetStats(), podemos perguntar ao MemCached as estatísticas de uso até o momento. As
informações são retornadas por referência no Array aStats passado como parâmetro, onde cada
linha é uma string contento um identificador e seu respectivo valor, veja o exemplo abaixo:
pid 11128
uptime 10
time 1547326165
version 1.4.5_4_gaa7839e
pointer_size 64
curr_connections 10
total_connections 11
connection_structures 11
cmd_get 0
cmd_set 0
cmd_flush 0
get_hits 0
get_misses 0
delete_misses 0
delete_hits 0
incr_misses 0
incr_hits 0
decr_misses 0
decr_hits 0
cas_misses 0
cas_hits 0
cas_badval 0
auth_cmds 0
auth_errors 0
bytes_read 16
bytes_written 26
limit_maxbytes 67108864
accepting_conns 1
listen_disabled_num 0
threads 4
conn_yields 0
bytes 0
curr_items 0
total_items 0
evictions 0
reclaimed 0
Método _STORE
//
===============================================================================
Local nRecv
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
Return .F.
Endif
If !( ('.'+cMode+'.') $ ('.set.add.replace.append.prepend.cas.') )
Return .F.
Endif
// <mode> <key> <flags> <exptime> <bytes>
// ------------------------------------------
If nOptFlag == NIL
else
Endif
If nOptExpires == NIL
else
Endif
cSendCmd += cValToChar(len(cValue))
cSendCmd += CRLF
// ------------------------------------------
IF ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
// Etapa 02
nSend := ::oTCPConn:Send(cValue+CRLF)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
If ::lVerbose
Conout(Str2HexDmp(cValue+CRLF))
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
If nRecv == 0
::_GetTCPError()
Return .F.
Endif
If ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
cRecvBuff := strtran(cRecvBuff,CRLF,'')
If cRecvBuff != 'STORED'
Return .F.
Endif
Return .T.
O método _STORE é de uso interno da classe. Ele é usado pelos métodos públicos ADD, REPLACE e
SET. Internamente, a sintaxe dos comandos e o retorno é praticamente o mesmo para estas três
ações de armazenamento. Logo, optei por criar um método interno capaz de realizar as três
operações, e os três métodos públicos para consumir estas ações no fonte AdvPL.
Em cada um destes três métodos, informamos a chave de identificação do dado, o valor a ser
gravado em cache, e opcionalmente podemos especificar um tempo de vida (expires) em segundos
no cache. O default é 0 (zero=no expires). Todos os métodos acima retornam .F. quando a operação
não pode ser realizada.
ADD() somente var armazenar o valor caso a chave ainda não tenha sido gravada anteriormente.
Ela não atualiza valor de chave existente.
Replace() somente troca o valor de uma chave existente. Caso você tente trocar o valor de uma
chave que não existe, ela retorna uma condição de erro.
O método SET sempre atualiza o valor de uma chave, se ela ainda não existe no cache, ela é criada.
Método GET
Local nRecv
Local nPos
Local cLine
Local aTmp
Local cTeco
Local nSize
Local nSend
::cError := ''
::cResponse := ''
cValue := NIL
If !::oTCPConn:Isconnected()
return -1
Endif
If ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
If nRecv < 0
::_GetTCPError()
return -1
Endif
If nRecv == 0
::_GetTCPError()
return -1
Endif
If ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
// Parser do retorno
While !empty(cRecvBuff)
nPos := at(CRLF,cRecvBuff)
If nPos < 1
return -1
Endif
cLine := left(cRecvBuff,nPos-1)
cRecvBuff := substr(cRecvBuff,nPos+2)
If cLine == "END"
// acabaram os dados
// Sai do loop
EXIT
Endif
// varinfo("aTmp",aTmp)
// [1] "VALUE"
// [2] <key>
// [3] <flags>
// [4] <size>
nSize := val(aTmp[4])
// e acrescenta no buffer
cTeco := ''
nRecv := ::oTCPConn:Receive(@cTeco,::nRvcTimeOut)
If nRecv < 0
::_GetTCPError()
return -1
Endif
If nRecv == 0
::_GetTCPError()
return -1
Endif
If ::lVerbose
Conout(Str2HexDmp(cTeco))
Endif
cRecvBuff += substr(cTeco,1,nRecv)
If ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
Enddo
cValue := left(cRecvBuff,nSize)
// Ja desconsiderando o CRLF
cRecvBuff := substr(cRecvBuff,nSize+3)
aSize(aTmp,0)
EXIT
Else
return .F.
Endif
Enddo
If empty(cRecvBuff)
Return .T.
Endif
If left(cRecvBuff,5) == "END" + CHR(13)+Chr(10)
Return .T.
Endif
return .F.
O método GET foi feito para recuperar o valor em cache associado a uma chave. A variável para
receber o valor é informado por referência na chamada do método. O fonte é um pouco mais
“rebuscado” pois precisa permanecer recebendo dados do MemCache enquanto ele não enviar o
conteúdo inteiro armazenado.
Um detalhe importante: A função somente retorna .F. em caso de ERRO, por exemplo perda de
conexão ou resposta inesperada ou não tratada do MemCached. Se o valor a ser recuperado na
chave não existe no cache, isto não é considerado um erro, logo o método vai retornar .T. , e cabe
ao desenvolvedor verificar se o dado retornado por referência não está NIL.
Método DELETE
Usamos o método DELETE para remover uma chave e seu valor associado do cache.
Local cSendCmd
Local nRecv
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
Return .F.
Endif
// ------------------------------------------
If ::lVerbose
Conout("zMemCached:Delete() SEND")
Conout(Str2HexDmp(cSendCmd))
Endif
// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
If nRecv == 0
::_GetTCPError()
Return .F.
Endif
If ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
cRecvBuff := strtran(cRecvBuff,CRLF,'')
If cRecvBuff != 'DELETED'
Return .F.
Endif
Return .T.
Podemos armazenar no cache — usando Add ou Set — um vamor numérico representado em string
em uma determinada chave. E, usando os métodos Increment() e Decrement(), podemos
respectivamente aumentar ou diminuir o valor desta chave. Internamente o MemCached não vai
deixar duas operações de incremento rodar ao mesmo tempo. Cada operação realizada retorna o
novo valor da chave após a operação ser realizada. O valor é recuperado por reverência na chamada
do método, no parâmetro nValue.
Local nRecv
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
Return .F.
Endif
If nStep == NIL
cSendCmd += '1'
Else
cSendCmd += cValToChar(nStep)
Endif
cSendCmd += CRLF
If ::lVerbose
Conout("zMemCached:Increment() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
Conout(Str2HexDmp(cSendCmd))
Endif
// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
If nRecv == 0
::_GetTCPError()
Return .F.
Endif
If ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
// Parser do retorno
cRecvBuff := strtran(cRecvBuff,CRLF,'')
If !(left(cRecvBuff,1)$'0123456789')
::_GetTCPError()
Return .F.
Endif
nValue := val(cRecvBuff)
Return .T.
Local nRecv
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
Return .F.
Endif
If nStep == NIL
cSendCmd += '1'
Else
cSendCmd += cValToChar(nStep)
Endif
cSendCmd += CRLF
If ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
// Se tudo der certo, aqui eu devo receber o valor apos o decremento
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
If nRecv == 0
::_GetTCPError()
Return .F.
Endif
If ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
// Parser do retorno
cRecvBuff := strtran(cRecvBuff,CRLF,'')
If !(left(cRecvBuff,1)$'0123456789')
Endif
nValue := val(cRecvBuff)
Return .T.
Método FLUSH
E, para finalizar, se eu quiser evaporar com todo o conteúdo em cache — todas as chaves e valores
armazenadas — eu chamo o método Flush().
Local nSend
::cError := ''
::cResponse := ''
If !::oTCPConn:Isconnected()
Return .F.
Endif
IF ::lVerbose
Conout(Str2HexDmp(cSendCmd))
Endif
nSend := ::oTCPConn:Send(cSendCmd)
If nSend <= 0
::_GetTCPError()
Return .F.
Endif
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv == 0
::_GetTCPError()
Return .F.
Endif
If nRecv < 0
::_GetTCPError()
Return .F.
Endif
IF ::lVerbose
Conout(Str2HexDmp(cRecvBuff))
Endif
If Left(cRecvBuff,2)!='OK'
Return .F.
Endif
Return .T.
Programa de Testes
#include 'Protheus.ch'
#include 'zLibStr2HexDmp.ch'
// ---------------------------------------------------------------------------------
Local oClient
Local aStats := {}
Local nI , nX
Local xValue
Local nNewValue
oClient := ZMEMCACHED():New( TEST_HOST , TEST_PORT )
// oClient:lVerbose := .t.
IF !oClient:Connect()
conout("Falha de conexão...")
conout(oClient:cError)
return
Endif
If !oClient:GetVersion(@cVersion)
conout(oClient:cError)
return
endif
If !oClient:GetStats(@aStats)
conout(oClient:cError)
return
endif
If !oClient:Flush()
conout(oClient:cError)
return
endif
cValue1 := RandomStr(64)
cValue2 := RandomStr(64)
// Acrescenta o valor
conout(oClient:cError)
Return
Endif
Endif
conout(oClient:cError)
Return
Endif
// Deleta a chave
If !oClient:Delete( 'chave')
conout(oClient:cError)
Return
Endif
Endif
conout(oClient:cError)
Return
Endif
If !lOk
conout(oClient:cError)
Return
Endif
Conout(Str2HexDmp(xValue))
If ! (xValue == cValue1 )
UserException("Divergencia de valor")
Endif
If !lOk
conout("Retorno inesperado")
conout(oClient:cError)
Return
Endif
conout(oClient:cError)
Return
Endif
nNewValue := 0
conout(oClient:cError)
Return
Endif
conout("nNewValue = "+cValToChaR(nNewValue))
If nNewValue != 667
Endif
conout(oClient:cError)
Return
Endif
conout("nNewValue = "+cValToChaR(nNewValue))
If nNewValue != 666
Endif
Else
Endif
conout(oClient:cError)
Return
Endif
For nX := 1 to 4
// le direto o valor
Else
Conout(Str2HexDmp(xValue))
Endif
If !lOk
conout(oClient:cError)
Return
Endif
Sleep(1000)
Next
If !oClient:GetStats(@aStats)
conout(oClient:cError)
return
endif
oClient:Disconnect()
FreeObj(oClient)
Return
// ---------------------------------------------------------------------------------
// Função RandomStr()
While nSize>0
cRet += chr(randomize(32,128))
nSize--
enddo
Return cRet
O programa deve apenas emitir echo no log de console, ele apenas vai abortar a execução com um
erro caso ele não consiga conexão, ou no caso de algum comportamento inesperado do
MemCached. Os testes foram realizados com um MemCached 1.4.5 para Windows, mas a camada
TCP é a mesma para Linux.
Cache Distribuído
Quem já usa APIs de MemCached deve estar se perguntando: — Onde eu acrescento os servidores
do MemCached? Bem, esta é a primeira versão de client, então ela conecta com apenas uma
instância de MemCached. O MemCached foi construído para ser um cache distribuído, onde a
capacidade de montar um cluster e dividir os dados entre as instâncias online do MemCached é do
Client.
Por hora, a implementação deste client em AdvPL conversa com apenas uma instância, que pode
ser colocada por exemplo em uma máquina, onde existam mais de um serviço de Protheus Server
para consumir o cache.
Cuidados no Uso
Recomendo a leitura das recomendações de uso do MemCached no site da ferramenta, tanto sobre
dimensionamento como boas práticas de segurança.
Um serviço de MemCached normalmente pode ser acessado por mais de um programa Cliente
consumidor do Cache, então não crie nomes “curtos demais” para identificar as suas chaves,
inclusive preferencialmente crie um padrão de nomenclatura, como o environment + “_” + Modulo
ou programa + “_” + identificador do cache.
Conclusão
Por hora a API armazena basicamente um buffer em formato Caractere no AdvPL. Inclusive, pode
armazenar formato binário, como uma imagem. Caso seja necessário armazenar por exemplo um
Array ou outros valores, é necessário convertê-los para Caractere, e depois convertê-los de volta.
Estou estudando uma forma rápida de se fazer isso. Na continuação deste post, espero já ter
finalizado esta questão de forma elegante.
Fontes da ZLIB
Agradeço a todos novamente pela audiência, e lhes desejo TERABYTES DE SUCESSO !!!
Referências
https://memcached.org/
https://github.com/memcached/memcached/wiki
1 comentário
Introdução
Classe ZISAMFILE
Tudo o que é comum e exatamente igual na implementação de ambas as classes de acesso a DBF
em disco e em memória são parte de uma lógica de acesso e comportamento ISAM. Ao criar a classe
ZISAMFILE, ela passa a ter as propriedades e métodos comuns a ambas implementações, que são
removidas das respectivas implementações e colocadas nela.
A classe ZISAMFILE não tem construtor explícito, ela não têm um “New”. Mas não precisa, pois ela
não foi feita para ser instanciada diretamente. Ela deve ser a classe superior a ser herdada pelas
classes ZMEMFILE e ZDBFFILE, da seguinte forma:
// Ao invés de
// Agora temos
Métodos reimplementados
Existem alguns métodos comuns implementados tanto na classe filha como na classe pai. Ao
implementar na classe filha um método da classe pai, você pode ou não chamar o método da classe
pai de dentro da classe filha, quando o objetivo do método não é substituir a implementação da
classe pai, mas sim COMPLEMENTÁ-LA.
Por exemplo, cada uma das classes (ZDBFFILE e ZMEMFILE) possui propriedades específicas,
declaradas em sua definição. E, a classe pai ( ZISAMFILE) também tem as suas propriedades, comuns
a todas as heranças. Na implementação original, o método de uso interno da classe chamado
_InitVars() foi feito para justamente inicializar estas propriedades, e ele agora também foi
implementado na classe ZISAMFILE.
A forma correta e elegante de se fazer isso é: Cada método _InitVars() da sua classe inicializa as
propriedades da sua classe. E, as classes que herdam a ZISAMFILE -- no caso ZMEMFILE e ZDBFFILE
-- antes de mais nada chamam o método _InitVars() da classe superior (ZISAMFILE). Sendo assim, o
método _InitVars da classe ZMEMFILE ficou assim:
METHOD _InitVars() CLASS ZMEMFILE
_Super:_InitVars()
::aFileData := {}
::lOpened := .F.
::lExclusive := .F.
::lCanWrite := .T.
::dLastUpd := ctod("")
::aGetRecord := {}
::aPutRecord := {}
::lUpdPend := .F.
::lSetDeleted := .F.
::nRecno := 0
Return
Como eu disse, ainda existem propriedades em duplicidade implementadas nas classes ZMEMFILE
e ZDBFFILE, elas serão remanejadas em outro momento. Mas sabe o que é o mais lindo de tudo
isso?
Os programas de teste que usavam as classes continuam funcionando perfeitamente, pois todos
eles acessam as funcionalidades das classes através de métodos, até mesmo as propriedades são
retornadas por métodos — recurso também chamado de “Getters and Setters” — torne as
propriedades privadas da classe, e encapsule qualquer mudança de estado das propriedades em
métodos Set<Propriedade>(), e as consultas por métodos Get<Propriedade>()
A classe ZISAMFILE ficou com 700 linhas. Isto significa que cada fonte das classes ZMEMFILE e
ZDBFFILE agora tem cada um 700 linhas a menos, eliminando a duplicidade de código, e
implementando as funcionalidades na classe pai.
Outras mudanças
Aproveitando o momento de refatoração, a classe de índices em memória deixou de se chamar
ZDBFMEMINDEX e passou a ser ZMEMINDEX — afinal ela é usada pelos métodos e propriedades de
controle da implementação da ZISAMFILE. Outra alteração interessante era o processamento de
uma expressão AdvPL, onde era necessário trocar a ocorrência de campos na expressão pelo
o:FieldGet() do campo. Isto era feito exatamente da mesma forma tanto na classe de índice quanto
nas classes de ZDBFFILE e ZMEMFILE para aplicar filtros.
Agora, existe um método chamado _BuildFieldExpr(), que recebe uma string com uma expressão
AdvPL qualquer que use campos da tabela — onde todos os campos na expressão devem ser
colocados com letras maiúsculas — e retorna uma string com o texto do Codeblock com a expressão
resultante. Agora, quem precisa desta funcionalidade chama o método _BuildFieldExpr() da classe
ZISAMFILE, e com a expressão resultante, criar o Codeblock dinâmico com macro-execução e usar
conforme a necessidade.
GITHUB
Conforme o projeto vai sendo alterado e os fontes refatorados, novos recursos arquivos vão sendo
acrescentados no GITHUB, a versão mais atual de todos os fontes envolvidos está lá. Pode ser
necessário remover alguns fontes do projeto e recompilar os programadas para dar tudo certo. Em
breve os fontes das implementações de arquivo e implementações comuns vão fazer parte de um
novo projeto — Chamado “ZLIB”.
Conclusão
Eu deixo a conclusão desse post e da implementação para vocês. Espero que este exemplo sirva não
somente pela sua funcionalidade, mas como um modelo de boas práticas de desenvolvimento.
23/03/2015 ADVPL, Jogos, Orientação a Objeto ADVPL, Exemplos, Games, Interface, Orientação a
Objetos
Introdução
Nada mais providencial do que ter em mãos um fonte relativamente complexo, escrito utilizando
funções e variáveis estáticas e construído totalmente amarrado à camada de interface, para usar de
exemplo para aplicar a orientação a objetos.
Tetris em AdvPL
No post sobre o clone do jogo Tetris escrito em AdvPL, ao analisarmos o código-fonte, percebemos
que ele se encaixa perfeitamente na introdução deste tópico.
Embora o núcleo do jogo trabalhe com um array de strings contento a representação da tela do
jogo, onde cada caractere dentro do array representa um espaço em branco ou um espaço ocupado
pela parte de uma peça de uma determinada cor, as funções do jogo que lidam com os eventos de
atualização de interface estão amarrados às características e objetos de interface, tanto os
resources para a pintura da tela, quando o objeto tTimer, usado para movimentar a peça em jogo
uma linha para baixo em intervalos de um segundo.
Utilizando a orientação a objetos, e segmentando um pouco o código, é possível separar boa parte
das variáveis e funçoes estáticas em propriedades e métodos de uma classe ApTetris, que será
responsável pelo processamento do “core” (ou núcleo) do jogo. E, indo um pouco mais além,
podemos desamarrar a interface do jogo, fornecendo algumas propriedades para a classe, para esta
ser capaz de chamar as funções apropriadas para compor a interface do jogo quando e como
necessário.
Segmentação e refatoração
A primeira parte da refatoração do código foi criar a classe APTetris, transformando todas as
variáveis STATIC em propriedades da classe, e praticamente todas as funções STATIC em métodos.
Neste primeiro momento, em 10 minutos o jogo já estava operacional novamente, porém as
propriedades da classe armazenavam e lidavam diretamente com a pintura da interface.
Num segundo momento, este um pouco mais demorado, os métodos que lidavam estritamente com
as tarefas de pintar o grid, score, timer e mensagens do jogo, bem como as mudanças de estado
(running, pause, game over), passaram a chamar CodeBlocks para estas tarefas. Cada code-block é
chamado em um momento distinto, para atender a um tipo de evento disparado pelo Core do Jogo.
Todos os CodeBlocks foram implementados como propriedades da classe principal, e devem ser
alimentados após a construção da instância. E, os métodos ligados diretamente a interface voltaram
a ser STATIC FUNCTIONS do código, que recebem como parâmetro informações do core do jogo e
da interface em uso, para interagir com ela.
Vamos ao código
Após estas duas etapas completas com sucesso, algumas correções na lógica do jogo inicial foram
realizadas, commo por exemplo a funçao de “Pause” agora apaga a tela do jogo, para o jogador não
se aproveitar da pausa para ficar estudando onde melhor encaixar a peça, entre outros ajustes
menores. A última etapa foi isolar as constantes usadas para alguns elementos de arrays e status do
core do jogo para um arquivo de #include separado, e a classe do jogo, totalmente desamarrada de
interface ser isolada em um código-fonte separado.
Segue abaixo o fonte client, responsável pela interface e utilização da classe ApTetris.
#include 'protheus.ch'
#include 'tetris-core.ch'
/* ========================================================
Função U_TETRISOO
Data 21/03/2015
Versão 1.150321
A ou J = Move esquerda
D ou L = Move Direita
S ou K = Para baixo
======================================================== */
// =======================================================
Local nC , nL
Local oTetris
Local aBMPGrid
Local aBMPNext
Local aResources
aResources := {
"BLACK","YELOW2","LIGHTBLUE2","ORANGE2","RED2","GREEN2","BLUE2","PURPLE2" }
DEFINE DIALOG oDlg TITLE "Object Oriented Tetris AdvPL" FROM 10,10 TO 450,365 ;
For nL := 1 to 20
For nC := 1 to 10
aBMPGrid[nL][nC] := oBmp
Next
Next
For nL := 1 to 4
For nC := 1 to 5
aBMPNext[nL][nC] := oBmp
Next
Next
@ 90,120 SAY oScore PROMPT " " SIZE 60,120 OF oDlg PIXEL
@ 120,120 SAY oElapTime PROMPT " " SIZE 60,120 OF oDlg PIXEL
@ 140,120 SAY oGameMsg PROMPT " " SIZE 60,120 OF oDlg PIXEL
@ 480,10 BUTTON oDummyB0 PROMPT '&A' ACTION ( oTetris:DoAction('A') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB1 PROMPT '&S' ACTION ( oTetris:DoAction('S') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB2 PROMPT '&D' ACTION ( oTetris:DoAction('D') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB4 PROMPT '&J' ACTION ( oTetris:DoAction('J') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB5 PROMPT '&K' ACTION ( oTetris:DoAction('K') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB6 PROMPT '&L' ACTION ( oTetris:DoAction('L') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB7 PROMPT '&I' ACTION ( oTetris:DoAction('I') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB8 PROMPT '& ' ACTION ( oTetris:DoAction(' ') ) SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyB9 PROMPT '&P' ACTION ( oTetris:DoPause() ) SIZE 1, 1 OF oDlg PIXEL
oTetris := APTetris():New()
// Define um timer, para fazer a peça em jogo
// Apos uma ação ser processada pelo objeto Tetris, caso o score
// Apos uma ação ser processada pelo objeto Tetris, caso o tempo
// estado de jogo
// Apos processamento de ação, caso seja sorteada uma nova próxima peça,
// este bloco de código será disparado para pintar a proxima peça na interface
Return
/* -------------------------------------------------------
------------------------------------------------------- */
Local cMsg
If nStat == GAME_RUNNING
oTimer:Activate()
cMsg := "*********"+CRLF+;
"*********"
// Jogo em pausa
oTimer:DeActivate()
cMsg := "*********"+CRLF+;
"*********"
// Game Over
oTimer:DeActivate()
// e acresenta a mensagem de "GAME OVER"
cMsg := "********"+CRLF+;
"********"+CRLF+;
"********"
Endif
oGameMsg:SetText(cMsg)
Return
/* ----------------------------------------------------------
Função PaintGame()
---------------------------------------------------------- */
For nL := 1 to 20
cLine := aGameGrid[nL+1]
For nC := 1 to 10
nPeca := val(substr(cLine,nC+2,1))
If aBmpGrid[nL][nC]:cResName != aResources[nPeca+1]
aBmpGrid[nL][nC]:SetBmp(aResources[nPeca+1])
endif
Next
Next
Return
/* -----------------------------------------------------------------
----------------------------------------------------------------- */
For nL := 1 to 4
cLine := aNext[nL]
For nC := 1 to 5
nPeca := val(substr(cLine,nC,1))
If aBMPNext[nL][nC]:cResName != aResources[nPeca+1]
aBMPNext[nL][nC]:SetBmp(aResources[nPeca+1])
endif
Next
Next
Return
E, agora segue abaixo o fonte Tetris-Core.PRW, que contém a classe core do clone do Tetris.
#include 'protheus.ch'
#include 'tetris-core.ch'
// ============================================================================
// ============================================================================
CLASS APTETRIS
// Propriedades publicas
DATA aGamePieces // Peças que compoe o jogo
DATA bChangeState // CodeBlock para indicar mudança de estado ( pausa / continua /game over )
// Metodos Publicos
METHOD _DropDown() // Movimenta a peça corrente direto até onde for possível
METHOD _SetPiece(aPiece,aGrid) // Seta uma peça no Grid em memoria do jogo
ENDCLASS
/* ----------------------------------------------------------
Construtor da classe
---------------------------------------------------------- */
::aGamePieces := ::_LoadPieces()
::nGameTimer := 0
::nGameStart := 0
::aNextPiece := {}
::aGameCurr := {}
::nGameScore := 0
::aGameGrid := {}
::nGameStatus := GAME_RUNNING
Return self
/* ----------------------------------------------------------
---------------------------------------------------------- */
::aGameGrid := aClone(::_GetEmptyGrid())
nPiece := randomize(1,len(::aGamePieces)+1)
::aGameCurr := {nPiece,1,1,6}
::_SetPiece(::aGameCurr,::aGameGrid)
::aNextPiece := array(4,"00000")
::nNextPiece := randomize(1,len(::aGamePieces)+1)
aDraw := {::nNextPiece,1,1,1}
::_SetPiece(aDraw,::aNextPiece)
::nGameStart := seconds()
Eval(::bChangeState , ::nGameStatus )
cScore := str(::nGameScore,7)
/* ----------------------------------------------------------
---------------------------------------------------------- */
Local aOldPiece
If ::nGameStatus != GAME_RUNNING
Return .F.
Endif
cOldScore := str(::nGameScore,7)
cOldElapTime := STOHMS(::nGameTimer)
aOldPiece := aClone(::aGameCurr)
if cAct $ 'AJ'
::_DelPiece(::aGameCurr,::aGameGrid)
::aGameCurr[PIECE_COL]--
If !::_SetPiece(::aGameCurr,::aGameGrid)
::aGameCurr := aClone(aOldPiece)
::_SetPiece(::aGameCurr,::aGameGrid)
Endif
Elseif cAct $ 'DL'
::_DelPiece(::aGameCurr,::aGameGrid)
::aGameCurr[PIECE_COL]++
If !::_SetPiece(::aGameCurr,::aGameGrid)
::aGameCurr := aClone(aOldPiece)
::_SetPiece(::aGameCurr,::aGameGrid)
Endif
::_DelPiece(::aGameCurr,::aGameGrid)
// Rotaciona a peça
::aGameCurr[PIECE_ROTATION]--
If ::aGameCurr[PIECE_ROTATION] < 1
::aGameCurr[PIECE_ROTATION] := len(::aGamePieces[::aGameCurr[PIECE_NUMBER]])-1
Endif
If !::_SetPiece(::aGameCurr,::aGameGrid)
::_SetPiece(::aGameCurr,::aGameGrid)
Endif
::_MoveDown()
If cAct $ 'SK'
::nGameScore++
Endif
If !::_DropDown()
::_MoveDown()
Endif
Else
Endif
// Dispara a repintura do Grid
If ::nGameTimer < 0
::nGameTimer += 86400
Endif
cScore := str(::nGameScore,7)
cElapTime := STOHMS(::nGameTimer)
Endif
Endif
Return .T.
/* ----------------------------------------------------------
---------------------------------------------------------- */
Local nPaused
Local cElapTime
Local cOldElapTime
cOldElapTime := STOHMS(::nGameTimer)
If ::nGameStatus == GAME_RUNNING
lChanged := .T.
::nGameStatus := GAME_PAUSED
::nGamePause := seconds()
lChanged := .T.
::nGameStatus := GAME_RUNNING
nPaused := seconds()-::nGamePause
If nPaused < 0
nPaused += 86400
Endif
::nGameStart += nPaused
Endif
If lChanged
Eval(::bChangeState , ::nGameStatus )
If ::nGameStatus == GAME_PAUSED
Else
// Game voltou da pausa, pinta novamente o Grid
Endif
If ::nGameTimer < 0
::nGameTimer += 86400
Endif
cElapTime := STOHMS(::nGameTimer)
Endif
Endif
Return
/* ----------------------------------------------------------
Metodo SetGridPiece
Serve tanto para o Grid do Jogo quando para o Grid da próxima peça
---------------------------------------------------------- */
Local nL , nC
Local aTecos := {}
cPieceId := str(nPiece,1)
cPeca := ::aGamePieces[nPiece][1+nRotate][nL-nRow+1]
If nL > len(aGrid)
If '1' $ cPeca
Return .F.
Else
EXIT
Endif
Endif
cTecoGrid := substr(aGrid[nL],nCol,4)
For nC := 1 to 4
If Substr(cPeca,nC,1) == '1'
If SubStr(cTecoGrid,nC,1) != '0'
Return .F.
Endif
cTecoGrid := Stuff(cTecoGrid,nC,1,cPieceId)
Endif
Next
aadd(aTecos,cTecoGrid)
Next
aGrid[nL] := stuff(aGrid[nL],nCol,4,aTecos[nL-nRow+1])
Next
Return .T.
/* -----------------------------------------------------------------
aLPieces[1][1] C "O"
aLPieces[1][2][1] "0000"
aLPieces[1][2][2] "0110"
aLPieces[1][2][3] "0110"
aLPieces[1][2][4] "0000"
----------------------------------------------------------------- */
Local aLPieces := {}
aadd(aLPieces,{'O', { '0000','0110','0110','0000'}})
// Peça "I" , em pé e deitada
aadd(aLPieces,{'I', { '0000','1111','0000','0000'},;
{ '0010','0010','0010','0010'}})
aadd(aLPieces,{'S', { '0000','0011','0110','0000'},;
{ '0010','0011','0001','0000'}})
aadd(aLPieces,{'Z', { '0000','0110','0011','0000'},;
{ '0001','0011','0010','0000'}})
aadd(aLPieces,{'L', { '0000','0111','0100','0000'},;
{ '0010','0010','0011','0000'},;
{ '0001','0111','0000','0000'},;
{ '0110','0010','0010','0000'}})
aadd(aLPieces,{'J', { '0000','0111','0001','0000'},;
{ '0011','0010','0010','0000'},;
{ '0100','0111','0000','0000'},;
{ '0010','0010','0110','0000'}})
aadd(aLPieces,{'T', { '0000','0111','0010','0000'},;
{ '0010','0011','0010','0000'},;
{ '0010','0111','0000','0000'},;
{ '0010','0110','0010','0000'}})
Return aLPieces
/* ----------------------------------------------------------
Função _MoveDown()
---------------------------------------------------------- */
Local aOldPiece
Local nMoved := 0
If ::nGameStatus != GAME_RUNNING
Return
Endif
aOldPiece := aClone(::aGameCurr)
::_DelPiece(::aGameCurr,::aGameGrid)
::aGameCurr[PIECE_ROW]++
If ::_SetPiece(::aGameCurr,::aGameGrid)
Return
Endif
::aGameCurr := aClone(aOldPiece)
::_SetPiece(::aGameCurr,::aGameGrid)
::nGameScore += 4
::_FreeLines()
nPiece := ::nNextPiece
If !::_SetPiece(::aGameCurr,::aGameGrid)
::nGameStatus := GAME_OVER
Eval(::bChangeState , ::nGameStatus )
Return
Endif
::aNextPiece := array(4,"00000")
::nNextPiece := randomize(1,len(::aGamePieces)+1)
Local aOldPiece
Local nMoved := 0
If ::nGameStatus != GAME_RUNNING
Return .F.
Endif
aOldPiece := aClone(::aGameCurr)
::_DelPiece(::aGameCurr,::aGameGrid)
::aGameCurr[PIECE_ROW]++
While ::_SetPiece(::aGameCurr,::aGameGrid)
nMoved++
// Incrementa o Score
::nGameScore++
::_DelPiece(::aGameCurr,::aGameGrid)
aOldPiece := aClone(::aGameCurr)
// Desce a peça mais uma linha pra baixo
::aGameCurr[PIECE_ROW]++
Enddo
::aGameCurr := aClone(aOldPiece)
::_SetPiece(::aGameCurr,::aGameGrid)
/* -----------------------------------------------------------------------
----------------------------------------------------------------------- */
Local nL, nC
If nL > len(aGrid)
Return
Endif
cTecoGrid := substr(aGrid[nL],nCol,4)
For nC := 1 to 4
If Substr(cTecoPeca,nC,1)=='1'
cTecoGrid := Stuff(cTecoGrid,nC,1,'0')
Endif
Next
aGrid[nL] := stuff(aGrid[nL],nCol,4,cTecoGrid)
Next
Return
/* -----------------------------------------------------------------------
----------------------------------------------------------------------- */
Local nErased := 0
Local cTecoGrid
For nL := 21 to 2 step -1
cTecoGrid := substr(::aGameGrid[nL],3)
If !('0'$cTecoGrid)
adel(::aGameGrid,nL)
ains(::aGameGrid,1)
::aGameGrid[1] := GRID_EMPTY_LINE
nL++
nErased++
Endif
Next
If nErased == 4
::nGameScore += 100
ElseIf nErased == 3
::nGameScore += 50
ElseIf nErased == 2
::nGameScore += 25
ElseIf nErased == 1
::nGameScore += 10
Endif
Return
/* ------------------------------------------------------
------------------------------------------------------ */
Local aEmptyGrid
aEmptyGrid := array(21,GRID_EMPTY_LINE)
Return aEmptyGrid
/* ------------------------------------------------------
------------------------------------------------------ */
Local nHor
Local nMin
nHor := int(nSecs/3600)
nSecs -= (3600*nHor)
nMin := int(nSecs/60)
nSecs -= (60*nMin)
Return strzero(nHor,2)+':'+Strzero(nMin,2)+':'+strzero(nSecs,2)
Conclusão
A implementação realizada poderia ser mais refinada ou flexível, mas atende a esta necessidade.
Uma outra alternativa interessante, ao invés de criar vários CodeBlocks, um para cada evento, seria
criar apenas um CodeBlock e passar ele em uma propriedade da classe, e através dele fazer todas
as chamadas de interface, passando como parâmetros a instância do jogo (self), um código para
indicar qual evento está sendo disparado, e um ou mais parâmetros especificos do evento. Neste
caso, o fonte de interface teria que construir uma função única de “CallBack”, onde dentro dela cada
evento seria tratado em um DO CASE…END CASE, por exemplo.
Agora, dêem uma olhada no código antigo, todo “amarrado”, e no código novo. Pra mim é
visivelmente mais fácil dar manutenção no código orientado a objetos do que no código procedural,
pois cada coisa está visivelmente em seu lugar, e cada parte do código têm a sua responsabilidade
e atribuições bem definidas. Espero que vocês tirem proveito da orientação a objeto, com tanta
satisfação como a que eu tenho em escrever estes códigos !!
7 Comentários
06/12/2014 ADVPL, Orientação a Objeto ADVPL, Boas Práticas, Classes, Orientação a Objetos
E, para finalizar a a introdução do tópico “Classes em Advpl”, hoje vamos abordar algumas boas
práticas da orientação a objeto, com foco no uso com ADVPL.
Simplicidade
A correta representação do domínio do problema deve ser simples, mesmo para um problema
complexo. Por exemplo, fazer um jogo de damas ou xadrez no computador pode parecer algo muito
complexo, certo ? Com apenas uma classe de tabuleiro, uma classe de jogador, uma classe base (ou
abstrata) para uma peça genérica, e uma classe para cada peça do tabuleiro, onde a instância de
tabuleiro é responsável por fazer a interface com o usuário e permitir ele mover uma peça de um
lugar de origem para um lugar de destino, eu garanto que fica mais simples. A interface recebe o
input do jogador, e aciona o método de mover peça do tabuleiro, que verifica se tem uma peça na
posição de origem, e chama um método da peça para listar as posições válidas para onde a peça
pode ser movida. Cada objeto tem a sua camada de inteligência (métodos) e validações.
A implementação feita desta forma fica isolada em cada peça, afinal você precisa escrever apenas
um método para cada peça para determinar as posições possiveis de movimento a partir do
tabuleiro em um determinado estado, onde um metodo do tabuleiro se encarrega de varrer a lista
de peças em jogo de um dos jogadores e perguntar para cada uma para onde ela pode mover-se.
Com isso é mais fácil implementar a mecânica dos movimentos das peças, e até um mecanismo de
projeção de movimentos possíveis do adversário.
Quanto maior o detalhamento que você precisa, maior será a quantidade de classes e propriedades
necessárias para lhe atender. Atenha-se ao que você precisa, e de forma ordenada. Por exemplo,
ao prototipar três classes, A , B, e C, onde B e C herdam A, na classe superior (A) você deve colocar
propriedades que são comuns a todas as classes da herança, e nas classes filhas apenas as
propriedades e métodos específicos que somente caberiam na instância da classe filha,
permanecendo os métodos comuns na classe pai. Muitas vezes implementamos uma herança sem
ter propriedades específicas, mas implementações de métodos com comportamentos diferenciados
por instância.
Uma instância de uma classe na linguagem ADVPL não possui declaração explícita de métodos
destrutores, porém o kernel do ADVPL realiza um controle de reaproveitamento de memória da
instância da classe, e mantém a instância na memória, mesmo que ela não seja mais referenciável,
apenas eliminando a memória consumida pela instância quando a função que cria a instância da
classe é chamado e cria uma nova instância. A memória ocupada pela instância envolve todas as
propriedades da instância.
Logo, é elegante e saudável para a memória você criar um método “CleanUp” na classe para limpar
as propriedades que não estão sendo mais referenciadas desta instância, uma vez que a mesma não
seja mais necessária, e após chamar o CleanUp() da instância, você executa a função FreeObj(),
passando a variável que contém a instância como parâmetro.
Se você executar um FreeObj() em uma instância de classe ADVPL, mas ela ainda estava sendo
referenciada em uma ou mais varíaveis ou propriedades de outras classes, automaticamente estas
referências tornam-se nulas (NIL). Caso algum programa tente acessá-las, será gerada uma
ocorrência de erro “Variable is not an object”.
A função FreeObj() também serve para eliminar uma classe da interface visual do Advpl. Neste caso,
muito cuidado com o seu uso, pois se você por exemplo executar um FreeObj() em uma instância
de tWindow, tDialog, tPanel, ou qualquer instância de container de interface, que está ativa na tela
e na pilha de execuções, você pode provocar uma invasão de memória ( Access Violation ou
Segment Fault ).
A dica de limpeza vale também para funções em Advpl, onde as variáveis locais daquela execução
permanecem alocadas na memória, somente sendo desalocadas em uma próxima execução da
função. Por exemplo, uma função de processamento intermediário cria um array local dentro do
fonte, e popula este array para fazer um cálculo. Se o retorno desta função não for o próprio array,
o conteúdo alocado na memória pelos elementos não será necessário e nem acessível quando a
função retornar o valor calculado, mas a área de memória ocupada vai permanecer alocada. Neste
caso, você deve limpar o array, usando a função de redimensionamento de array da seguinte forma:
aSize(aVarArray,0) — onde aVarArray é a variável que contém o array a ser limpo.
Caso as propriedades da classe apontem para arrays de outros objetos, que estão compartilhados
com outros componentes e não exatamente devem ser destruídos, é interessante e elegante que
você atribua NIL nestas propriedades, para elas deixarem de referenciar os arrays e objetos em
questão.
Performance
Por exemplo, em uma rotina de processamento onde os métodos realizam tarefas, decisões e
manipulações de dados e propriedades, e podem acessar banco de dados ou informações no disco,
o tempo de processamento do método vai ser muito maior do que o tempo da chamada. Ao
calcularmos o tempo total de processamento de 1 milhão de requisições de um determinado
método, onde cada requisição demora em média 1/10 de segundo, serão 166 minutos (duas horas
e 46 minutos) de processamento, mais quatro segundos do tempo gasto com as chamadas dos
métodos. Se este loop fosse feito com chamadas de função, acrescentaríamos ao tempo total
apenas 1,5 segundo. Esta diferença de tempo, em um processo de 166 minutos, não quer dizer nada.
Este overhead somente torna-se significativo quando os métodos são extremamente curtos, como
por exemplo operações aritméticas ou apenas encapsulamento para retorno de propriedades. E
mesmo assim, são necessárias milhões de requisições para isso tornar-se perceptível.
Conclusão
A orientação a objetos é um paradigma muito interessante de ser explorado, mas como toda a
solução em tecnologia da informação, existem casos onde uma abordagem com funções pode ser
mais interessante, até mesmo dentro do mesmo aplicativo. Prevalece sempre a análise de caso, use
um paradigma ou abordagem para resolver os problemas onde ela apresenta a melhor relação custo
x benefício.
Posteriormente eu devo voltar no tema de orientação a objetos, focando mais em exemplos práticos
e casos de uso em ADVPL.
3 Comentários
No tópico anterior, vimos um exemplo de uma classe ADVPL herdando outra classe em ADVPL.
Vimos também que a herança não pode ser múltipla, isto é, uma classe não pode herdar mais de
uma classe pai ao mesmo tempo, e vimos também que é possível herdar uma classe que já possua
herança. Agora, vamos criar uma classe ADVPL que herda uma classe básica da linguagem. Vamos
criar uma classe de botão diferenciada, herdando a classe básica de botão do ADVPL (tButton).
Desta vez sem muita teoria, os dois tópicos anteriores já cuidaram dessa parte ! Agora, as
explicações ficam pro final do tópico, vamos ao código: Crie um fonte novo (extensão .PRW), copie,
cole, salve, compile e execute U_APTST03 através do Smartclient.
// --------------------------------------------------
DEFINE DIALOG oDlg TITLE "Exemplo de Herança" FROM 10,10 TO 150,300 COLOR
CLR_BLACK,CLR_WHITE PIXEL
Return
// ------------------------------------------------------------
METHOD Hide()
METHOD Show()
ENDCLASS
:New(nTop,nLeft,cCaption,oParent,bAction,nWidth,nHeight,NIL,NIL,NIL,.T.)
::SetColor(CLR_WHITE,CLR_BLACK)
_Super:Hide()
Return self
Return _Super:Hide()
Return _Super:Show()
As diferenças
Os dois botões são criados de formas diferentes, o botão oBtn1 usando a definição padrão do
ADVPL, e o oBtn2 usando a nossa classe APBUTTON, que herda TBUTTON. A primeira diferença é o
construtor. A herança de classe básica do ADVPL exige que a primeira linha do método construtor
chame o construtor da classe pai da herança, usando apenas
“:”+nomedoconstrutor+”(“+parâmetros+”)” . A utilização da diretiva “_Super:” dentro da
implementação dos métodos funciona da mesma forma, exceto para a chamada do construtor, que
exige a grafia diferenciada.
Ao executar o programa acima, devemos ver um botão do ADVPL, que ao ser clicado mostra o novo
botão APBUTTON e esconde o botão pressionado. Antes de mostrar o novo botão, como o método
Hide() foi reimplementado, será mostrada uma mensagem informativa. A mesma coisa acontece
para o método Show(), porém apenas do botão implementado com a classe APBUTTON. A ação do
botão APBUTTON será mostrar novamente o botão ADVPL e esconder-se.
Os limites
Bem, até aqui tudo é lindo, mas estes recursos possuem alguns limites específicos. Atualmente, uma
classe ADVPL que herda uma classe do binário não pode ser herdada por outra classe ADVPL. Caso
você tente por exemplo criar uma classe APBUTTON2 que herda APBUTTON, a mesma vai compilar,
mas na hora de executar será gerado um erro de inicialização do construtor nos níveis superiores.
Já a herança de classe ADVPL atualmente suporta apenas 2 níveis de herança. Por exemplo, classe
FILHA FROM PAI, NETA FROM FILHA. Se você implementar a classe BISNETA e tentar herdar a classe
NETA, ao executar por exemplo o construtor da BISNETA, onde haverá uma cascata de _Super para
os construtores das camadas superiores, ( BISNETA -> NETA -> FILHA -> PAI ), a execução dos
construtores entra em loop, finalizando o processo em execução com uma ocorrência de erro Advpl
“stack depth overflow”.
As boas práticas
Conclusão
Estes tópicos servem como uma base, uma introdução ao assunto com alguns detalhes. O que vai
fazer a diferença na utilização destes recursos é você pesquisar mais sobre o tema, e começar a usá-
los no seu dia a dia, a experiência adquirida com alguns calos nos dedos e neurônios queimados
usando estes recursos é que vai fazer a diferença. Na TDN, existe um guia completo das classes de
interface visual e não-visual da linguagem Advpl, no link
“http://tdn.totvs.com/pages/viewpage.action?pageId=6063177“. Para absorver este conteúdo, ler
não é o bastante … é apenas o princípio !
Até o próximo post, pessoal 😉
17 Comentários
Continuando de onde paramos nas classes, vamos ver agora como criar duas classes em ADVPL,
onde uma classe herdará a outra. Lembrando que ambas são casses cujo fonte é escrito em ADVPL
e compilado no Repositório. Posteriormente vamos ver como codificar uma classe ADVPL herdando
uma classe básica la linguagem ADVPL, implementada no TOTVS Application Server.
Quando criamos uma classe em Advpl herdando outra classe em Advpl, apenas especificamos na
classe filha quem é a classe pai de onde herdamos todos os métodos e propriedades. Como não há
escopo restrito para os métodos, todos os métodos são virtuais (isto é, podem ser re-
implementados na classe filha).
E, é claro, de dentro de um método da classe filha, podemos chamar qualquer método das classes
superiores. Em ADVPL não é permitido o recurso de herança múltipla ( onde uma classe filha pode
herdar mais de uma classe pai simultâneamente).
Vamos implementar duas classes, uma classe pai e uma classe filha herdando a classe pai, e
sobrescrevendo um de seus métodos. Vamos implementar um arroz com feijão e depois brincar um
pouco com ela.
Partindo da premissa que você tenha acesso a um ambiente do ERP Microsiga Protheus, com um
IDE / TDS para compilar código ADVPL, crie um novo fonte para testes, chamado APHELLO.PRW,
acrescente-o ao seu projeto ( crie um novo apenas para testes), e entre com o código abaixo. Pode
copiar e colar que funciona 😉
#INCLUDE "protheus.ch"
oObj := APFILHA():New(123)
oObj:SayValue()
Return
// -----------------------------------------------------------
CLASS APPAI
METHOD SayValue()
ENDCLASS
::nValue := nNum
Return self
MsgInfo(::nValue,"Classe Pai")
Return
// -----------------------------------------------------------
ENDCLASS
_Super:New(nNum)
return self
_Super:SayValue()
Else
MsgInfo(::nValue,"Classe Filha")
Endif
Return
Após compilar este fonte, você pode chamar a função U_APTST2 diretamente a partir do
Smartclient, e deve ser apresentado na sua interface uma caixa de diálogo perguntando se você
quer chamar o método da classe pai. Caso você responda sim, deve ser mostrada uma caixa de
diálogo contendo o valor 123 guardado na propriedade nValue da classe pai, onde o título da janela
é “Classe Pai”. Caso contrário, será mostrada uma caixa de diálogo com o mesmo valor, onde a
propriedade nValue da classe pai foi acessada de dentro do método da classe filha — repare no
título diferenciado das caixas de diálogo das duas implementações. Agora vamos olhar com uma
lupa.
Na declaração da classe APFILHA, após o nome da classe usarmos a instrução FROM, seguido do
nome da classe pai, chamada APPAI. Dada a natureza dinâmica das classes Advpl, a classe APPAI
usada na herança pode estar em outro fonte / arquivo.
Para chamar um método da classe superior, usamos a palavra reservada “_Super”. Ela deve ser
usada apenas dentro da implementação (corpo) de um método, e deve ser escrita exatamente
assim, com as letras maiúsculas e minúsculas desta forma (esta palavra reservada é case-sensitive),
e somente pode ser usada para referenciar um método do nível superior (classe pai) da herança.
No caso da herança ADVPL, a classe filha não precisa chamar explicitamente o construtor da classe
pai, mas isto é uma boa prática de desenvolvimento na orientação a objetos. Como a herança das
classes ADVPL permite você herdar uma classe que herda de outra classe (CLASS APNETA FROM
APFILHA), caso a classe APPAI possua um método que não foi reimplementado na classe APFILHA ,
mas a classe APNETA quer utilizá-lo, basta esta referenciar o método usando _Super. A busca nos
níveis superiores da herança é recursiva e automática para métodos e propriedades.
Onde foi parar o self ?
No exemplo de classes do post anterior, dentro da implementação dos métodos, para referenciar
uma propriedade da minha instância, foi usada a variável “self”, e no exemplo atual usamos “::”. A
única diferença entre eles é a grafia. A sequencia de dois caracteres dois-pontos juntos “::” é um
#translate de compilação, que é traduzido para “self:”. Utilizar o “::” ao invés de “self:” é a
convenção de grafia mais usual.
Conclusão
A cada passo da orientação a objeto vamos estudar um pouco mais de como ela funciona, bem
como as melhores formas de tirar proveito dessa tecnologia! No próximo post sobre Classes em
ADVPL vamos ver as diferenças para codificar uma classe ADVPL que herda diretamente de uma
classe de objeto de interface visual da linguagem!
4 Comentários
Vamos sair um pouco da linha teórica dos posts anteriores, e entrar em outro assunto legal de
desenvolvimento: A orientação a objetos, com foco no ADVPL.
Introdução
O paradigma da orientação a objetos é a melhor estratégia parar representar de forma mais fiel o
mundo real do domínio de um problema em um conjunto de componentes de software. Este
modelo, quando corretamente aplicado, permite um alto nível de abstração do algoritmo e uma
implementação limpa, especializada e flexível, onde as classes representam a estrutura de atributos
(propriedades) e sua ações (métodos).
Um pouco de teoria
Não há como abordar diretamente este tema sem antes contar um pouco das características atuais
da orientação a objetos em ADVPL, ainda mais para leitores que já trabalham com OOP (Object
Oriented Programming) em outras linguagens.
A orientação a objeto em ADVPL exige a prototipagem de propriedades e métodos, inclusive existem
diretivas até para prototipagem das propriedades, porém dada a natureza da dinâmica do ADVPL,
fazer a tipagem das propriedades é informativa, e a prototipagem é feita no fonte, e não em headers
(arquivos com extensão “.ch”). A implementação dos métodos é feita no mesmo fonte onde foi feita
a prototipagem, e quando um outro fonte vai consumir uma determinada classe, não existe a
necessidade de refazer a prototipagem neste fonte ou usar algum #include, a resolução da
existência da classe é dinâmica, isto é, realizada em tempo de execução. Isto dá bastante
flexibilidade, mas o programador precisa estar atento ao que deve ser informado nas propriedades
e nos parâmetros dos métodos.
Nas classes implementadas diretamente no ADVPL, todas as propriedades são públicas e permitem
leitura e escrita, bem como seus métodos. Pode ser usada a herança, porem sempre herança
simples, e o escopo é invariavelmente público. Você pode reimplementar métodos na herança, mas
não propriedades, e existe uma forma de, dentro de um método re-implementado em uma classe
filha, chamar o método da classe pai. Vamos entrar em exemplos dessa natureza, mas primeiro vêm
o arroz com feijão.
Partindo da premissa que você tenha acesso a um ambiente do ERP Microsiga Protheus, com um
IDE / TDS para compilar código ADVPL, crie um novo fonte para testes, chamado APHELLO.PRW,
acrescente-o ao seu projeto ( crie um novo apenas para testes), e entre com o código abaixo, sem
copiar a numeração de linha — colocada neste exemplo apenas para facilitar a explicação do código
neste post.
02.
05. oObj:SayHello()
06. Return
07.
13.
17.
19. MsgInfo(self:cMsg)
Após compilar este fonte, você pode chamar a função U_APTST diretamente a partir do Smartclient,
e deve ser apresentado na sua interface uma caixa de diálogo contendo o texto “Olá mundo Advpl”
e um botão “OK” logo abaixo.
Na linha 01, o uso do #include ‘protheus.ch’ (ou ‘totvs.ch’) possibilita a utilização das diretivas de
orientação a objeto e declaração de classes, entre outras.
Na linha 03, declaramos a User Function APTST, que será a função Advpl que vai consumir a classe
APHELLO e demonstrar seu uso.
Na linha 04 declaramos uma variável local (oObj), que recebe a instância da classe APTST. Para tal,
realizamos uma chamada do método construtor, usando o nome da classe seguido de “()”, como se
a mesma fosse uma função, porém seguido de “:” e a chamada do construtor ( NEW ), informando
como parâmetro uma string, que será armazenada pelo construtor na propriedade cMsg da
instância atual.
Na linha 05, chamamos o método SayHello() para a instância da classe armazenada nesta variável,
havendo então o resultado de tela esperado.
Entre as linhas 08 e 12, temos o bloco de declaração ou prototipação da classe, onde declaramos a
propriedade cMSg, e os métodos New() e SayHello(). Como eu já havia mencionado, eu posso
informar na declaração da propriedade o tipo esperado de seu conteúdo, mas atualmente para
classes em ADVPL isto é meramente informativo(*).
Uma instância de qualquer classe, armazenada em uma variável do tipo “O” (Objeto), ao ser
atribuída ou ser passada como parâmetro, sempre aponta para uma referência da instância, da
mesma forma que um bloco de código ou array.
Conclusão
Parar um primeiro exemplo e introdução ao assunto, eu acho que até aqui já ficou legal, senão o
post fica quilométrico, e esse tema será detalhado em posts subsequentes. No próximo post sobre
o assunto, vamos ver — com exemplos — a criação de uma classe com herança no ADVPL.
Referências
ORIENTAÇÃO A OBJETOS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014.
Disponível em:
<http://pt.wikipedia.org/w/index.php?title=Orienta%C3%A7%C3%A3o_a_objetos&oldid=4061131
8>. Acesso em: 1 dez. 2014.
Observações
(*) A declaração da tipagem para classes ADVPL, declaradas com CLASS … ENDCLASS atualmente é
meramente informativa, porém existem classes no ADVPL, como as classes Client e Server de
WebServices, onde a tipagem é consistida pela camada de funções de Framework ADVPL criadas
para este fim.