Você está na página 1de 120

ZLIB Framework – Parte 01

11/02/2019 ADVPL, Boas Práticas de Programação, Infraestrutura, Orientação a Objeto


Performance, Programação

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.

Na ciência da computação, biblioteca é uma coleção de subprogramas utilizados no


desenvolvimento de software. Bibliotecas contém código e dados auxiliares, que provém serviços a
programas independentes, o que permite o compartilhamento e a alteração de código e dados de
forma modular. Alguns executáveis são tanto programas independentes quanto bibliotecas, mas a
maioria das bibliotecas não são executáveis.

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.

Orientação a Objetos e Abstração

Estas são duas chaves importantes no reaproveitamento de código e desenvolvimento modular. A


orientação a objetos nos permite criar classes com uma finalidade (abstração) e implementar para
múltiplos cenários ou recursos.

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

Um dos primeiros mandamentos da criação de componentes é : A CRIAÇÃO DE QUALQUER


COMPONENTE DEVE SER MOTIVADA PELA NECESSIDADE. Criar componentes adicionais ou agregar
funcionalidades demais a um componente só por que vai ser “legal” só engorda código. Limite-se a
uma funcionalidade por classe, e coloque nela o que realmente é comum a todos. Exceções são
tratadas na implementação, a abstração é genérica.

Quando aos níveis de implementação — ou camadas — normalmente os componentes de alto nível


são construídos para usar os de mais baixo nível. Na prática eles são construídos para usar todas as
implementações feitas sobre uma abstração. Por exemplo, um componente de CRUD feito para usar
a abstração ZISAMFILE pode usar qualquer implementação feita sobre ela, como a ZDBFFILE,
ZTOPFILE, ZMEMFILE…

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

Criar um conjunto de funções e funcionalidades que, permitam escrever programas, funções e


rotinas, separando totalmente o processamento da interface, focando em SOA utilizando micro-
serviços, filas e controladores, com foco em desempenho, escalabilidade, resiliência e alta
disponibilidade.

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

BIBLIOTECA (COMPUTAÇÃO). In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation,


2018. Disponível em:
<https://pt.wikipedia.org/w/index.php?title=Biblioteca_(computa%C3%A7%C3%A3o)&oldid=5372
2844>. Acesso em: 30 nov. 2018.

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

Abstração de Acesso a Dados e Orientação a Objetos – Parte 05

20/01/2019 ADVPL, Banco de Dados, Orientação a Objeto Classes, Programação

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

Quando queremos um result-set de uma Query em AdvPL, usamos a função TCGenQry() em


conjunto com a função DBUseArea(), para abrir um cursor READ ONLY e FORWARD ONLY usando
um ALIAS em AdvPL.

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:

DbSkip() — Avança um registro no cursor. Podemos especificar um número de registros positivo


para avançar, mas nunca negativo. Lembre-se, cursor FORWARD ONLY.

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.

DbCloseArea() — Fecha o alias da Query

FieldGet() — Recupera o valor de um campo da query pelo número do campo

DbSTruct() — Retorna o array com a definição dos campos retornados na Query.

FieldPos() — Retorna a posição de um campo na estrutura a partir do nome.

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

O DBAccess permite eu criar um arquivo em um banco relacional (SQL) homologado, e emular um


acesso ISAM neste arquivo, como se ele fosse um arquivo DBF. Toda essa infra-estrutura já está
pronta e operacional dentro do Protheus Server. O objetivo desta classe é fazer o mesmo
encapsulamento de acesso feito pela ZQUERYFILE, porém implementando todas as funcionalidades
disponíveis, como filtros e índices, além da navegação entre registros pela ordem de um índice.

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.

Uso após a implementação

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.

Idéias e mais idéias

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

Quando começamos a encapsular recursos, realmente conseguimos reaproveitar muito código. Os


exemplos dos posts anteriores de CRUD em AdvPL — usando SmartClient e AdvPL ASP — foram
úteis para mostrar o que pode ser feito usando instruções básicas da linguagem AdvPL. Agora, com
o agrupamento de algumas funcionalidades em um Framework (zLib), podemos reaproveitar código
e implementar funcionalidades similares com menos esforço. As classes mencionadas acima ainda
estão em desenvolvimento, e serão acompanhadas de novos posts assim que estiverem “prontas”
para uso !!!

Agradeço novamente a audiência e lhes desejo TERABYTES DE SUCESSO !!!


2 Comentários

Abstração de Acesso a Dados e Orientação a Objetos – Parte 04

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.

SDF Text File Format Specifications

------------------------------------------------------------------------

File Element Format

------------------------------------------------------------------------

Character fields Padded with trailing blanks

Date fields yyyymmdd

Logical fields T or F

Memo fields Ignored

Numeric fields Padded with leading blanks for zeros

Field separator None

Record separator Carriage return/linefeed

End of file marker 1A hex or CHR(26)

------------------------------------------------------------------------
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.

Então, como eu faço isso?

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.

User Function TstExport()

Local cQuery

Local nH

nH := tclink("MSSQL/DBLIGHT","localhost",7890)

IF nH < 0

MsgStop("TCLINK ERROR - "+cValToChar(nH))

Return

Endif

// Abre uma Query no AdvPL

cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"

USE (tcGenQry(,,cQuery)) ALIAS QRY SHARED NEW VIA "TOPCONN"

TCSetField("QRY","VALOR","N",12,2)

// Cria um arquivo em memoria com a Query usando o ALIAS / WORKAREA

oMemory := ZMEMFILE():New('qrymemory')

oMemory:CreateFrom("QRY",.T.)

// Fecha a Query
USE

// Exporta os registros para SDF

oMemory:Export("SDF",'\temp\tstexport.sdf' )

// Exporta os registros para CSV

oMemory:Export("CSV",'\temp\tstexport.csv' )

// Exporta para JSON

oMemory:Export("JSON",'\temp\tstexport.json' )

// Fecha a tabela temporária em memória

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:

METHOD Export( cFormat, cFileOut ) CLASS ZISAMFILE

Local nHOut

Local nPos

Local cBuffer := ''

Local lFirst := .T.

// Primeiro, a tabela tem qye estar aberta

IF !::lOpened

UserException("ZISAMFILE:EXPORT() Failed - Table not opened")


Return .F.

Endif

cFormat := alltrim(Upper(cFormat))

If cFormat == "SDF"

// Formato SDF

// Texto sem delimitador , Campos colocados na ordem da estrutura

// CRLF como separador de linhas

// Campo MEMO não é exportado

nHOut := fCreate(cFileOut)

If nHOut == -1

::_SetError(-12,"Output SDF File Create Error - FERROR "+cValToChar(Ferror()))

Return .F.

Endif

::GoTop()

While !::Eof()

// Monta uma linha de dados

cRow := ""

For nPos := 1 TO ::nFldCount

cTipo := ::aStruct[nPos][2]

nTam := ::aStruct[nPos][3]

nDec := ::aStruct[nPos][4]
If cTipo = 'C'

cRow += ::FieldGet(nPos)

ElseIf cTipo = 'N'

cRow += Str(::FieldGet(nPos),nTam,nDec)

ElseIf cTipo = 'D'

cRow += DTOS(::FieldGet(nPos))

ElseIf cTipo = 'L'

cRow += IIF(::FieldGet(nPos),'T','F')

Endif

Next

cRow += CRLF

cBuffer += cRow

If len(cBuffer) > 32000

// A cada 32 mil bytes grava em disco

fWrite(nHOut,cBuffer)

cBuffer := ''

Endif

::Skip()

Enddo

// Grava flag de EOF

cBuffer += Chr(26)

// Grava resto do buffer que falta

fWrite(nHOut,cBuffer)
cBuffer := ''

fClose(nHOut)

ElseIf cFormat == "CSV"

// Formato CSV

// Strings entre aspas duplas, campos colocados na ordem da estrutura

// Virgula como separador de campos, CRLF separador de linhas

// Gera o CSV com Header

// Campo MEMO não é exportado

nHOut := fCreate(cFileOut)

If nHOut == -1

::_SetError(-12,"Output CSV File Create Error - FERROR "+cValToChar(Ferror()))

Return .F.

Endif

// Primeira linha é o "header" com o nome dos campos

For nPos := 1 TO ::nFldCount

If nPos > 1

cBuffer += ','

Endif

cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'"'

Next

cBuffer += CRLF

::GoTop()
While !::Eof()

// Monta uma linha de dados

cRow := ""

For nPos := 1 TO ::nFldCount

cTipo := ::aStruct[nPos][2]

nTam := ::aStruct[nPos][3]

nDec := ::aStruct[nPos][4]

If nPos > 1

cRow += ","

Endif

If cTipo = 'C'

// Dobra aspas duplas caso exista dentro do conteudo

cRow += '"' + StrTran(rTrim(::FieldGet(nPos)),'"','""') + '"'

ElseIf cTipo = 'N'

// Numero trimado

cRow += cValToChar(::FieldGet(nPos))

ElseIf cTipo = 'D'

// Data em formato AAAAMMDD entre aspas

cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'

ElseIf cTipo = 'L'

// Boooleano true ou false

cRow += IIF(::FieldGet(nPos),'true','false')

Endif

Next

cRow += CRLF

cBuffer += cRow
If len(cBuffer) > 32000

// A cada 32 mil bytes grava em disco

fWrite(nHOut,cBuffer)

cBuffer := ''

Endif

::Skip()

Enddo

// Grava resto do buffer que falta

If len(cBuffer) > 0

fWrite(nHOut,cBuffer)

cBuffer := ''

Endif

fClose(nHOut)

ElseIf cFormat == "JSON"

// Formato JSON - Exporta estrutura e dados

// Objeto com 2 propriedades

// header : Array de Arrays, 4 colunas, estrutura da tabela

// data : Array de Arrays, cada linha é um registro da tabela,

// campos na ordem da estrutura

// -- Campo Memo não é exportado

/*
{

"header": [

["cCampo", "cTipo", nTam, nDec], ...

],

"data": [

["José", 14, true], ...

*/

nHOut := fCreate(cFileOut)

If nHOut == -1

::_SetError(-12,"Output JSON File Create Error - FERROR "+cValToChar(Ferror()))

