Skip to content

Padrões de Desenvolvimento 2 – Estrutura do Projeto

daniekans edited this page Nov 3, 2018 · 17 revisions

Padrões de Desenvolvimento – Estrutura do Projeto

Aqui serão abordados assuntos mais relacionados ao projeto em si, como a estrutura de arquivos, interface cliente-servidor e o código básico para o desenvolvimento das funcionalidades da aplicação.


Front-end

Como essa parte conterá apenas arquivos e recursos voltados ao cliente, a estrutura do projeto poderá ser definida por aqueles que forem focar no front-end. O mais importante é manter tudo organizado e de mais fácil acesso possível.

Práticas recomendadas:

  • Separe os arquivos em pastas, de acordo com a sua função (arquivos CSS em uma pasta, arquivos JavaScript em outra, etc);
  • Dê nomes objetivos aos arquivos e que tenham relação direta com o seu conteúdo;
  • Analise sempre se será melhor utilizar caminho absoluto ou relativo em alguma parte do projeto para que não haja muitos problemas caso o arquivo atual ou recurso que está nesta localização, ao serem mudados de localização, não precisem de muitas alterações.
  • Será muito importante criar uma forma fácil de fazer requisições AJAX, que serão a base de comunicação com o banco de dados. As páginas não vão ser baseadas em formulários, os quais serão praticamente inexistentes. Dessa forma, foi criada uma classe no JavaScript que tem métodos estáticos que ofereçam uma interface de realização das requisições AJAX (no nosso caso, apenas GET e POST). Será utilizada uma abordagem com Promises. Veja alguns exemplos de uso da classe:
// Requisição simples:
Request.get('caminho-para-o-servidor?parametro=valor')
    .then(function(resultado) {
        /* Manipula os dados recebidos do servidor... */
    });

// Requisição tratando erro:
Request.get('...')
    .then(function(resultado) { ... })
    .catch(function(erro) { /* Tramento do erro... */ });

// Requisição POST com parâmetros fora da URL:
Request.post('caminho-para-o-servidor', 'parametro1=valor1&parametro2=valor2')
    .then(function(resultado) { ... });

// Exemplo usando funções de seta para um código mais sucinto:
Request.get('...')
    .then(res => /* ... */)
    .catch(err => /* ... */);

Atenção: O método post() faz com que uma certa string seja enviada, mas ela não será reconhecida como parâmetros da requisição, mesmo se estiver em uma estrutura "parametro1=valor1&parametro2=valor2", como mostra o exemplo. No back-end, a string enviada é lida através do HttpServletRequest::getReader, que funciona de forma semelhante ao HttpServletResponse::getWriter. Um método para converter a string para parâmetros já está sendo feita. Por enquanto, é preferível o uso do método get().

Back-end

O projeto foi criado com a build engine Maven para o suporte de diferentes perfis de construção e melhor definição de dependências (isso será abordado com mais detalhes depois). Tem praticamente a mesma estrutura de um projeto normal como os que já fizeram em laboratório. No entanto, há algumas adições que serão explicadas.

A ideia do projeto é de se comunicar com o servidor apenas através do método HTTP (ou seja, utilizando somente URLs e requisições AJAX). Fazer requisições de outras formas, como por meio de formulários ou de páginas que estão localizadas no próprio servidor (páginas JSP, por exemplo), gera muita desorganização e dependência de código, tornando difícil encontrar a localização de um erro e principalmente solucioná-lo. Utilizando somente métodos HTTP – especialmente o GET e o POST – e requisições assíncronas, é muito fácil recuperar dados que dependem do servidor. Todo o resto é manipulado no lado do cliente. Esse tipo de padrão é muito semelhante ao que ocorre em uma RESTful API, mas não preenche todos os requisitos para ser uma.

Você já pode ter percebido que o servidor não conterá nenhum recurso que é voltado ao front-end (como páginas web, scripts, imagens, etc). Mas como serão feitas as requisições pelas URLs e de que forma os dados serão retornados? A URL será a de um servlet (contendo os devidos parâmetros), o qual retornará as informações no formato JSON. Um exemplo de URL seria localhost:8080/getproduto?nome=cafe, e a resposta poderia ser algo como {"preco": 90, "peso": 10, "imagem-src": "cafe.png", "descricao": "Saca de Café"}. É como você estivesse apenas chamando uma função comum no JavaScript que retorna um objeto.

A estrutura do projeto é a seguinte:

Estrutura de diretórios:

Estrutura de diretórios

Estrutura geral do projeto:

Estrutura geral do projeto

OBS: alguns arquivos estão com o nome verde ou azul devido à integração com o Git. Nomes de arquivo verdes indicam que ele foi adicionado e nomes azuis indicam que ele foi apenas modificado.

