30.5.05

EXISTS, IN e NULL

Ora aqui está um daqueles artigos que servem para tirar algumas dúvidas base.

Quando se aprende uma tecnologia nova, há alguns pontos que passam despercebidos, e levam-nos mais tarde a perder algum tempo de volta do google para clarificar algumas dúvidas subtis. O artigo em causa fala do comportamento dos operadores EXISTS e IN acerca de valores NULL.

Espero que vos seja esclarecedor tanto quanto foi para mim.

Um abraço,
Gama Franco

27.5.05

Os testes e o modelo de dados.

O modelo de dados é certamente uma das partes mais importantes de qualquer projecto, e é geralmente o ponto de partida da fase de desenvolvimento. Também é comum destacar-se uma camada para aceder a esses mesmos dados. No caso do Java, a maioria das vezes, utiliza-se o modelo ‘MVC’, cabendo à camada ‘Model’ esta tarefa. Nas arquitecturas de .Net a camada equivalente é denominada de ‘DataAccess’.

Quem utiliza métodos ágeis depara sempre com o mesmo problema no início de qualquer projecto: “Como é que vou testar a camada de acesso aos dados sem introduzir alterações na base de dados de testes?”
Esta dúvida surge porque, se estamos a testar a inserção dos dados de clientes num teste unitário, e noutro a verificar o número de clientes que existem no sistema, é certo que o segundo vai falhar se correr depois do primeiro. Desta forma está-se a violar o princípio do ‘Isolamento’.
Neste artigo irei sugerir uma técnica que permite testar a interacção entre o nosso programa e o modelo de dados, sem que se viole o isolamento dos testes de regressão.
A filosofia desta técnica consiste em registar todos os acessos numa única transacção, e depois de verificadas todas as condições, forçar o ‘rollback’ da mesma.

Por questões de simplicidade o exemplo será apresentado em Java recorrendo a uma ligação JDBC para MySql, mas a sua utilização em C# ou noutra linguagem orientada aos objectos é análoga. Gostaria apenas de realçar que para se utilizar esta técnica é necessário que o ‘DBMS’ ou a ‘Framework’ suporte transacções. Para uma arquitectura em Java com MySql é obrigatória a utilização de tabelas do tipo ‘INNODB’ por ser o único que suporta transacções. Alternativamente pode-se recorrer a um contentor de ‘EJBs’ que garanta a transacção ao nível da ‘Framework’.
Para a tecnologia .Net a minha preferência resume-se à utilização do ‘Distribucted Transaction Manager’, e efectuar manualmente a gestão das transacções.

Em primeiro lugar é necessário encapsular a ligação à base de dados utilizada, desta forma ficaremos com a classe:


public class Database implements IDatabase
{
private Connection connection;
private boolean transactionStarted = false;

public Database(String databaseName, String username, String password)
throws ClassNotFoundException, SQLException
{
Class.forName("org.gjt.mm.mysql.Driver");
connection = DriverManager.getConnection(...);
}


A nossa classe irá implementar um Interface que apresenta todos os seus métodos públicos, e a ligação à base de dados é feita no construtor. A forma como a ligação é feita é irrelevante, e pode-se optar por ler os dados da ligação de um ficheiro de configuração.


public Connection getConnection()
{
return connection;
}

public void beginTransaction()
throws TransactionAlreadyStartedException, SQLException
{
if (transactionStarted)
throw new TransactionAlreadyStartedException();
setTransactionStarted(true);
connection.setAutoCommit(false);
}

public void commitTransaction()
throws TransactionNotStartedException, SQLException
{
if (!transactionStarted)
throw new TransactionNotStartedException();
connection.commit();
connection.setAutoCommit(true);
setTransactionStarted(false);
}

public void rollbackTransaction()
throws TransactionNotStartedException, SQLException
{
if (!transactionStarted)
throw new TransactionNotStartedException();
connection.rollback();
connection.setAutoCommit(true);
setTransactionStarted(false);
}

public boolean inTransaction()
{
return transactionStarted;
}


O método getConnection() retorna a ligação que se está a utilizar, mas é importante garantir que o nosso código não vai executar métodos que afectem o estado das transacções. Eu optei por esta forma por questões de simplicidade, mas se quisermos ser rigorosos teremos que utilizar uma técnica em que a ligação não seja exposta publicamente.



protected void setTransactionStarted(boolean transactionStarted)
{
this.transactionStarted = transactionStarted;
}
}