Return .F.

Endif

cBuffer += '{' + CRLF

cBuffer += '"header": [' + CRLF

For nPos := 1 to len(::aStruct)

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

cBuffer += ']' + CRLF

cBuffer += ',' + CRLF

cBuffer += '"data": [' + CRLF

::GoTop()

While !::Eof()

// Monta uma linha de dados

if lFirst

cRow := "["

lFirst := .F.

Else

cRow := "],"+CRLF+"["

Endif

For nPos := 1 TO ::nFldCount

cTipo := ::aStruct[nPos][2]

nTam := ::aStruct[nPos][3]

nDec := ::aStruct[nPos][4]

If nPos > 1

cRow += ","

Endif

If cTipo = 'C'

// Usa Escape sequence de conteudo


// para astas duplas. --

cRow += '"' + StrTran(rTrim(::FieldGet(nPos)),'"','\"') + '"'

ElseIf cTipo = 'N'

// Numero trimado

cRow += cValToChar(::FieldGet(nPos))

ElseIf cTipo = 'D'

// Data em formato AAAAMMDD como string

cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'

ElseIf cTipo = 'L'

// Boooleano = true ou false

cRow += IIF(::FieldGet(nPos),'true','false')

Endif

Next

cBuffer += cRow

If len(cBuffer) > 32000

// A cada 32 mil bytes grava em disco

fWrite(nHOut,cBuffer)

cBuffer := ''

Endif

::Skip()

Enddo

// Termina o JSON

cBuffer += ']' + CRLF

cBuffer += ']' + CRLF


cBuffer += '}' + CRLF

// Grava o final do buffer

fWrite(nHOut,cBuffer)

cBuffer := ''

// Fecha o Arquivo

fClose(nHOut)

Else

UserException("Formato ["+cFormat+"] não suportado. ")

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 😀

Desejo a todos, como de costume, MAIS TERABYTES DE SUCESSO !!!

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

Abstração de Acesso a Dados e Orientação a Objetos – Parte 03

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.

Criando uma Tabela ( DBF ou MEMORY ) a partir de um ALIAS

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"

User Function QRY2MEM()

Local cQuery

Local nH

nH := tclink()

IF nH < 0

MsgStop("TCLINK ERROR - "+cValToChar(nH))

Return

Endif

// Cria um select statement

cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"

// Abre a Query no alias QRY

USE (tcGenQry(,,cQuery)) ALIAS QRY SHARED NEW VIA "TOPCONN"

// Ajusta um campo

TCSetField("QRY","VALOR","N",12,2)

// Cria um objeto de arquivo em memoria

oMemory := ZMEMFILE():New('QRYINMEMORY')

// Popula o objeto com a estrutura e dados da Query

// Passando o ALIAS como parâmetro.

oMemory:CreateFrom("QRY",.T.)

// Mostra o conteudo da tabela na memoria.


// Nao precisa abrir o objeto, o CreateFrom() já fez isso

While !oMemory:Eof()

conout(oMemory:Fieldget(1)+" "+oMemory:FieldGet(2)+" "+cValToChar(oMemory:FieldGet(3)))

oMemory:Skip()

Enddo

// Fecha e mata a tabela

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.

Métodos CreateFrom e AppendFrom

METHOD CreateFrom( _oDBF , lAppend ) CLASS ZISAMFILE

Local lFromAlias := .F.

Local cAlias := ""

Local aStruct := {}

If lAppend = NIL ; lAppend := .F. ; Endif


If valtype(_oDBF) == 'C'

// Se a origem é caractere, só pode ser um ALIAS

lFromAlias := .T.

cAlias := alltrim(upper(_oDBF))

If Select(cAlias) < 1

UserException("Alias does not exist - "+cAlias)

Endif

aStruct := (cAlias)->(DbStruct())

Else

aStruct := _oDBF:GetStruct()

Endif

If !::Create(aStruct)

Return .F.

Endif

IF lAppend

// Dados serão apendados na criação

// Abre para escrita exclusiva

If !::Open(.T.,.T.)

Return .F.

Endif

// Apenda os dados

IF !::AppendFrom(_oDBF)

Return .F.

Endif

// E posiciona no primeiro registro

::GoTop()
Endif

Return .T.

METHOD AppendFrom( _oDBF , lAll, lRest , cFor , cWhile ) CLASS ZISAMFILE

Local aFromTo := {}

Local aFrom := {}

Local nI, nPos, cField

Local lFromAlias := .F.

Local cAlias := ""

DEFAULT lAll := .T.

DEFAULT lRest := .F.

DEFAULT cFor := ''

DEFAULT cWhile := ''

// Primeiro, a tabela tem qye estar aberta

IF !::lOpened

UserException("AppendFrom Failed - Table not opened")

Return .F.

Endif

IF !::lCanWrite

UserException("AppendFrom Failed - Table opened for READ ONLY")

Return .F.

Endif

If valtype(_oDBF) == 'C'

// Se a origem é caractere, só pode ser um ALIAS

lFromAlias := .T.

cAlias := alltrim(upper(_oDBF))
If Select(cAlias) < 1

UserException("Alias does not exist - "+cAlias)

Endif

aFrom := (cAlias)->(DbStruct())

Else

aFrom := _oDBF:GetStruct()

Endif

// Determina match de campos da origem no destino

For nI := 1 to len(aFrom)

cField := aFrom[nI][1]

nPos := ::FieldPos(cField)

If nPos > 0

aadd( aFromTo , { nI , nPos })

Endif

Next

IF lFromAlias

// Dados de origem a partir de uma WorkArea

If lAll

// Se é para importar tudo, pega desde o primeiro registro

(cAlias)->(DbGoTop())

Endif

While !(cAlias)->(EOF())

// Insere um novo registro na tabela atual

::Insert()

// Preenche os campos com os valores da origem

For nI := 1 to len(aFromTo)

::FieldPut( aFromTo[nI][2] , (cAlias)->(FieldGet(aFromTo[nI][1])) )


Next

// Atualiza os valores

::Update()

// Vai para o próximo registro

(cAlias)->(DbSkip())

Enddo

Else

If lAll

// Se é para importar tudo, pega desde o primeiro registro

_oDBF::GoTop()

Endif

While !_oDBF:EOF()

// Insere um novo registro na tabela atual

::Insert()

// Preenche os campos com os valores da origem

For nI := 1 to len(aFromTo)

::FieldPut( aFromTo[nI][2] , _oDBF:FieldGet(aFromTo[nI][1]) )

Next

// Atualiza os valores

::Update()

// Vai para o próximo registro

_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”).

Desejo a todos um bom proveito desta implementação, e TERABYTES DE SUCESSO !!!

Deixe um comentário

MemCached Client em AdvPL – Parte 02

14/01/2019 ADVPL, Orientação a Objeto Classes, Programação

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.

Conceito e Boas Práticas

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.

// Pega a Query do banco e coloca no array

cQuery := "SELECT CAMPO1, CAMPO2, CAMPO3 FROM TABELA WHERE CAMPO1 = '001' ORDER BY
1,2"

USE (TcGenQry(,,cQuery)) ALIAS QRY EXCLUSIVE NEW VIA "TOPCONN"

While !eof()

AADD(aQryData , { QRY->CAMPO1 , QRY->CAMPO2 , QRY->CAMPO3 } )

QRY->(DbSkip())

Enddo

USE

Agora, vamos ao uso do cache. Primeiro, criamos o objeto client e a conexão.

// Cria instancia do Cache

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)

Logo, o fluxo da rotina passa a ser:

Cria conexão com o cache

Verifica se o array está lá, tentando recuperar o array

Se o array não foi recuperado, roda a Query no banco, cria o array e coloca ele em cache

Segue o fluxo normal da rotina usando o array

Usando o objeto client do cache

Normalmente criamos o objeto, conectamos, pedimos algo do cache, desconectamos e matamos o


objeto — FreeObj() — após desconectar. Como a conexão com o cache é algo feito muito
rapidamente, não faz muito sentido mantermos uma conexão permanente em uma variável STATIC

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

Autor Julio Wittwer

Data 01/2019
Descrição Encapsula objeto client do MemCache, usando contador de referencias.

Os programas que consomem o cache devem obter a instância usando :

ZMEMCACHEDPOOL():GetCache( @oMemCache , @cError )

E após o uso, soltar a instancia usando :

ZMEMCACHEDPOOL():ReleaseCache( @oMemCache )

==============================================================================*/

STATIC _oMemCache // Objeto do Cache em "Cache"

STATIC _nRefCount := 0 // Contador de referencias

CLASS ZMEMCACHEDPOOL FROM LONGNAMECLASS

METHOD GetCache()

METHOD ReleaseCache()

METHOD RefCount()

ENDCLASS

// ----------------------------------------------------

// Obtem por referencia uma instancia do cache, e em caso de

// falha, obtem o erro também por referência

METHOD GetCache( oCache , cError ) CLASS ZMEMCACHEDPOOL

// Inicializa parâmetros passados por referência

oCache := NIL

cError := ""
IF _oMemCache != NIL

// Já tenho um objeto de conexao

// Verifico se a conexão está OK

IF _oMemCache:IsConnected()

// Conexão OK, incremento contador de referencias

// e retorno

_nRefCount++

oCache := _oMemCache

Else

// A conexão não está OK

// Limpa o objeto e passa para a próxima parte

FreeObj(_oMemCache)

_oMemCache := NIL

Endif

Endif

IF _oMemCache == NIL

// Nao tenho o objeto de conexão

// Crio o objeto e tento conectar

_oMemCache := ZMEMCACHED():New("localhost",11211)

IF _oMemCache:Connect()

// Conexão OK,incrementa contador e atribui o objeto

_nRefCount++

oCache := _oMemCache

Else

// Nao conectou, recupera o erro, alimenta cError

// e mata este objeto

cError := _oMemCache:GetErrorStr()

FreeObj(_oMemCache)
_oMemCache := NIL

Endif

Endif

Return

// ----------------------------------------------------

// Solta a referência do cache em uso, anula a variável

// recebida por referencia, e caso o contador

// seja menor que um, limpa o objeto da memória