A estrutura é semelhante ao que vocês já estão acostumados. O pacote padrão é o br.cefetmg.staygreen (é comum utilizar em projetos mais elaborados uma nomenclatura que possui o domínio em ordem reversa e o nome do projeto em seguida). No entanto, as classes não devem ser colocadas diretamente nesse pacote. A estrutura padrão é a seguinte:

  • br.cefetmg.staygreen.servlet: contém todas as servlets do projeto (é comum colocar "Servlet" como sufixo no nome das classes);
  • br.cefetmg.staygreen.service: neste pacote haverão as classes que fornecem algum tipo de serviço, isto é, uma funcionalidade que, apesar de ser independente dos servlets, tem uma relação com o projeto (tem um certo nível de dependência). Por exemplo, uma classe VendaService poderá fornecer métodos para a realização de qualquer tipo de venda no projeto (é independente do código das servlets); entretanto, essa classe é específica para este projeto, não podendo ser reaproveitada na maior parte das outras aplicações. Resumindo, os serviços são a lógica do programa;
  • br.cefetmg.staygreen.util: as classes desse pacote, assim como os serviços, fornecem algum tipo de funcionalidade independente. Porém, tais recursos são tão gerais que independem totalmente do projeto, como por exemplo leitura de arquivo, interface básica com o banco de dados, operações matemáticas, entre outros. Em suma, utilize ou crie classes desse pacote para funções gerais e crie os serviços para ajudar em algum processo específico da aplicação, como interface de alto nível com o banco de dados (como resgatar dados de uma tabela específica sob uma certa condição), compra e vendas de pertences, verificação de informações do banco de dados (como depreciação ou gerência das atividades do produtor), entre outros;
  • br.cefetmg.staygreen.table: contém classes que representam tabelas. Todas essas classes devem estar anotadas com @Tabela("nome_da_tabela") para que seja possível utilizar os recursos da classe SQL. Falarei mais disso adiante;
  • br.cefetmg.staygreen.exception: conterá todas as eventuais exceções específicas do projeto;
  • br.cefetmg.staygreen.annotation: conterá as anotações utilizadas pela aplicação (não precisa se preocupar com essa parte);

Como se pode perceber, já existem algumas classes no pacote br.cefetmg.staygreen.util. Você deve utilizá-las para facilitar grande parte das tarefas. Não se preocupe com o código de cada uma dessas classes, apenas com o seu uso (semelhante ao que fazemos ao usar as Collections). Darei a seguir uma breve descrição de cada uma delas (leia a documentação presente no código para saber mais):

  • IO: é uma classe voltada à manipulação de entrada e saída. Seus únicos métodos são para resgatar as propriedades de um arquivo com extensão .properties – chamado de arquivo de propriedades, útil para descrever dados de configuração – e para conseguir um caminho compatível com o sistema operacional que está sendo utilizado (a notação dos caminhos muda, especialmente o sentido da barra). A classe IO poderá ser estendida futuramente, caso seja necessário;
  • JSON: possui métodos para conversão de dados no formato JSON (string para objeto, objeto para string e arquivo para objeto). Use-a principalmente quando for enviar as respostas para o cliente/front-end;
  • Reflection: classe que oferece métodos úteis relacionados a Reflexão (não precisa se preocupar com essa parte);
  • SQLConnectionFactory: classe que segue o design pattern Factory. A aplicação só terá uma conexão aberta, instanciada na classe SQL. Portanto, é desaconselhado utilizar esta Factory;
  • SQL: fornece a interface básica com o banco de dados, facilitando bastante os processos caso seja utilizada corretamente. Ela possui cinco métodos principais:
    • query(String): realiza qualquer requisição e retorna seu resultado, caso tenha;
    • insert(Object): insere um objeto em sua respectiva tabela;
    • update(Object): atualiza um registro de uma tabela baseado em um objeto;
    • delete(int, Class): deleta um registro de uma tabela que possui um id determinado;

Um pouco mais sobre a representação de tabelas

É importante destacar novamente que as classes que representam tabelas precisam estar anotadas com @Tabela, indicando qual é a tabela no banco de dados a qual a classe ou objeto passado a esses métodos estão associados (exemplos a seguir). As classes também precisam ter um atributo anotado com @Id, análogo ao id de um registro na tabela. É preferível declarar os atributos da classe utilizando apenas classes (não use int, por exemplo, mas sim Integer). O padrão a ser seguido é usar somente as classes para tipos primitivos (classes invólucro Integer, Double, etc), a classe String e a classe Calendar (não utilize Date, pois ela já está depreciada). Todos os atributos precisam ter os nomes exatamente como os das colunas da tabela do banco de dados. Um último método da classe SQL, chamado getNomeTabela(Class) retorna o nome da tabela baseado na classe passada como parâmetro. Caso não saiba, classes podem ser passadas como parâmetro através do membro class. Exemplo:

String tabela = SQL.getNomeTabela(Produtor.class);

Exemplo de classe que representa uma tabela:

package br.cefetmg.staygreen.table;

@Tabela("produtor")
public class Produtor {
  
  @Id
  private Long idProdutor; // Prefira usar classes em vez de tipos primitivos
  private String nome;
  private Integer idade;
  
  /* Recomendo que sempre crie um construtor em que seja possível informar
   * todos os atributos da classe, pois facilitará o uso futuro: */
  public Produtor(Long idProdutor, String nome, Integer idade) {
    // ...
  }

  /* Métodos da classe... */

}

Note que cada objeto dessas classes anotadas com @Tabela representa uma linha da tabela associada. Nesse exemplo, cada objeto da classe Produtor representa uma linha da tabela "produtor". Os métodos da classe SQL reconhecem isso e realizam devidamente a operação. Caso o objeto ou classe informados não sejam válidos (não tenham as anotações), uma exceção será lançada. É preciso lembrar que os objetos dessa classe, ao representar linhas, agem como "objetos de transporte". Isso significa que nem sempre você irá preencher todos os campos do objeto; tudo dependerá do que você estiver querendo fazer. Por exemplo, o id pode ser irrelevante ao inserir um dado, já que é calculado automaticamente, porém é de extrema importância para o update de alguma linha. Outro exemplo é caso você deseje atualizar somente uma coluna de um registro, não se importando com os outros campos, que são deixados como null). Veja um exemplo de uso dos métodos estáticos de SQL:

public class AlgumaClasse {

  public void algumMetodo() {

    // Inserindo um registro na tabela:
    SQL.insert(new Produtor(null, "Maurício", 57)); // nesse caso, informar o id é irrelevante
    SQL.insert(new Produtor(null, "Geraldo", 62));
    SQL.insert(new Produtor(null, "Rosália", 59));

    // Fazendo o update de algum registro:
    SQL.update(new Produtor(2, null, 65)); // atualiza apenas a idade do registro com
                                           // id = 2 (Geraldo, nesse caso)
    SQL.update(new Produtor(1, "Marcos", null)); // atualiza apenas o nome do registro
                                           // com id = 2 (Maurício)

    // Deletando registros:
    SQL.delete(3, Produtor.class); // deleta o registro com id = 3 (Rosália) da
                                   // tabela associada à classe Produtor
    
    // Recuperando todos os registros:
    List<Produtor> produtores = SQL.getRegistros(Produtor.class);

    // Realizando query genérica:
    ResultSet resultado = SQL.query("SELECT * FROM "
        + SQL.getNomeTabela(Produtor.class) + " WHERE idade >= 60");
    
  }

}

Agora que você já sabe melhor como o projeto é estruturado, é preciso falar sobre as dependências e a respeito da configuração do projeto através dos perfis do Maven.

Gerenciamento de dependências

É muito comum a utilização de bibliotecas ou drivers externos em aplicações. Eles são necessários para facilitar uma operação específica do programa, estabelecer o driver necessário para a conexão com o banco de dados, possibilitar o uso de frameworks, entre outras coisas. Ficar transportando esses arquivos junto com o projeto o deixa muito pesado e geralmente causa erros de compatibilidade caso precise abrir o projeto em outro computador. Além desses problemas principais, existem vários outros erros relacionados, e uma solução interessante é apenas declarar as dependências da aplicação, sem que haja uma ligação direta dos arquivos com o projeto em si. Para utilizar as dependências declaradas, é necessário apenas fazer o download delas e pronto (o NetBeans, ao ver que tem algo faltando, dará um aviso; nesse momento, clique para "Resolver Problemas", caso apareça algo do tipo, e em seguida clique para baixar as dependências). A pasta "Dependências", como é mostrado na imagem com a estrutura geral, contém as bibliotecas ou outros arquivos necessários ao projeto. Como utilizaremos o Git, há como declarar que tudo que for gerado pela construção do projeto será ignorado e, dessa forma, podemos baixar localmente as dependências somente quando for necessário (falarei sobre o controle de versão em outra página da wiki).

Configurações do projeto

Conforme é mostrado na imagem com o esquema de arquivos, há um diretório chamado "resources". É nele que serão colocados arquivos caso seja necessário. Na verdade, esta é a Maven Resources API, que facilita um pouco o acesso a arquivos externos no projeto (fazer isso em uma aplicação web comum é bastante complicado). Dentro do diretório há apenas o arquivo configuracoes.properties. É nele que estão as credenciais para a conexão com o banco de dados. Mas certamente o nome de usuário e a senha serão diferentes para várias pessoas (como ocorre nos computadores dos laboratórios, onde a senha é 123456, mas localmente a senha quase sempre é uma string vazia). Como resolver este problema? O Maven permite a criação de perfis de construção, que são basicamente definições diferentes que são aplicadas durante contrução da aplicação. O arquivo pom.xml é um dos mais essenciais do projeto, pois tem toda a definição de como a aplicação será construída. Observe esta parte do arquivo:

<project>

<!-- declarações de dependências, metadados do projeto, etc... -->

    <profiles>
        <profile>
            <id>geral-local</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <db.url>jdbc:mysql://localhost:3306/staygreen</db.url>
                <db.usuario>root</db.usuario>
                <db.senha></db.senha>
            </properties>
        </profile>
        <profile>
            <id>geral-cefet</id>
            <properties>
                <db.url>jdbc:mysql://localhost:3306/staygreen</db.url>
                <db.usuario>root</db.usuario>
                <db.senha>123456</db.senha>
            </properties>
        </profile>
    </profiles>
</project>

Cada tag <profile> dentro de <profiles> contém um perfil diferente. As propriedade de cada perfil são definidas dentro de <properties> (nesse caso, as informações são as tags <db.url>, <db.usuario> e <db.senha>). Antes de executar a aplicação, é possível definir o perfil como mostra a imagem:

perfis de construção

Defini 2 perfis básicos: um chamado geral-local (usuário é root e senha é string vazia, perfil padrão) e outro chamado geral-cefet (usuário é root e senha é 123456) Durante a construção do projeto, o Maven procura nos arquivos de propriedades onde os dados do perfil podem ser colocados. O conteúdo de configuracoes.properties é o seguinte:

db.url = ${db.url}
db.usuario = ${db.usuario}
db.senha = ${db.senha}

O que está dentro de ${...} é substituído pelas propriedades do perfil, e a classe SQLConnectionFactory consegue criar a conexão devidamente. Caso o computador onde você está executando o projeto tenha outro nome de usuário, outra senha ou até uma URL diferente, basta criar um perfil (exemplo: daniel-local). OBS: é necessário construir novamente o projeto (clicando no martelo próximo ao botão de execução) para qualquer modificação no pom.xml. Sempre faça o build quando estiver iniciando a aplicação pela primeira vez.

Banco de Dados

A maior parte operações com o banco de dados serão feitas, como você já pôde perceber, com códigos Java. Entretanto, para a criação de tabelas e inserção de dados predefinidos (qualquer coisa que já era para estar no banco de dados desde o início), vai ser essencial colocar os comandos em arquivos com a extensão .sql. Isso vai ser necessário para que todos possam atualizar o banco de dados facilmente e quando quiserem, pois são arquivos que poderão ser importados no MySQL – banco de dados que iremos utilizar ao longo do projeto.

São necessários apenas dois arquivos, localizados em src/main/db:

  • schema_db.sql: além da definição do banco de dados (já está no arquivo), possui todas as queries apenas de criação de tabelas (CREATE TABLE tabela (...);). Esse deve ser o primeiro script a ser importado, pois define a estrutura do banco de dados (e, logo após a importação, tudo é excluído);
  • popular_db.sql: contém todas as queries que somente populam as tabelas, isto é, inserem dados nas tabelas (INSERT INTO tabela (...) VALUES (...), (...), ..., (...);). Note que isso não inclui registros que são feitos apenas durante o uso da aplicação, como as tarefas do produtor, os patrimônios, etc, mas haverá inserção de dados prévios, como tipos predefinidos de tarefas, preços de insumos, preços de frete para cada região possível, etc.

Outras recomendações

  • Siga as convenções de código do projeto;
  • Quando ocorrer algum erro no servidor após a chamada do servlet, não retorne uma string vazia ou algo do tipo. Sempre altere o código de resposta (500, 404, 200, etc) utilizando o método setStatus(int) de HttpServletResponse. Minha sugestão: crie uma classe Resposta, que contém constantes para as respostas mais comuns e que ofereça uma interface de criar respostas ao cliente facilmente, podendo inclusive conter a mensagem de erro da exceção. Tente tratar no JavaScript respostas com códigos que não sejam 200.
  • Note que a estrutura de quase todas as servlets será algo como:
// declaração de pacote e importações...

@WebServlet("/funcionalidade")
public class MeuServlet extends HttpServlet {

  @Override
  public void service(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // resgata parâmetros recebidos, caso existam:
    // ...
    // executa alguma lógica com estes dados, podendo usar serviços e outros recursos:
    // ...
    // responde o cliente com um certo dado:
    try (PrintWriter out = resp.getWriter()) {
      out.println(JSON.stringify(/* dado de resposta */));
    }
  }

}
  • Sempre que criar uma tabela no schema.sql, defina o encoding para UTF-8, da seguite forma: CREATE TABLE tabela (...) DEFAULT CHARACTER SET utf8;.
  • Caso tenha alguma dúvida (tenho quase certeza que terá), o Daniel tentará te ajudar.

Referências

Alguns links úteis: