Muita gente odeia utilizar a construção WITH-DO do Pascal. Dizem que é difícil de ler o código, introduz bugs… esqueçam que WITH-DO existe! Eles dizem.
Mas será que WITH-DO é mesmo do mal ou esses programadores é que não sabem utilizá-lo no momento certo?
Introdução
Se você não sabe o que é WITH-DO, aqui está uma boa explicação técnica.
Então você define um bloco ou contexto e pode acessar os métodos e atributos de um Objeto sem a referência explícita à uma variável.
A primeira vista parece que o objetivo é escrever menos e, como bem sabemos, a maioria dos programadores são preguiçosos.
Eu utilizo WITH-DO. Muito. E é claro, também quero escrever menos.
Mas tento escrever menos da maneira correta. Não para poupar bytes, mas para deixar o código mais legível, menos verboso e menos acoplado.
Ao longo de muitos anos utilizando WITH-DO eu posso contar nos dedos de apenas uma mão a quantidade de problemas que já tive ao utilizar essa construção, ou seja, acho que seu uso vale a pena.
Nesse artigo você aprenderá a utilizar a declaração WITH-DO.
Imperativo
Creio que o mal que WITH-DO nos traz é devido ao seu uso 100% imperativo, sem nenhum contexto determinando onde começa ou termina seu uso.
Veja aqui um exemplo de mal uso da declaração WITH-DO, postado no fórum do Lazarus.
Vou replicar o código abaixo, caso o link não esteja mais disponível:
try
btnOK.Enabled:=False;
with datamodule.qryRegister do begin
datamodule.connection.AutoCommit:=True;
datamodule.connection.StartTransaction;
SQL.Clear;
SQL.Text:=
'INSERT INTO table VALUES (:name, :sex, :dateofbirth, :maritalstatus)';
ParamByName('name').AsString:=teName.Text;
ParamByName('sex').AsInteger:=cbSex.ItemIndex;
ParamByName('dateofbirth').AsDate:=StrToDate(teDateOfBirth.Text);
ParamByName('maritalstatus').AsInteger:=cbMaritalStatus.ItemIndex;
SQL.Clear;
SQL.Text:='SELECT LAST_INSERT_ID() AS iddb FROM table';
Open;
idUser:=FieldByName('iddb').AsInteger;
ExecSQL;
Close;
try
datamodule.connection.Commit;
clearFields;
except
datamodule.connection.Rollback;
ShowMessage('Error');
end;
end; //with ends statement
except
ShowMessage('Error: no connection to the database');
end;
bntOK.Enabled:=True;
O que é questionado nesse post é irrelevante.
Apenas analisem o código e vejam como o autor está utilizando o WITH-DO:
- Ele inicia uma transação;
- Configura uma cláusula SQL;
- Passa parâmetros;
- Limpa a query(?)
- Executa a query;
- Commit… ClearFields…
É um código totalmente procedural, imperativo e confuso!
É muito difícil saber o que está sendo utilizado pelo WITH-DO e o que não está, concorda?
Nesses casos, quando há ambiguidade entre métodos utilizados pelo WITH-DO que não deveriam ser utilizados, alguns programadores culpam o WITH-DO quando, na verdade, deveriam culpar a si mesmos por escrever um código dessa maneira.
Essa é uma maneira imperativa de codificação. Procedural. Um código altamente acoplado que, realmente, não deveria utilizar WITH-DO porque só piora as coisas.
Declarativo
A codificação declarativa é utilizada nas linguagens funcionais. Também é utilizada, por exemplo, quando codificamos SQL.
Na codificação declarativa você define o que fazer, mas não se importa como será feito.
SELECT * FROM Clients ORDER BY name;
Quando escrevemos o código SQL acima, não nos preocupamos como o SGBD irá trazer o resultado; quais índices; quais tuplas. Apenas queremos o resultado o mais rápido possível.
Assim deve ser a codificação Orientada a Objetos, onde os Objetos pensam e tomam as próprias decisões, sem que tenha um “controlador” informando como fazer linha a linha.
E o que isso tem haver com o uso do WITH-DO?
Bem, o WITH-DO nos ajuda a transformar um código procedural em algo mais declarativo, mais Orientado a Objetos.
Um código mais Orientado a Objetos é mais legível.
Um código com menos declarações de variáveis, é menos verboso.
E se você não declara variáveis de tipos específicos, automaticamente seu código é menos acoplado, com menos usos de Units para determinar os tipos das variáveis.
Vou lhe mostrar como.
Vamos pegar o mesmo código procedural acima e reescrever utilizando WITH-DO.
Esse código será apenas um pouco mais Orientado a Objetos, com uma melhor legibilidade que o primeiro:
try
btnOK.Enabled := False;
with TDatabase.Instance.StartTransaction do
try
with
TQuery.New(
'INSERT INTO table VALUES (' +
' :name, :sex, :dateofbirth, :maritalstatus)'
)
do
begin
Params
.Add('name', ftString, teName.Text)
.Add('sex', ftInteger, cbSex.ItemIndex)
.Add('dateofbirth', ftDateTime, StrToDate(teDateOfBirth.Text))
.Add('maritalstatus', ftInteger, cbMaritalStatus.ItemIndex)
Execute;
end;
idUser :=
TQuery.New('SELECT LAST_INSERT_ID() AS iddb FROM table')
.Open
.Field('iddb').AsInteger;
Commit;
except
Rollback;
raise;
end;
except
ShowMessage('Error: no connection to the database');
end;
bntOK.Enabled:=True;
O código continua ruim, porém melhor.
Há muita coisa acontecendo num único método.
Não deveríamos parar a refatoração agora. Cada execução deveria estar num método a parte…
No entanto o objetivo é mostrar o uso do WITH-DO e acho que dá pra ver a diferença, certo?
Ao todo são apenas 2 declarações de WITH-DO. Cada declaração tem um contexto bem definido por TRY-EXCEPT ou BEGIN-END. Não há ambiguidade.
A declaração TDatabase.Instance.StartTransaction
irá retornar um
Objeto do tipo ITransaction
.
A declaração TQuery.New
irá retornar um Objeto do tipo IQuery
,
através do Método New.
O código ficou mais legível.
Não há declarações de variáveis.
Não há variáveis globais como existe no código procedural —
datamodule.qryRegister
é um Objeto global que, possivelmente, é
reutilizado em toda a aplicação, sendo um design muito errado,
não thread-safe e gerador de problemas.
Orientado a Objetos
A declaração WITH-DO nos ajuda a escrever um código mais simples e elegante. Mas sejamos sinceros. Utilizamos WITH-DO para produzirmos código procedural. E não tem nada de errado nisso, visto que é quase impossível codificar 100% Orientado a Objetos em linguagens que são imperativas.
Uma vez que WITH-DO define uma variável implícita, o programador pode chamar os métodos na sequência, linha a linha.
O uso de WITH-DO deixa o código melhor, mas se dá pra codificar Orientado a Objetos, é o que devemos fazer.
Então vou reescrever o código.
Não há espaço aqui para a definição das Classes utilizadas no exemplo.
Qualquer dúvida, basta postar nos comentários.
Abaixo um hipotético código declarativo e Orientado a Objetos:
btnOK.Enabled := False;
try
idUser :=
TDatabase.Instance.Transaction(
TTasks.New
.Add(
TQueryTask.New(
'INSERT INTO table VALUES (' +
' :name, :sex, :dateofbirth, :maritalstatus)',
TDataParams.New
.Add('name', ftString, teName.Text)
.Add('sex', ftInteger, cbSex.ItemIndex)
.Add('dateofbirth', ftDateTime, teDateOfBirth.Text)
.Add('maritalstatus', ftInteger, cbMaritalStatus.ItemIndex)
)
)
.Add(
TQueryTask.New('SELECT LAST_INSERT_ID() AS iddb FROM table')
)
)
.Results
.Get('iddb').AsInteger;
finally
bntOK.Enabled := True;
end;
O que está ocorrendo aqui?
Bem, temos TQueryTask
que implementa ITask
.
Temos uma lista de tasks.
A lista é passada para Transaction
que, internamente, irá executar
cada task — chamando o método Execute
que não está explícito no
código — dentro de uma transação.
Por fim Transaction
tem um método Results
que pode ser do tipo
IDataParams
e através de Get
é retornado um valor.
O que acontece dentro desses Objetos é de responsabilidade deles. O programador é apenas o orquestrador dessa comunicação.
Conclusão
Na minha opinião WITH-DO pode e deve ser utilizado.
Muita gente deseja cada vez mais features para a linguagem Object Pascal. Mas será que já sabemos utilizar, de forma eficaz, tudo o que já temos nessa elegante linguagem chamada Object Pascal?
Até onde eu sei, essa feature não existe em nenhuma outra linguagem.
Apenas nós temos o WITH-DO!
Mas será que sou o único que usa e aprecia essa feature? :)