METHOD ReleaseCache( oCache ) CLASS ZMEMCACHEDPOOL

IF oCache != NIL

oCache := NIL

_nRefCount--

IF _nRefCount < 1

_oMemCache:Disconnect()

FreeObj(_oMemCache)

_oMemCache := NIL

Endif

Endif

Return

// ----------------------------------------------------

// Retorna o contador de referencias de uso

// do objeto do Cache

METHOD RefCount() CLASS ZMEMCACHEDPOOL

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:

// --- Fonte Original ----

// **** Cria o objeto e conecta com o Cache ****

oMemCache := ZMEMCACHED():New("localhost",11211)

If !oMemCache:Connect()

conout("oMemCache:Connect() FAILED")

conout(oClient:GetErrorStr())

return .F.

Endif

// **** USA O CACHE ****

// **** Desconecta e limpa o objeto ****

oMemCache:Disconnect()

FreeObj(oMemCache)

oMemCache := NIL

// FONTE NOVO -- Usando o ZMEMCACHEDPOOL

// Obtém o objeto do Cache

oMemCache := NIL

cError := ''

ZMEMCACHEDPOOL():GetCache( @oMemCache , @cError )


IF oMemCache == NIL

conout("ZMEMCACHEDPOOL:GetCache() FAILED")

conout(cError)

return .F.

Endif

// (...)

// **** USA O CACHE ****

// (...)

// **** Faz Release do objeto ****

ZMEMCACHEDPOOL():ReleaseCache( @oMemCache )

Conclusão

Valendo-se destas premissas, e usando elegantemente os recursos de cache, vários processos


podem obter benefícios diretos e indiretos desta implementação.

Todos os fontes atualizados da ZLIB estão no GitHub https://github.com/siga0984/zLIB ! É baixar,


compilar e usar. Os exemplos de uso e teste do cache estão no https://github.com/siga0984/Blog

Novamente agradeço a audiência e desejo a todos TERABYTES DE SUCESSO !!!

Deixe um comentário

MemCached Client em AdvPL – Parte 01

13/01/2019 ADVPL, Escalabilidade e Performance, Orientação a Objeto Classes, Performance,


Programação

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"

CLASS ZMEMCACHED FROM LONGNAMECLASS

DATA cMemCacheIP // IP da instancia Memcached

DATA nMemCachePort // Porta da instancia Memcached

DATA nRvcTimeOut // Timeout de recebimento em milissegundos ( default 1000 )

DATA oTCPConn // Objeto Socket Client

DATA cError // Ultimo erro da API

DATA cResponse // Response header da ultima requisicao


DATA lVerbose // Modo verbose de operação

METHOD New() // Construtor da classe

METHOD Connect() // Estabelece conexão com o MemCached

METHOD GetVersion() // Recupera a versao do MemCached

METHOD GetStats() // Recupera estatisticas da intancia do memcached

METHOD Disconnect() // Desconecta do MemCAched

METHOD Add() // Acrescenta uma chave / valor ( apenas caso nao exista )

METHOD Replace() // Troca o valor de uma chave existente

METHOD Set() // Armazena uma chave / valor no MemCached

METHOD Get() // Recupera o valor de uma chave armazeanda

METHOD Delete() // Remove do cache um valor pela chave

METHOD Increment() // Incrementa um contador pela chave -- valor em string numerica

METHOD Decrement() // Decrementa um contador pela chave -- valor em string numerica

METHOD Flush() // Limpa todas as variáveis do cache

// ********* METODOS DE USO INTERNO *********

METHOD _Store( cMode, cKey , cValue, nOptFlag, nOptExpires )

METHOD _GetTCPError()

ENDCLASS

Método NEW

Method NEW( cIp , nPorta ) CLASS ZMEMCACHED

::cMemCacheIP := cIp

::nMemCachePort := nPorta
::nRvcTimeOut := 1000

::oTCPConn := tSocketClient():New()

::cError := ''

::cResponse := ''

::lVerbose := .F.

Return self

O construtor da classe Client do MemCached recebe logo de cara o IP e Porta da instância do


MemCached que ele deve utilizar. Ele já cria um objeto do tipo tSocketClient() para conversar com
o MemCached, mas ainda não estabelele a conexão, apenas inicializa as propriedades de uso da
classe.

Método CONNECT

METHOD Connect() CLASS ZMEMCACHED

Local iStat

::cError := ''

::cResponse := ''

IF ::lVerbose