Este método será utilizado para se iniciar uma nova transacção.

Em seguida iremos criar uma 'Factory', por duas razões. Em primeiro lugar queremos garantir que apenas existirá um objecto do tipo Database no nosso programa, depois porque mais tarde iremos substituir este objecto por outro sem que o nosso programa se aperceba disso.


public class DatabaseFactory implements IDatabaseFactory
{
private IDatabase database;
private ILog log;

public ModelFactory() throws ClassNotFoundException, SQLException
{
}

public setDatabase(IDatabase database)
{
this.database = database;
}

public IDatabase getDatabase()
{
return database;
}
}


Esta classe é bastante simples, e penso que dispensa comentários.

Agora falta arranjar uma forma que permita ao nosso programa localizar a factory disponível, por forma a obter a ligação. Eu optei pelo padrão 'Service Locator', mas existem outras técnicas válidas.


public final class Services {

private static IModelFactory modelFactory;

private Services() {
}

public static IModelFactory geModelFactory()
{
return modelFactory;
}

public static void setModelFactory(IModelFactory factory)
{
modelFactory = factory;
}
}


No ponto de arranque do nosso programa procede-se à configuração da classe Services, para podermos disponibilizar as fábricas que serão utilizadas durante a execuçãp. Neste exemplo apenas temos uma 'Factory' mas é comum existir pelo menos uma por camada.

Agora que temos o nosso ambiente de desenvolvimento completo, podemos começar a trabalhar uma classe que irá aceder aos dados. No entanto, à boa maneira ágil, iremos começar pelos testes :)
O nosso objectivo é registar um cliente e a sua morada, estando estes dados em tabelas diferentes (o exemplo poderia ser melhor, mas estou com falta de imaginação). Por questões de simplicidade o cliente é apenas composto pelo seu nome, a morada pela localidade e não irei utilizar sequências nas tabelas (o nome é a chave).

Nos testes iremos substituir a classe Database por uma outra cuja transacção não seja efectuada. Assim teremos uma classe de nome DatabaseTestInstance para ser utilizada exclusivamente nos testes.

   
public class DatabaseTestInstance extends Database implements IDatabase
{
public Database(String databaseName, String username, String password)
throws ClassNotFoundException, SQLException
{
super(databaseName, username, password);
getConnection().setAutoCommit(false);
}

public void commitTransaction()
throws SQLException, TransactionNotStartedException
{
rollBackTransaction();
}

public void rollBackTransaction()
throws SQLException, TransactionNotStartedException
{
super.rollbackTransaction();
getConnection().setAutoCommit(false);
}
}


Como podemos observar, o contrutor desta classe desliga o modo 'auto-commit' e faz sempre 'rollback' quando uma transacção termina.

Passando ao teste propriamente dito temos:


public class ClientTest extends TestCase
{
private IClientModel clientModel;

public TestBase(String name) throws ClassNotFoundException, SQLException
{
super(name);
IDatabaseFactory databaseFactory = new DatabaseFactory();
modelFactory.setDatabase(
new model.tests.unit.injections.Database
("database", "user", "pass"));
Services.setModelFactory(databaseFactory);
}


Aqui pode-se observar que estamos a substituir a classe que representa a ligação por uma classe de testes. A este conceito dá-se o nome de 'Dependency Injection'.


public static junit.framework.Test suite()
{
junit.framework.TestSuite suite =
new junit.framework.TestSuite(MillionaireTest.class);
return suite;
}

protected void setUp() throws java.lang.Exception {
super.setUp();
clientModel = new ClientModel();
}

public void testInsertClient() throws SQLException {
clientModel.InsertClient("Maria", "Lisboa");

IDatabase database = Services.getDatabaseFactory().getDatabase();
Statement statement = database.getConnection().createStatement();
String query = "SELECT count(*) as count " +
"FROM clients, local" +
"WHERE name like 'Maria'";
statement.execute(query);
ResultSet result = statement.getResultSet();
result.next();

assertTrue("Client not inserted.", result.getInt("count") == 1);

result.close();
}


Este teste verifica a inserção de um novo cliente. O próximo testa se o número de clientes na tabela é o esperado.


public void testCountClients() throws SQLException {
assertIsTrue("Number of clients is not the expected",
clientModel.countClients() == 10);
}


Desde já pode-se verificar que se o isolamento entre os testes não se verificar, correr testInsertClient e depois testCountClients é diferente de correr testCountClients e depois testInsertClient.

Agora que temos os testes, falta apenas programar a classe ClientModel:


public class ClientModel implements IClientModel
{

private final String GET_NUMBER_CLIENTS = "SELECT count(*) as count " +
"FROM clients";

private final String INSERT_CLIENT = "INSERT INTO clients VALUES(?)";

private final String INSERT_LOCAL = "INSERT INTO local VALUES(?, ?)";

public ClientModel()
{
}

public int countClients() throws SQLException
{
IDatabase database = Services.getDatabaseFactory().getDatabase();
Statement statement = database.getConnection().createStatement();

statement.execute(GET_NUMBER_CLIENTS);
ResultSet result = statement.getResultSet();
result.next();

int numberOfClients = result.getInt("count");
result.close();

return numberOfClients;
}


Note-se a subtileza da declaração da variável 'database'. O valor atribuído a esta variável é o que a fábrica retornar, e é isso que torna possível substituir a ligação à base de dados sem que o programa tenha conhecimento.

Agora o método em falta:


public void insertClient(String clientName, String local)
throws SQLException, InvalidValueException, ModelException
{
IDatabase database = Services.getDatabaseFactory().getDatabase();
Statement statement = database.getConnection().createStatement();

try {
db.beginTransaction();
insertClient(clientName);
insertLocal(local, clientName);
db.commitTransaction();
}
catch (TransactionException e)
{
throw new ModelException(e);
}
}
...


Deixo por implementar os métodos insertClient e insertLocal. De qualquer forma, é possível verificar que o comportamento de db.commitTransaction() é diferente caso se esteja em testes ou a executar o programa.


Resumo:
Verificámos como é possível garantir o isolamento dos testes unitários no que diz respeito à utilização de um modelo de dados. Para isso tivemos que encapsular a ligação numa classe, utilizar uma fábrica e injectar as dependências através de um 'service locator'.
Gostaria ainda de realçar que esta técnica funciona se o nosso programa não efectuar vários pedidos ao DBMS em simultâneo (i.e. não efectuar transacções concorrentes). Além disso, se acima da camada de acesso aos dados necessitar de iniciar transacções, esta técnica deve ser aplicada nessa camada também. Para evitar problemas futuros, é aconselhável começar transacções sempre na mesma camada.

Um abraço,
Gama Franco

24.5.05

Da Web para a Net.

Quem tem o vício de subscrever 'RSS' e está constantemente a trocar de 'PC' ou a formatar a sua máquina habitual, certamente deparou com o problema de colocar novamente a sua lista em funcionamento. A maior parte das aplicações podem exportar a lista para um determinado formato mas não deixa de ser um procedimento aborrecido, e corre-se sempre o risco de formatar a máquina antes de se lembrar que havia uma lista de 'RSS' para copiar. Já para não referir aqueles caso em que acontece uma catástrofe a ao PC, mas também quem é que não faz 'Backups'???

Aconteceu-me que tinha uma lista considerável de 'Blogs' e afins no computador onde trabalhava até à um mês e meio, e esta lá ficou. Como tive que refazer a lista, decidi que desta vez iria utilizar uma página na Internet para o efeito. Antes de deitar mãos à obra, fiz uma pesquisa no 'Google' para ver se alguém teve uma ideia semelhante à minha (acontece sempre). Depois deparei com o 'Bloglines'. Este serviço permite organizar uma lista de 'RSS', mas tem outras funcionalidades, das quais gostava de destacar duas delas.

A primeira permite a visualização de uma lista de 'Blogs' com o mesmo contexto de um que se esteja a seguir. Já encontrei alguns 'sites' interessantes à custa desta funcionalidade.
A outra funcionalidade que gostaria de destacar é a facilidade com que partilhamos a nossa lista de 'RSS', ora vejam.

Adicionalmente existem também uma variedade de 'plugins' que permitem receber notificações quando um novo artigo é colocado num 'site' da nossa lista. Cheguei mesmo a instalar um para o 'Firefox', mas este não funcionou como eu esperava...

Agora só estou à espera de um pouco de paciência para tentar aplicar esta técnica à minha lista de favoritos. Sempre me agradou a ideia de ter os recursos disponíveis na internet em vez de na própria máquina. Páginas como o 'Bloglines' são sempre de grande utilidade.

Um abraço,
Gama Franco

20.5.05

O custo de uma boa variável.

Desde os primeiros anos de faculdade que participei em várias discussões sobre qual o nome a dar a uma determinada variável. Na realidade, os meus primeiros programas tinham sempre algo do tipo:

char *cstmr;

Que com muita imaginação se pode perceber que nesta variável iria guardar o nome de um cliente. É claro que conforme ia introduzindo 'bugs' nos meus programas passava a dar mais valor ao nome das variáveis, mas depois surgia a dúvida sobre o tipo de notação a utilizar:

char *costumer_name;

ou

char *costumerName;

ou ainda

char *CostumerName;

Na realidade estas acabavam por ser discussões inúteis, que nada contribuíam para a celeridade do trabalho. Muitas vezes a filosofia da tecnologia com que se está a desenvolver já tem algo a dizer sobre a notação que utilizar, mas quando isso não acontece acho que a única coisa importante é que todos os elementos do projecto sigam a mesma regra.

Mas apesar disto houve algo que me passou despercebido até ler este artigo, da autoria de Joel Spolsky. Nele é sugerida uma notação que facilita a detecção de erros, e creio que todos sabemos a dor de cabeça que é detectar e corrigir bugs. Apesar de ser uma sugestão simples, a sua subtileza surpreendeu-me. Acho que vale a pena perder dez minutos e evitar alguns dissabores.

Aposto que após lerem o artigo ficarão a pensar no custo efectivo de uma má escolha no nome das variáveis, pelo menos foi o que aconteceu comigo.

Um abraço,
Gama Franco

19.5.05

O primeiro e provavelmente o pior.

Como é fácil de verificar este é a primeira entrada no recém-criado "A vida não é um padrão". Sendo novo nestas lides, espero que este seja de todos o meu pior 'post'.

Em primeiro lugar gostaria de registar a razão deste Blog (sei que não será fácil identificar a piadola do nome). "A vida não é um padrão" serve para registar e divulgar algumas leituras importantes que vou encontrando pela 'net', focando-me principalmente nas novas tecnologias e mais precisamente na Engenharia de Software. É possível que numa ou outra altura venha a fugir deste tema, mas espero que seja raro.

Como costuma dizer um amigo meu, "esta coisa de ser Engenheiro Informático é pior do que ser médico". É claro que se refere meramente ao esforço que temos que fazer para nos mantermos actualizados, tendo em conta a velocidade vertiginosa a que esta área evolui. Este facto é mais verdadeiro se notarmos a existência de alguns masoquistas que (tal como eu) se recusam a focar os seus conhecimentos apenas numa tecnologia.

Logo aqui podemos notar dois tipos de abordagem a exercer Engenharia Informática, uma que se foca numa tecnologia e provavelmente fica-se a ganhar um ordenado chorudo antes de se ter filhos, a outra tenta-se saber um pouco de tudo e a probabilidade baixa radicalmente. Esta é a percepção que tenho, e tem muito a ver com a natureza do mercado Português. Mas sobre este tema espero falar mais tarde.

De qualquer forma, pretendo durante os próximos tempos colocar neste local referências aos artigos que acho mais pertinentes, e talvez mais tarde publicar alguns da minha autoria (serão os menos lidos). Isto porque quem perde algum tempo a investigar novas metodologias e tecnologias acaba sempre por apanhar muito lixo pelo meio, porque a boa informação, aquela que marca a diferença, é difícil de encontrar.

Um abraço,
Gama Franco