Conout("zMemCached:Connect() to "+::cMemCacheIP+" Port


"+cValToChar(::nMemCachePort))

Endif

If ::oTCPConn:Isconnected()

::cError := "Memcached client already connected."

Return .F.

Endif

// Estabelece a conexao com o memcache DB

iStat := ::oTCPConn:Connect( ::nMemCachePort , ::cMemCacheIP, 100 )


If iStat < 0

::cError := "Memcached connection Error ("+cValToChar(iStat)+")"

::_GetTCPError()

Return .F.

Endif

O método Connect apenas estabelece a conexão TCP no IP e Porta especificados no construtor.


Nesta etapa, ele não faz nenhum tipo de HandShake — por hora — ele apenas abre a conexão
TCP/IP. Em caso de erro, o método retorna .F., e a razão do erro pode ser recuperada na propriedade
::cError.

Método DISCONNECT

METHOD Disconnect() CLASS ZMEMCACHED

Local cSendCmd := 'quit'+CRLF

Local nSend

::cError := ''

::cResponse := ''

If ::oTCPConn == NIL

::cError := "Memcached client already Done."

Return .F.

Endif

if( ::oTCPConn:IsConnected() )

// Se ainda está conectado, manda um "quit"

// para fechar a conexao de modo elegante

IF ::lVerbose

Conout("zMemCached:DONE() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send( cSendCmd )

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

Endif

::oTCPConn:CloseConnection()

::oTCPConn := NIL

Return .T.

O método Disconnect() desconecta do MemCached de forma “elegante”, enviando um aviso de


desconexão (instrução quit). Simples assim. Após desconectar, a mesma instância pode ser
aproveitada para uma nova conexão — por hora no mesmo IP e Porta — na mesma instância do
MemCached.

Método GETVERSION

METHOD GetVersion( cVersion ) CLASS ZMEMCACHED

Local nRecv, cRecvBuff := ''

Local cSendCmd := "version" + CRLF

Local nSend

::cError := ''

::cResponse := ''

If !::oTCPConn:Isconnected()
::cError := "Memcached client not connected."

Return .F.

Endif

IF ::lVerbose

Conout("zMemCached:GetVersion() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))

Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0

::cError := "Receive Error"

::_GetTCPError()

Return .F.

Endif

IF ::lVerbose

Conout("zMemCached:GetVersion() RECV "+cValToChar(nRecv)+" Byte(s)")


Conout(Str2HexDmp(cRecvBuff))

Endif

If Left(cRecvBuff,8)!='VERSION '

::cError := "Response Error : " + cRecvBuff

Return .F.

Endif

// Recupera a versão por referencia

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

METHOD GetStats( aStats ) CLASS ZMEMCACHED

Local nRecv, cRecvBuff := ''

Local nI , nT , aTmp

Local cSendCmd := "stats" + CRLF

Local nSend

::cError := ''

::cResponse := ''

If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

Return .F.
Endif

IF ::lVerbose

Conout("zMemCached:GetStats() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))

Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0

::cError := "Receive stats error"

::_GetTCPError()

Return .F.

Endif

If nRecv == 0

::cError := "Receive stats time-out

::_GetTCPError()

Return .F.
Endif

IF ::lVerbose

Conout("zMemCached:GetStats() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif

// Recupera estatisticas

aTmp := strtokarr2( strtran(cRecvBuff,CRLF,chr(10)) , chr(10) )

nT := Len(aTmp)

For nI := 1 to nT

If Left(aTmp[nI],5)=='STAT '

aadd(aStats , substr(aTmp[nI],6) )

Endif

Next

// Limpa o array temporario

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

//
===============================================================================

// Guarda um valor no memcache

// Mode = set, add, replace, append, prepend

// cas ainda nao implementado

METHOD _Store( cMode, cKey , cValue, nOptFlag, nOptExpires ) CLASS ZMEMCACHED

Local cSendCmd := ''

Local nRecv

Local cRecvBuff := ''

Local nSend

::cError := ''

::cResponse := ''

If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

Return .F.

Endif

If !( ('.'+cMode+'.') $ ('.set.add.replace.append.prepend.cas.') )

::cError := "Invalid Store mode ["+cMode+"]"

Return .F.

Endif
// <mode> <key> <flags> <exptime> <bytes>

// ------------------------------------------

cSendCmd += cMode + ' '

cSendCmd += cKey + ' '

If nOptFlag == NIL

cSendCmd += '0 '

else

cSendCmd += cValToChar(nOptFlag)+' '

Endif

If nOptExpires == NIL

cSendCmd += '0 '

else

cSendCmd += cValToChar(nOptExpires)+' '

Endif

cSendCmd += cValToChar(len(cValue))

cSendCmd += CRLF

// ------------------------------------------

IF ::lVerbose

Conout("zMemCached:_Store() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))

Endif

// Etapa 01 Envia o comando

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()
Return .F.

Endif

// Etapa 02

// Envia o valor a ser armazenado

nSend := ::oTCPConn:Send(cValue+CRLF)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

If ::lVerbose

Conout("zMemCached:Store("+cMode+") SEND VALUE ")

Conout(Str2HexDmp(cValue+CRLF))

Endif

// Se tudo der certo, aqui eu devo receber um "stored"

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0

::cError := "Store("+cMode+") failed - connection error" + cValTochar(nRecv)

::_GetTCPError()

Return .F.
Endif

If nRecv == 0

::cError := "Store("+cMode+") failed - response time-out"

::_GetTCPError()

Return .F.

Endif

If ::lVerbose

Conout("zMemCached:Store("+cMode+") RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If cRecvBuff != 'STORED'

::cError := "Store ["+cMode+"] failed: "+cRecvBuff

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.

Métodos ADD, REPLACE e SET

METHOD Add( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED

Return ::_Store("add", cKey , cValue, NIL, nOptExpires)


METHOD Replace( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED

Return ::_Store("replace", cKey , cValue, NIL, nOptExpires)

METHOD Set( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED

Return ::_Store("set", cKey , cValue, NIL, nOptExpires)

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.

A diferença entre eles é que:

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

METHOD Get( cKey , cValue ) CLASS ZMEMCACHED

Local cSendCmd := ''

Local nRecv

Local cRecvBuff := ''

Local nPos

Local cLine

Local aTmp

Local cTeco

Local nSize

Local nSend

::cError := ''
::cResponse := ''

// Limpa o retorno por referencia

cValue := NIL

If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

return -1

Endif

// Monta o comando de recuperacao

cSendCmd += 'get '+cKey + CRLF

If ::lVerbose

Conout("zMemCached:Get() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))

Endif

// Manda o comando

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

// Se tudo der certo, aqui eu devo receber os dados ...

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)
If nRecv < 0

::cError := "Get() failed - connection error" + cValTochar(nRecv)

::_GetTCPError()

return -1

Endif

If nRecv == 0

::cError := "Get() failed - response time-out"

::_GetTCPError()

return -1

Endif

If ::lVerbose

Conout("zMemCached:Get() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif

// Parser do retorno

While !empty(cRecvBuff)

// Primeiro pega a linha de status

nPos := at(CRLF,cRecvBuff)

If nPos < 1

::cError := "Get() failed - missing CRLF"

return -1

Endif
cLine := left(cRecvBuff,nPos-1)

cRecvBuff := substr(cRecvBuff,nPos+2)

If cLine == "END"

// acabaram os dados

// Sai do loop

EXIT

Endif

If Left(cLine,6) == "VALUE "

// Tem valor ... opa ... legal

aTmp := strtokarr2(cLine,' ')

// varinfo("aTmp",aTmp)

// [1] "VALUE"

// [2] <key>

// [3] <flags>

// [4] <size>

// [5] Optional [uniqueid]

nSize := val(aTmp[4])

While len(cRecvBuff) < nSize

// Se ainda falta coisa pra receber, recebe mais um teco

// e acrescenta no buffer

cTeco := ''

nRecv := ::oTCPConn:Receive(@cTeco,::nRvcTimeOut)
If nRecv < 0

::cError := "Get() failed - connection error" + cValTochar(nRecv)

::_GetTCPError()

return -1

Endif

If nRecv == 0

::cError := "Get() failed - response time-out"

::_GetTCPError()

return -1

Endif

If ::lVerbose

Conout("zMemCached:Get() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cTeco))

Endif

// So acrescenta o que recebeu

cRecvBuff += substr(cTeco,1,nRecv)

If ::lVerbose

Conout("zMemCached:Get() Total ReceivedBuffer ")

Conout(Str2HexDmp(cRecvBuff))

Endif

Enddo

// Valor ja foi recebido na integra


// Coloca o valor recebido no retorno

cValue := left(cRecvBuff,nSize)

// Arranca valor recebido do buffer

// Ja desconsiderando o CRLF

cRecvBuff := substr(cRecvBuff,nSize+3)

// Limpa o array temporário

aSize(aTmp,0)

// Eu só espero recener um valor

EXIT

Else

// Se nao tem o valor, ou nao tem o "END", deu merda ?!

::cError := "Get() failed - Unexpected ["+cLine+"]"

return .F.

Endif

Enddo

If empty(cRecvBuff)

// Se o buffer esta vazio, entao nao chegou nenhum valor

// A operação de GET foi feita com sucesso,

// naou houve erro, apenas o valor nao foi encontrado.

Return .T.

Endif
If left(cRecvBuff,5) == "END" + CHR(13)+Chr(10)

// Depois do valor, eu espero um END (CRLF) \

// Se nao chegou um END, tem mais de um valor na chave ? ....

Return .T.

Endif

::cError := "Get() failed - Unexpected Multiple Value(s)"

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.

METHOD Delete( cKey ) CLASS ZMEMCACHED

Local cSendCmd

Local nRecv

Local cRecvBuff := ''

Local nSend

::cError := ''

::cResponse := ''
If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

Return .F.

Endif

cSendCmd := 'delete ' + cKey + CRLF

// ------------------------------------------

If ::lVerbose

Conout("zMemCached:Delete() SEND")

Conout(Str2HexDmp(cSendCmd))

Endif

// Manda o comando

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

// Se tudo der certo, aqui eu devo receber DELETED

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

// Pega apenas a primeira linha do resultado

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)
If nRecv < 0

::cError := "Delete() failed - connection error" + cValTochar(nRecv)

::_GetTCPError()

Return .F.

Endif

If nRecv == 0

::cError := "Delete() failed - response time-out"

::_GetTCPError()

Return .F.

Endif

If ::lVerbose

Conout("zMemCached:Delete() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If cRecvBuff != 'DELETED'

::cError := "Delete failed - Error: "+cRecvBuff

Return .F.

Endif

Return .T.

Métodos INCREMENT e DECREMENT

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.

Method Increment( cKey , nValue , nStep ) CLASS ZMEMCACHED

Local cSendCmd := ''

Local nRecv

Local cRecvBuff := ''

Local nSend

::cError := ''

::cResponse := ''

If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

Return .F.

Endif

// Monta o comando de recuperacao

cSendCmd += 'incr '+cKey+' '

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

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

// Se tudo der certo, aqui eu devo receber o valor apos o incremento

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0

::cError := "Increment() failed - connection error" + cValTochar(nRecv)

::_GetTCPError()

Return .F.

Endif

If nRecv == 0

::cError := "Increment() failed - response time-out"

::_GetTCPError()

Return .F.

Endif
If ::lVerbose

Conout("zMemCached:Increment() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif

// Parser do retorno

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If !(left(cRecvBuff,1)$'0123456789')

::cError := "Increment() failed - Error "+cRecvBuff

::_GetTCPError()

Return .F.

Endif

// Pega e retorna o valor apos o incremento

nValue := val(cRecvBuff)

Return .T.

Method Decrement( cKey , nValue , nStep ) CLASS ZMEMCACHED

Local cSendCmd := ''

Local cRecvBuff := ''

Local nRecv

Local nSend

::cError := ''

::cResponse := ''
If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

Return .F.

Endif

// Monta o comando de recuperacao

cSendCmd += 'decr '+cKey+' '

If nStep == NIL

cSendCmd += '1'

Else

cSendCmd += cValToChar(nStep)

Endif

cSendCmd += CRLF

If ::lVerbose

Conout("zMemCached:Decrement() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))

Endif

// Manda o comando

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_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

::cError := "Decrement() failed - connection error" + cValTochar(nRecv)

::_GetTCPError()

Return .F.

Endif

If nRecv == 0

::cError := "Decrement() failed - response time-out"

::_GetTCPError()

Return .F.

Endif

If ::lVerbose

Conout("zMemCached:Decrement() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif

// Parser do retorno

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If !(left(cRecvBuff,1)$'0123456789')

::cError := "Decrement() failed - Error "+cRecvBuff


Return .F.

Endif

// Pega e retorna o valor apos o decremento

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().

METHOD Flush() CLASS ZMEMCACHED

Local nRecv, cRecvBuff := ''

Local cSendCmd := "flush_all" + CRLF

Local nSend

::cError := ''

::cResponse := ''

If !::oTCPConn:Isconnected()

::cError := "Memcached client not connected."

Return .F.

Endif

IF ::lVerbose

Conout("zMemCached:Flush() SEND "+cValToChar(len(cSendCmd))+" byte(s).")

Conout(Str2HexDmp(cSendCmd))

Endif
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0

::cError := "Memcached client SEND Error."

::_GetTCPError()

Return .F.

Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv == 0

::cError := "Receive timed-out"

::_GetTCPError()

Return .F.

Endif

If nRecv < 0

::cError := "Receive Error"

::_GetTCPError()

Return .F.

Endif

IF ::lVerbose

Conout("zMemCached:Flush() RECV "+cValToChar(nRecv)+" Byte(s)")

Conout(Str2HexDmp(cRecvBuff))

Endif
If Left(cRecvBuff,2)!='OK'

::cError := "Response Error : " + cRecvBuff

Return .F.

Endif

Return .T.

Programa de Testes

Para verificar as funcionalidades do Cache, criei um programa de testes da funcionalidade client da


API, para confirmar os comportamentos, segue fonte abaixo:

#include 'Protheus.ch'

#include 'zLibStr2HexDmp.ch'

// ---------------------------------------------------------------------------------

// Utiliza direto uma inistancia da classe ZMEMCACHED

// para ler e gravar valores, testa todas as operações, expires e contador/sequenciador

#define TEST_HOST 'localhost'

#define TEST_PORT 11211

User Function MemTst1()

Local oClient

Local cVersion := ""

Local aStats := {}

Local nI , nX

Local xValue

Local nNewValue
oClient := ZMEMCACHED():New( TEST_HOST , TEST_PORT )

// Modo verbose apenas para depuração

// oClient:lVerbose := .t.

IF !oClient:Connect()

conout("Falha de conexão...")

conout(oClient:cError)

return

Endif

// Recupera a versao da instancia atual

If !oClient:GetVersion(@cVersion)

conout("Falha ao recuperar versao...")

conout(oClient:cError)

return

endif

conout("Memcache Version: "+cVersion)

// Pega as estatisticas da instancia atual

If !oClient:GetStats(@aStats)

conout("Falha ao recuperar estatisticas...")

conout(oClient:cError)

return

endif

conout(padc(" STATISTICS ",79,"-"))

aEval(aStats , {|x| conout(x) })


// Apaga todas as chaves

If !oClient:Flush()

conout("Falha na limpeza global...")

conout(oClient:cError)

return

endif

// Testando armazenamento de valores

cValue1 := RandomStr(64)

cValue2 := RandomStr(64)

// Acrescenta o valor

// Fuciona apenas caso a chave nao exista

If !oClient:Add( 'chave' , cValue1 )

conout("Falha ao adicionar chave ...")

conout(oClient:cError)

Return

Endif

// Agora tenta acrescentar na mesma chave

// isso nao deveria ser possivel

If oClient:Add( 'chave' , cValue1 )

UserException("Permitiu adicionar valor de chave ja existente")

Endif

// Troca valor - apenas se a chave existe


If !oClient:Replace( 'chave' , cValue2 )

conout("Falha ao trocar chave ...")

conout(oClient:cError)

Return

Endif

// Deleta a chave

If !oClient:Delete( 'chave')

conout("Falha ao deletar chave ...")

conout(oClient:cError)

Return

Endif

// agora tenta trocar o valor.

// deveria falhar, pois a chave nao existe

If oClient:Replace( 'chave' , cValue1 )

UserException("Permitiu troca de valor de chave que nao existe")

Endif

// Acrescenta o valor de novo

// Deve funcionar, pois a chave tinha sido deletada

If !oClient:Add( 'chave' , cValue1 )

conout("Falha ao adicionar chave ...")

conout(oClient:cError)

Return

Endif

// Mostra no console o valor graavdo

conout(padc(" STORED VALUE ",79,"-"))


Conout(Str2HexDmp(cValue1))

// Agora le o valor da chave

lOk := oClient:Get('chave' , @xValue )

If !lOk

conout("Falha ao ler a chave ...")

conout(oClient:cError)

Return

Endif

conout(padc(" READED VALUE ",79,"-"))

Conout(Str2HexDmp(xValue))

If ! (xValue == cValue1 )

UserException("Divergencia de valor")

Endif

// busca uma chave que nao existe

lOk := oClient:Get('naoexiste' , @xValue )

If !lOk

conout("Retorno inesperado")

conout(oClient:cError)

Return

Endif

// Cria um contador para incremento

// Ja inicializado com um valor


If !oClient:Add( 'contador' , '666' )

conout("Falha ao adicionar contador ...")

conout(oClient:cError)

Return

Endif

// Agora testa o incremento

nNewValue := 0

If !oClient:Increment( 'contador' , @nNewValue )

conout("Falha ao incrementar contador ...")

conout(oClient:cError)

Return

Endif

conout("nNewValue = "+cValToChaR(nNewValue))

If nNewValue != 667

UserException("Incr Failed - Expected 667 Reveived "+cvaltochar(nNewValue))

Endif

If !oClient:Decrement( 'contador' , @nNewValue )

conout("Falha ao incrementar contador ...")

conout(oClient:cError)

Return

Endif

conout("nNewValue = "+cValToChaR(nNewValue))
If nNewValue != 666

UserException("Decr Failed - Expected 667 Reveived "+cvaltochar(nNewValue))

Endif

// Agora incrementa um contador que nao existe

If oClient:Increment( 'contador2' , @nNewValue )

UserException("Nao deveria incrementar algo que nao existe")

Else

Conout("-- Falha esperada -- contador realmente nao existe ")

Endif

// teste de valor com timeout

// expira em (aproximadamente) 2 segundos

If !oClient:Add( 'timer' , 'teste' , 2 )

conout("Falha ao adicionar contador ...")

conout(oClient:cError)

Return

Endif

// le o valor 4 vezes em intervalos de 1 segundo

// A partir da terceira leitura o valor

// já nao deveria existir

For nX := 1 to 4

// le direto o valor

lOk := oClient:Get('timer' , @xValue )

conout(padc(" GET VALUE ",79,"-"))


If xValue = NIL

conout("--- NIL --- ")

Else

Conout(Str2HexDmp(xValue))

Endif

If !lOk

conout("Falha ao ler a chave ...")

conout(oClient:cError)

Return

Endif

Sleep(1000)

Next

// Pega as estatisticas no final do teste

If !oClient:GetStats(@aStats)

conout("Falha ao recuperar estatisticas...")

conout(oClient:cError)

return

endif

conout(padc(" MEMCACHED STATSISTICS ",79,"-"))

aEval(aStats , {|x| conout(x) })

oClient:Disconnect()

FreeObj(oClient)

Return

// ---------------------------------------------------------------------------------
// Função RandomStr()

// Gera uma string com o tamanho espeficicado, sorteando caracteres

// ASICI da faixa de 32 a 127, contemplando letras, números e simbolos

STATIC Function RandomStr(nSize)

Local cRet := ''

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

A partir de hoje — 13/01/2019 — os fontes da ZLIB estão em um repositório separado do GITHUB.


Por hora só existe o branch “master”, e ele é o que sempre será atualizado. Os fontes existentes no
repositorio do blog permanecem lá, mas sem receber atualização — apenas para não quebrar os
links dos posts anteriores. Os fontes e includes da zLib estão na URL
https://github.com/siga0984/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

Abstração de Acesso a Dados e Orientação a Objetos – Parte 02

10/01/2019 ADVPL, DBF / ISAM, Orientação a Objeto Classes, Programação

Introdução

No post anterior (Abstração de Acesso a Dados e Orientação a Objetos) vimos o conceito de


abstração e uma ideia de implementar uma classe superior — ou superclasse — que eliminaria
várias duplicidades entre as classes ZDBFFILE e ZMEMFILE. Bem, mãos a obra.

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

CLASS ZDBFFILE FROM LONGNAMECLASS

CLASS ZMEMFILE FROM LONGNAMECLASS

// Agora temos

CLASS ZISAMFILE FROM LONGNAMECLASS

CLASS ZDBFFILE FROM ZISAMFILE

CLASS ZMEMFILE FROM ZISAMFILE

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

// Inicialização das propriedades da classe pai

_Super:_InitVars()

// Inicializa demais propriedades da ZMEMFILE

::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.

Até mesmo um método que era

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.

Desejo a todos novamente TERABYTES DE SUCESSO 😀


1 comentário

Tetris Orientado a Objetos em AdvPL

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.

O projeto final está no GitHub https://github.com/siga0984/Tetris-OO, que contém os resources


(imagens) das pedras, e os fontes “Tetris-OO.PRW” (fonte da interface do jogo para SmartClient), e
o fonte “Tetris-Core.prw”, que agora contém apenas o núcleo do jogo, sem a interface. Da mesma
forma que o jogo anterior, basta criar um projeto AdvPL no IDE ou TDS, acrescentar os fontes e
imagens, compilar e executar. a função U_TetrisOO diretamente a partir do SmartClient.

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

Autor Júlio Wittwer

Data 21/03/2015

Versão 1.150321

Descriçao Réplica do jogo Tetris, feito em AdvPL

Remake reescrito a partir do Tetris.PRW, utiliando Orientação a Objetos


Para jogar, utilize as letras :

A ou J = Move esquerda

D ou L = Move Direita

S ou K = Para baixo

W ou I = Rotaciona sentido horario

Barra de Espaço = Dropa a peça

======================================================== */

// =======================================================

USER Function TetrisOO()

Local oDlg, oBGGame , oBGNext

Local oFont , oLabel

Local oScore , oTimer

Local nC , nL

Local oTetris

Local aBMPGrid

Local aBMPNext

Local aResources

// Arrays de componentes e recursos de Interface

aBMPGrid := array(20,10) // Array de bitmaps de interface do jogo

aBMPNext := array(4,5) // Array de bitmaps da proxima peça

aResources := {
"BLACK","YELOW2","LIGHTBLUE2","ORANGE2","RED2","GREEN2","BLUE2","PURPLE2" }

// Fonte default usada na caixa de diálogo

// e respectivos componentes filhos

oFont := TFont():New('Courier new',,-16,.T.,.T.)

// Interface principal do jogo

DEFINE DIALOG oDlg TITLE "Object Oriented Tetris AdvPL" FROM 10,10 TO 450,365 ;

FONT oFont COLOR CLR_WHITE,CLR_BLACK PIXEL


// Cria um fundo cinza, "esticando" um bitmap

@ 8, 8 BITMAP oBGGame RESOURCE "GRAY" ;

SIZE 104,204 Of oDlg ADJUST NOBORDER PIXEL

// Desenha na tela um grid de 20x10 com Bitmaps

// para desenhar o Game

For nL := 1 to 20

For nC := 1 to 10

@ nL*10, nC*10 BITMAP oBmp RESOURCE "BLACK" ;

SIZE 10,10 Of oDlg ADJUST NOBORDER PIXEL

aBMPGrid[nL][nC] := oBmp

Next

Next

// Monta um Grid 4x4 para mostrar a proxima peça

// ( Grid deslocado 110 pixels para a direita )

@ 8, 118 BITMAP oBGNext RESOURCE "GRAY" ;

SIZE 54,44 Of oDlg ADJUST NOBORDER PIXEL

For nL := 1 to 4

For nC := 1 to 5

@ nL*10, (nC*10)+110 BITMAP oBmp RESOURCE "BLACK" ;

SIZE 10,10 Of oDlg ADJUST NOBORDER PIXEL

aBMPNext[nL][nC] := oBmp

Next
Next

// Label fixo, Pontuação do Jogo

@ 80,120 SAY oLabel1 PROMPT "[Score]" SIZE 60,20 OF oDlg PIXEL

// Label para Mostrar score

@ 90,120 SAY oScore PROMPT " " SIZE 60,120 OF oDlg PIXEL

// Label fixo, Tempo de Jogo

@ 110,120 SAY oLabel2 PROMPT "[Time]" SIZE 60,20 OF oDlg PIXEL

// Label para Mostrar Tempo de Jogo

@ 120,120 SAY oElapTime PROMPT " " SIZE 60,120 OF oDlg PIXEL

// Label para Mostrar Status do Jogo

@ 140,120 SAY oGameMsg PROMPT " " SIZE 60,120 OF oDlg PIXEL

// Botões com atalho de teclado

// para as teclas usadas no jogo

// colocados fora da area visivel da caixa de dialogo

@ 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 oDummyB3 PROMPT '&W' ACTION ( oTetris:DoAction('W') ) 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

// Inicializa o objeto do core do jogo

oTetris := APTetris():New()
// Define um timer, para fazer a peça em jogo

// descer uma posição a cada um segundo

// ( Nao pode ser menor, o menor tempo é 1 segundo )

// A ação '#' é diferente de "S" ou "K", pois nao atualiza

// o score quando a peça está descento "sozinha" por tempo

oTimer := TTimer():New(1000, {|| oTetris:DoAction('#') }, oDlg )

// Registra evento para atualização de score

// Apos uma ação ser processada pelo objeto Tetris, caso o score

// tenha mudado, este codeclobk será disparado com o novo score

oTetris:bShowScore := {|cMsg| oScore:SetText(cMsg) }

// Registra evento para atualização do tempo decorrido de jogo

// Apos uma ação ser processada pelo objeto Tetris, caso o tempo

// de jogo tenha mudado, este codeclobk será disparado com o tempo

// decorrido de jogo atualizado.

oTetris:bShowElap := {|cMsg| oElapTime:SetText(cMsg) }

// Registra evento de mudança de estado do jogo

// Running , Pause, Game Over. Caso seja disparado um

// pause ou continue, ou mesmo a última peça nao caber

// na TEla ( Game Over ), este bloco é disparado, informando o novo

// estado de jogo

oTetris:bChangeState := {|nStat| GameState( nStat , oTimer , oGameMsg ) }

// Registra evento de pintura do grid

// Apos processamento de ação, caso o grid precise ser repintado,

// este bloco de código será disparado

oTetris:bPaintGrid := {|aGameGrid| PaintGame( aGameGrid, aBmpGrid , aResources ) }

// Registra evento de pintura da proxima peça

// 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

oTetris:bPaintNext := {|aNextPiece| PaintNext(aNextPiece, aBMPNext, aResources) }


// Na inicialização do Dialogo, começa o jogo

oDlg:bInit := {|| oTetris:Start() }

ACTIVATE DIALOG oDlg CENTER

Return

/* -------------------------------------------------------

Notificação de mudança de estado de jogo

GAME_RUNNING, GAME_PAUSED ou GAME_OVER

------------------------------------------------------- */

STATIC Function GameState( nStat , oTimer , oGameMsg )

Local cMsg

If nStat == GAME_RUNNING

// Jogo em execuçao, ativa timer de interface

oTimer:Activate()

cMsg := "*********"+CRLF+;

"* PLAY *"+CRLF+;

"*********"

ElseIf nStat == GAME_PAUSED

// Jogo em pausa

// desativa timer de interface

oTimer:DeActivate()

// e acrescenta mensagem de pausa

cMsg := "*********"+CRLF+;

"* PAUSE *"+CRLF+;

"*********"

ElseIf nStat == GAME_OVER

// Game Over

// desativa timer de interface

oTimer:DeActivate()
// e acresenta a mensagem de "GAME OVER"

cMsg := "********"+CRLF+;

"* GAME *"+CRLF+;

"********"+CRLF+;

"* OVER *"+CRLF+;

"********"

Endif

// Atualiza a mensagem na interface

oGameMsg:SetText(cMsg)

Return

/* ----------------------------------------------------------

Função PaintGame()

Pinta o Grid do jogo da memória para a Interface

Chamada pelo objeto Tetris via code-block

Optimizada para apenas trocar os resources diferentes

---------------------------------------------------------- */

STATIC Function PaintGame( aGameGrid, aBmpGrid , aResources )

Local nL, nc , cLine, nPeca

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]

// Somente manda atualizar o bitmap se houve

// mudança na cor / resource desta posição

aBmpGrid[nL][nC]:SetBmp(aResources[nPeca+1])

endif

Next

Next
Return

/* -----------------------------------------------------------------

Pinta na interface a próxima peça a ser usada no jogo

Chamada pelo objeto Tetris via code-block

Optimizada para apenas trocar os resources diferentes

----------------------------------------------------------------- */

STATIC Function PaintNext(aNext,aBMPNext,aResources)

Local nL, nC, cLine , nPeca

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'

// ============================================================================

// Classe "CORE" do Jogo Tetris

// ============================================================================

CLASS APTETRIS

// Propriedades publicas
DATA aGamePieces // Peças que compoe o jogo

DATA nGameStart // Momento de inicio de jogo

DATA nGameTimer // Tempo de jogo em segundos

DATA nGamePause // Controle de tempo de pausa

DATA nNextPiece // Proxima peça a ser usada

DATA nGameStatus // 0 = Running 1 = PAuse 2 == Game Over

DATA aNextPiece // Array com a definição e posição da proxima peça

DATA aGameCurr // Array com a definição e posição da peça em jogo

DATA nGameScore // pontuação da partida

DATA aGameGrid // Array de strings com os blocos da interface representados em memoria

// Eventos disparados pelo core do Jogo

DATA bShowScore // CodeBlock para interface de score

DATA bShowElap // CodeBlock para interface de tempo de jogo

DATA bChangeState // CodeBlock para indicar mudança de estado ( pausa / continua /game over )

DATA bPaintGrid // CodeBlock para evento de pintura do Grid do Jogo

DATA bPaintNext // CodeBlock para evento de pintura da Proxima peça em jogo

// Metodos Publicos

METHOD New() // Construtor

METHOD Start() // Inicio de Jogo

METHOD DoAction(cAct) // Disparo de ações da Interface

METHOD DoPause() // Dispara Pause On/Off

// Metodos privados ( por convenção, prefixados com "_" )

METHOD _LoadPieces() // Carga do array de peças do Jogo

METHOD _MoveDown() // Movimenta a peça corrente uma posição para baixo

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

METHOD _DelPiece(aPiece,aGrid) // Remove uma peça no Grid em memoria do jogo

METHOD _FreeLines() // Verifica e eliminha linhas totalmente preenchidas

METHOD _GetEmptyGrid() // Retorna um Grid em memoria inicializado vazio

ENDCLASS

/* ----------------------------------------------------------

Construtor da classe

---------------------------------------------------------- */

METHOD NEW() CLASS APTETRIS

::aGamePieces := ::_LoadPieces()

::nGameTimer := 0

::nGameStart := 0

::aNextPiece := {}

::aGameCurr := {}

::nGameScore := 0

::aGameGrid := {}

::nGameStatus := GAME_RUNNING

Return self

/* ----------------------------------------------------------

Inicializa o Grid na memoria

Em memoria, o Grid possui 14 colunas e 22 linhas

Na tela, são mostradas apenas 20 linhas e 10 colunas

As 2 colunas da esquerda e direita, e as duas linhas a mais

sao usadas apenas na memoria, para auxiliar no processo

de validação de movimentação das peças.

---------------------------------------------------------- */

METHOD Start() CLASS APTETRIS

Local aDraw, nPiece, cScore

// Inicializa o grid de imagens do jogo na memória


// Sorteia a peça em jogo

// Define a peça em queda e a sua posição inicial

// [ Peca, rotacao, linha, coluna ]

// e Desenha a peça em jogo no Grid

// e Atualiza a interface com o Grid

// Inicializa o grid do jogo "vazio"

::aGameGrid := aClone(::_GetEmptyGrid())

// Sorteia peça em queda do inicio do jogo

nPiece := randomize(1,len(::aGamePieces)+1)

// E coloca ela no topo da tela

::aGameCurr := {nPiece,1,1,6}

::_SetPiece(::aGameCurr,::aGameGrid)

// Dispara a pintura do Grid do Jogo

Eval( ::bPaintGrid , ::aGameGrid)

// Sorteia a proxima peça e desenha

// ela no grid reservado para ela

::aNextPiece := array(4,"00000")

::nNextPiece := randomize(1,len(::aGamePieces)+1)

aDraw := {::nNextPiece,1,1,1}

::_SetPiece(aDraw,::aNextPiece)

// Dispara a pintura da próxima peça

Eval( ::bPaintNext , ::aNextPiece )

// Marca timer do inicio de jogo

::nGameStart := seconds()

// Chama o codeblock de mudança de estado - Jogo em execução

Eval(::bChangeState , ::nGameStatus )

// E chama a pintura do score inicial

cScore := str(::nGameScore,7)

Eval( ::bShowScore , cScore )


Return

/* ----------------------------------------------------------

Recebe uma ação de movimento de peça, e realiza o movimento

da peça corrente caso exista espaço para tal.

---------------------------------------------------------- */

METHOD DoAction(cAct) CLASS APTETRIS

Local aOldPiece

Local cScore, cElapTime

Local cOldScore, cOldElapTime

If ::nGameStatus != GAME_RUNNING

// Jogo não está rodando, nao aceita ação nenhuma

Return .F.

Endif

// Pega pontuação e tempo decorridos agora

cOldScore := str(::nGameScore,7)

cOldElapTime := STOHMS(::nGameTimer)

// Clona a peça em queda

aOldPiece := aClone(::aGameCurr)

if cAct $ 'AJ'

// Movimento para a Esquerda (uma coluna a menos)

// Remove a peça do grid

::_DelPiece(::aGameCurr,::aGameGrid)

::aGameCurr[PIECE_COL]--

If !::_SetPiece(::aGameCurr,::aGameGrid)

// Se nao foi feliz, pinta a peça de volta

::aGameCurr := aClone(aOldPiece)

::_SetPiece(::aGameCurr,::aGameGrid)

Endif
Elseif cAct $ 'DL'

// Movimento para a Direita ( uma coluna a mais )

// Remove a peça do grid

::_DelPiece(::aGameCurr,::aGameGrid)

::aGameCurr[PIECE_COL]++

If !::_SetPiece(::aGameCurr,::aGameGrid)

// Se nao foi feliz, pinta a peça de volta

::aGameCurr := aClone(aOldPiece)

::_SetPiece(::aGameCurr,::aGameGrid)

Endif

Elseif cAct $ 'WI'

// Movimento para cima ( Rotaciona sentido horario )

// Remove a peça do Grid

::_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)

// Se nao consegue colocar a peça no Grid

// Nao é possivel rotacionar. Pinta a peça de volta


::aGameCurr := aClone(aOldPiece)

::_SetPiece(::aGameCurr,::aGameGrid)

Endif

ElseIF cAct $ 'SK#'

// Desce a peça para baixo uma linha intencionalmente

::_MoveDown()

If cAct $ 'SK'

// se o movimento foi intencional, ganha + 1 ponto

::nGameScore++

Endif

ElseIF cAct == ' '

// Dropa a peça - empurra para baixo até a última linha

// antes de bater a peça no fundo do Grid. Isto vai permitir

// movimentos laterais e roração, caso exista espaço

If !::_DropDown()

// Se nao tiver espaço para o DropDown, faz apenas o MoveDown

// e "assenta" a peça corrente

::_MoveDown()

Endif

Else

UserException("APTETRIS:DOACTION() ERROR: Unknow Action ["+cAct+"]")

Endif
// Dispara a repintura do Grid

Eval( ::bPaintGrid , ::aGameGrid)

// Calcula tempo decorrido

::nGameTimer := seconds() - ::nGameStart

If ::nGameTimer < 0

// Ficou negativo, passou da meia noite

::nGameTimer += 86400

Endif

// Pega Score atualizado e novo tempo decorrido

cScore := str(::nGameScore,7)

cElapTime := STOHMS(::nGameTimer)

If ( cOldScore <> cScore )

// Dispara o codeblock que atualiza o score

Eval( ::bShowScore , cScore )

Endif

If ( cOldElapTime <> cElapTime )

// Dispara atualizaçao de tempo decorrido

Eval( ::bShowElap , cElapTime )

Endif

Return .T.

/* ----------------------------------------------------------

Coloca e retira o jog em pausa

Este metodo foi criado isolado, pois é o unico

que poderia ser chamado dentro de uma pausa

---------------------------------------------------------- */

METHOD DoPause() CLASS APTETRIS

Local lChanged := .F.

Local nPaused
Local cElapTime

Local cOldElapTime

cOldElapTime := STOHMS(::nGameTimer)

If ::nGameStatus == GAME_RUNNING

// Jogo em execução = Pausa : Desativa o timer

lChanged := .T.

::nGameStatus := GAME_PAUSED

::nGamePause := seconds()

ElseIf ::nGameStatus == GAME_PAUSED

// Jogo em pausa = Sai da pausa : Ativa o timer

lChanged := .T.

::nGameStatus := GAME_RUNNING

// Calcula quanto tempo o jogo ficou em pausa

// e acrescenta esse tempo do start do jogo

nPaused := seconds()-::nGamePause

If nPaused < 0

nPaused += 86400

Endif

::nGameStart += nPaused

Endif

If lChanged

// Chama o codeblock de mudança de estado - Entrou ou saiu de pausa

Eval(::bChangeState , ::nGameStatus )

If ::nGameStatus == GAME_PAUSED

// Em pausa, Dispara a pintura do Grid do Jogo vazio

Eval( ::bPaintGrid , ::_GetEmptyGrid() )

Else
// Game voltou da pausa, pinta novamente o Grid

Eval( ::bPaintGrid , ::aGameGrid)

Endif

// Calcula tempo de jogo sempre ao entrar ou sair de pausa

::nGameTimer := seconds() - ::nGameStart

If ::nGameTimer < 0

// Ficou negativo, passou da meia noite

::nGameTimer += 86400

Endif

// Pega novo tempo decorrido

cElapTime := STOHMS(::nGameTimer)

If ( cOldElapTime <> cElapTime )

// Dispara atualizaçao de tempo decorrido

Eval( ::bShowElap , cElapTime )

Endif

Endif

Return

/* ----------------------------------------------------------

Metodo SetGridPiece

Aplica a peça informada no array do Grid.

Retorna .T. se foi possivel aplicar a peça na posicao atual

Caso a peça não possa ser aplicada devido a haver

sobreposição, a função retorna .F. e o grid não é atualizado

Serve tanto para o Grid do Jogo quando para o Grid da próxima peça

---------------------------------------------------------- */

METHOD _SetPiece(aPiece,aGrid) CLASS APTETRIS

Local nPiece := aPiece[PIECE_NUMBER] // Numero da peça


Local nRotate := aPiece[PIECE_ROTATION] // Rotação

Local nRow := aPiece[PIECE_ROW] // Linha no Grid

Local nCol := aPiece[PIECE_COL] // Coluna no Grid

Local nL , nC

Local aTecos := {}

Local cTecoGrid, cPeca , cPieceId

conout("_SetPiece on COL "+cValToChar(nCol))

cPieceId := str(nPiece,1)

For nL := nRow to nRow+3

cPeca := ::aGamePieces[nPiece][1+nRotate][nL-nRow+1]

If nL > len(aGrid)

// Se o grid acabou, verifica se o teco

// da peça tinha alguma coisa a ser ligada

// Se tinha, nao cabe, se não tinha, beleza

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'

// Vai haver sobreposição,

// a peça nao cabe ...

Return .F.

Endif

cTecoGrid := Stuff(cTecoGrid,nC,1,cPieceId)
Endif

Next

// Array temporario com a peça já colocada

aadd(aTecos,cTecoGrid)

Next

// Aplica o array temporario no array do grid

For nL := nRow to nRow+len(aTecos)-1

aGrid[nL] := stuff(aGrid[nL],nCol,4,aTecos[nL-nRow+1])

Next

// A peça "coube", retorna .T.

Return .T.

/* -----------------------------------------------------------------

Carga do array de peças do jogo

Array multi-dimensional, contendo para cada

linha a string que identifica a peça, e um ou mais

arrays de 4 strings, onde cada 4 elementos

representam uma matriz binaria de caracteres 4x4

para desenhar cada peça

Exemplo - Peça "O"

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"

----------------------------------------------------------------- */

METHOD _LoadPieces() CLASS APTETRIS

Local aLPieces := {}

// Peça "O" , uma posição

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'}})

// Peça "S", em pé e deitada

aadd(aLPieces,{'S', { '0000','0011','0110','0000'},;

{ '0010','0011','0001','0000'}})

// Peça "Z", em pé e deitada

aadd(aLPieces,{'Z', { '0000','0110','0011','0000'},;

{ '0001','0011','0010','0000'}})

// Peça "L" , nas 4 posições possiveis

aadd(aLPieces,{'L', { '0000','0111','0100','0000'},;

{ '0010','0010','0011','0000'},;

{ '0001','0111','0000','0000'},;

{ '0110','0010','0010','0000'}})

// Peça "J" , nas 4 posições possiveis

aadd(aLPieces,{'J', { '0000','0111','0001','0000'},;

{ '0011','0010','0010','0000'},;

{ '0100','0111','0000','0000'},;

{ '0010','0010','0110','0000'}})

// Peça "T" , nas 4 posições possiveis

aadd(aLPieces,{'T', { '0000','0111','0010','0000'},;

{ '0010','0011','0010','0000'},;

{ '0010','0111','0000','0000'},;

{ '0010','0110','0010','0000'}})

Return aLPieces

/* ----------------------------------------------------------

Função _MoveDown()

Movimenta a peça em jogo uma posição para baixo.

Caso a peça tenha batido em algum obstáculo no movimento


para baixo, a mesma é fica e incorporada ao grid, e uma nova

peça é colocada em jogo. Caso não seja possivel colocar uma

nova peça, a pilha de peças bateu na tampa -- Game Over

---------------------------------------------------------- */

METHOD _MoveDown() CLASS APTETRIS

Local aOldPiece

Local nMoved := 0

If ::nGameStatus != GAME_RUNNING

Return

Endif

// Clona a peça em queda na posição atual

aOldPiece := aClone(::aGameCurr)

// Primeiro remove a peça do Grid atual

::_DelPiece(::aGameCurr,::aGameGrid)

// Agora move a peça apenas uma linha pra baixo

::aGameCurr[PIECE_ROW]++

// Recoloca a peça no Grid

If ::_SetPiece(::aGameCurr,::aGameGrid)

// Nao bateu em nada, beleza.

// Retorna aqui mesmo

Return

Endif

// Opa ... Esbarrou em alguma peça ou fundo do grid

// Volta a peça pro lugar anterior e recoloca a peça no Grid

::aGameCurr := aClone(aOldPiece)

::_SetPiece(::aGameCurr,::aGameGrid)

// Encaixou uma peça .. Incrementa o score em 4 pontos


// Nao importa a peça ou como ela foi encaixada

::nGameScore += 4

// Verifica apos a pea encaixada, se uma ou mais linhas

// foram preenchidas e podem ser eliminadas

::_FreeLines()

// Pega a proxima peça e coloca em jogo

nPiece := ::nNextPiece

::aGameCurr := {nPiece,1,1,6} // Peca, direcao, linha, coluna

If !::_SetPiece(::aGameCurr,::aGameGrid)

// Acabou, a peça nova nao entra (cabe) no Grid

// "** GAME OVER** "

::nGameStatus := GAME_OVER

// Chama o codeblock de mudança de estado - Game Over

Eval(::bChangeState , ::nGameStatus )

// E retorna aqui mesmo

Return

Endif

// Inicializa proxima peça em branco

::aNextPiece := array(4,"00000")

// Sorteia a proxima peça que vai cair

::nNextPiece := randomize(1,len(::aGamePieces)+1)

::_SetPiece( {::nNextPiece,1,1,1} , ::aNextPiece)

// Dispara a pintura da próxima peça

Eval( ::bPaintNext , ::aNextPiece )

// e retorna para o processamento de ações


Return

METHOD _DropDown() CLASS APTETRIS

Local aOldPiece

Local nMoved := 0

If ::nGameStatus != GAME_RUNNING

Return .F.

Endif

// Clona a peça em queda na posição atual

aOldPiece := aClone(::aGameCurr)

// Dropa a peça até bater embaixo

// O Drop incrementa o score em 1 ponto

// para cada linha percorrida. Quando maior a quantidade

// de linhas vazias, maior o score acumulado com o Drop

// Remove a peça do Grid atual

::_DelPiece(::aGameCurr,::aGameGrid)

// Desce uma linha pra baixo

::aGameCurr[PIECE_ROW]++

While ::_SetPiece(::aGameCurr,::aGameGrid)

// Peça desceu mais uma linha

// Incrementa o numero de movimentos dentro do Drop

nMoved++

// Incrementa o Score

::nGameScore++

// Remove a peça da interface

::_DelPiece(::aGameCurr,::aGameGrid)

// Guarda a peça na posição atual

aOldPiece := aClone(::aGameCurr)
// Desce a peça mais uma linha pra baixo

::aGameCurr[PIECE_ROW]++

Enddo

// Volta a peça na última posição válida,

::aGameCurr := aClone(aOldPiece)

::_SetPiece(::aGameCurr,::aGameGrid)

// Se conseguiu mover a peça com o Drop

// pelo menos uma linha, retorna .t.

Return (nMoved > 0)

/* -----------------------------------------------------------------------

Remove a peça informada do grid informado

----------------------------------------------------------------------- */

METHOD _DelPiece(aPiece,aGrid) CLASS APTETRIS

Local nPiece := aPiece[PIECE_NUMBER]

Local nRotate := aPiece[PIECE_ROTATION]

Local nRow := aPiece[PIECE_ROW]

Local nCol := aPiece[PIECE_COL]

Local nL, nC

Local cTecoGrid, cTecoPeca

// Como a matriz da peça é 4x4, trabalha em linhas e colunas

// Separa do grid atual apenas a área que a peça está ocupando

// e desliga os pontos preenchidos da peça no Grid.

// Esta função não verifica se a peça que está sendo removida

// é a correta, apenas apaga do grid os pontos ligados que

// a peça informada ocupa nas coordenadas especificadas

For nL := nRow to nRow+3


cTecoPeca := ::aGamePieces[nPiece][1+nRotate][nL-nRow+1]

If nL > len(aGrid)

// O Grid acabou, retorna

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

/* -----------------------------------------------------------------------

Verifica e elimina as linhas "completas"

após uma peça ser encaixada no Grid

----------------------------------------------------------------------- */

METHOD _FreeLines() CLASS APTETRIS

Local nErased := 0

Local cTecoGrid

For nL := 21 to 2 step -1

// Sempre varre de baixo para cima

cTecoGrid := substr(::aGameGrid[nL],3)

If !('0'$cTecoGrid)

// Se a linha nao tem nenhum espaço em branco

// Elimina esta linha e acrescenta uma nova linha


// em branco no topo do Grid

adel(::aGameGrid,nL)

ains(::aGameGrid,1)

::aGameGrid[1] := GRID_EMPTY_LINE

nL++

nErased++

Endif

Next

// Pontuação por linhas eliminadas

// Quanto mais linhas ao mesmo tempo, mais pontos

If nErased == 4

::nGameScore += 100

ElseIf nErased == 3

::nGameScore += 50

ElseIf nErased == 2

::nGameScore += 25

ElseIf nErased == 1

::nGameScore += 10

Endif

Return

/* ------------------------------------------------------

Retorna um grid de jogo vazio / inicializado

O Grid no core do tetris contem 21 linhas por 14 colunas

As limitações nas laterais esquerda e direita para

facilitar os algoritmos para fazer a manutenção no Grid

A área visivel nas colunas do Grid está indicada usando

"." Logo, mesmo que o grid em memoria

tenha 21x14, o grid de bitmaps de interface tem apenas 20x10,


a partir da coordenada 2,3 ( linha,coluna ) do Grid do Jogo

"11000000000011" -- Primeira linha, não visivel

"11..........11" -- demais 20 linhas, visiveis da coluna 2 a 11

------------------------------------------------------ */

METHOD _GetEmptyGrid() CLASS APTETRIS

Local aEmptyGrid

aEmptyGrid := array(21,GRID_EMPTY_LINE)

Return aEmptyGrid

/* ------------------------------------------------------

Função auxiliar de conversão de segundos para HH:MM:SS

------------------------------------------------------ */

STATIC Function STOHMS(nSecs)

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 !!

Até o próximo post, pessoal 😉 E obrigado pela audiência 😀

7 Comentários

Classes em Advpl – Parte 04

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.

Nível de abstração e herança adequadas

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.

Destrutores e limpeza de memória

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

Existe um overhead na performance na chamada de um método, em comparação com a chamada


de uma função. Fiz um teste de performance de chamadas de uma função e de um método de classe,
onde a função e a classe realizam um processamento – o retorno de um valor constante. O loop de
processamento demorou em média 1,5 segundos para chamar um milhão de vezes uma função, e
demorou 3,8 segundos para chamar um milhão de vezes um método. Parece uma grande diferença,
certo ? Bem, estamos falando aqui de 666 mil RPS(requisições por segundo) de função, contra 263
mil RPS em chamadas de método, usando uma CPU de 1.8 GHZ.

Lembre-se da Lei de Amdahl, já mencionada aqui em outra postagem sobre escalabilidade e


performance: “O ganho de desempenho que pode ser obtido melhorando uma determinada parte
do sistema é limitado pela fração de tempo que essa parte é utilizada pelo sistema durante a sua
operação.” Logo, a relevância do tempo em uma etapa específica de um processo somente é
perceptível caso ela seja representativa no tempo total do processo.

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.

Até o próximo post, pessoal 😉

3 Comentários

Classes em Advpl – Parte 03

03/12/2014 ADVPL, Orientação a Objeto ADVPL, Classes, Orientação a Objetos, Programação

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.

// --------------------------------------------------

// Fonte de teste da classe APBUTTON herdando tButton

User Function APPTST03()

Local oDlg , oBtn1, oBtn2

DEFINE DIALOG oDlg TITLE "Exemplo de Herança" FROM 10,10 TO 150,300 COLOR
CLR_BLACK,CLR_WHITE PIXEL

// Cria um botao normal

// e seta cor diferenciada para o botão

@ 10,5 BUTTON oBtn1 PROMPT 'TBUTTON' ;

ACTION ( oBtn2:Show() , oBtn1:Hide() ) ;

SIZE 040, 013 OF oDlg PIXEL


// Cria um botao usando a classe implementada

oBtn2 := APBUTTON():NEW(oDlg, "APBUTTON", 30, 5, 40, 13, {|| oBtn1:Show(),oBtn2:Hide() })

ACTIVATE DIALOG oDlg CENTER

Return

// ------------------------------------------------------------

CLASS APBUTTON FROM TBUTTON

METHOD New() CONSTRUCTOR

METHOD Hide()

METHOD Show()

ENDCLASS

// Construtor da classe inicializa construtor do botão

// e já seta todas as propriedades e comportamentos desejados

// ( Troca fonte, seta cor e esconde o botão )

METHOD New(oParent,cCaption,nTop,nLeft,nWidth,nHeight,bAction) CLASS APBUTTON

:New(nTop,nLeft,cCaption,oParent,bAction,nWidth,nHeight,NIL,NIL,NIL,.T.)

::SetColor(CLR_WHITE,CLR_BLACK)

::SetFont( TFont():New("Courier New",,14))

_Super:Hide()

Return self

METHOD Hide() CLASS APBUTTON

MsgInfo("Escondendo o botão ["+::cCaption+"]")

Return _Super:Hide()

METHOD Show() CLASS APBUTTON

MsgInfo("Mostrando o botão ["+::cCaption+"]")

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

Inicialmente, além de procurar respeitar os limites estabelecidos, existem características de


retenção de memória ligados ao uso de classes visuais e não-visuais no ADVPLe boas práticas gerais
ligadas à orientação a objetos em geral, que devido ao nível de detalhamento e quantidade de
tópicos, estes temas serão tratados nos próximos posts.

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

Classes em Advpl – Parte 02

02/12/2014 ADVPL, Orientação a Objeto ADVPL, Classes, Orientação a Objetos, Programação

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.

Mais um pouco de teoria

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).

Agora um pouco de código

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"

// Fonte de teste da classe com herança

User Function APTST2()


Local oObj

oObj := APFILHA():New(123)

oObj:SayValue()

Return

// -----------------------------------------------------------

// Classe superior para demonstração de herança

CLASS APPAI

DATA nValue as Integer

METHOD New(nNum) CONSTRUCTOR

METHOD SayValue()

ENDCLASS

// Construtor da classe pai, recebe um valor e guarda.

METHOD New(nNum) CLASS APPAI

::nValue := nNum

Return self

// Mostra o valor guardado na tela, identificando na tela que

// o método da classe Pai foi utilizado

METHOD SayValue() CLASS APPAI

MsgInfo(::nValue,"Classe Pai")

Return

// -----------------------------------------------------------

// Classe Filha, herda a classe pai

CLASS APFILHA FROM APPAI

METHOD NEW(nNum) CONSTRUCTOR

METHOD SayValue( lPai )

ENDCLASS

// Construtor da filha chama construtor da classe pai

METHOD NEW(nNum) CLASS APFILHA

_Super:New(nNum)
return self

// Metodo para mostrar o valor, pergunta ao operador se

// deve ser chamado o metodo da classe pai ou não.

METHOD SayValue() CLASS APFILHA

If MsgYesNo("Chamar a classe pai ?")

_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!

Até o próximo post, pessoal 😉

4 Comentários

Classes em Advpl – Parte 01

01/12/2014 ADVPL, Orientação a Objeto ADVPL, Classes, Orientação a Objetos

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.

Exemplo – Classe APHELLO

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.

01. #include “protheus.ch”

02.

03. USER FUNCTION APTST()

04. Local oObj := APHello():New(“Olá mundo Advpl”)

05. oObj:SayHello()

06. Return

07.

08. CLASS APHELLO

09. Data cMsg as String

10. Method New(cMsg) CONSTRUCTOR

11. Method SayHello()


12. ENDCLASS

13.

14. METHOD NEW(cMsg) CLASS APHELLO

15. self:cMsg := cMsg

16. Return self

17.

18. METHOD SAYHELLO() CLASS APHELLO

19. MsgInfo(self:cMsg)

20. Return .T.

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.

Agora, vamos entender o que aconteceu em cada pedaço deste código:

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(*).

O funcionamento, por dentro

A implementação de cada método é similar à implementação de uma função, apenas devemos


colocar o sufixo CLASS , seguido do nome da classe ao qual o método se refere. Dentro dos métodos,
quando queremos fazer referência a qualquer propriedade da classe, usamos a variável “self”, que
na orientação a objetos do Advpl é uma variável que contém a instância da classe em uso, o que
permite por exemplo que eu tenha um parâmetro com o mesmo nome de uma propriedade e
referencie ambos de formas distintas.

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.

O método construtor, normalmente convencionado como “New”, retorna implicitamente a


instância da classe atual contida na variável “self” no escopo da instância. Cada outro método fora
o construtor pode ter seu retorno próprio, da mesma forma que qualquer função escrita em ADVPL.

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.

Até o próximo post, pessoal 😉

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.

Você também pode gostar