Seções iMasters
Certificações + Java

Certificação SCJD – Parte 02

Mesmo a Oracle mudando as siglas da prova SCJD para Oracle Certified Master, Java Developer,
vale lembrar que o conteúdo manteve-se o mesmo. Com isso, venho neste
artigo continuar a sequência de dicas gerais para os candidatos
interessados. Antes de entrar na implementação do seu projeto oficial,
organize suas idéias com as dicas abaixo!

A. A prova se caracteriza muito bem com o modelo Waterfall de desenvolvimento. Com isso, é possível organizar a sequência do desenvolvimento da seguinte forma:

  1. Levantamento de Requisitos
  2. Definição de fluxos de dados
  3. Protótipo de GUI
  4. Arquitetura (Design)

B. O candidato é obrigado a fazer todos os requisitos discriminados
na especificação.
Na prova, fica bem claro que qualquer requisito não
atendido resulta na reprovação automática. Baseado nessa premissa, o
candidato deve prestar atenção nas palavras “must” e “must not” ocorridas no texto de especificação. A dica é:

Faça uma listagem das coisas que a prova diz que DEVEM ser
feitas e daquelas que NÃO DEVEM ser feitas. Faça esse “check-list”
durante todo desenvolvimento e principalmente antes de se submeter à prova
para avaliação.

C. Também fica bem claro na prova que o candidato não receberá pontos
por fazer/entregar extras, coisas não requisitadas
. A dica é:

Não inclua nada no projeto (por exemplos arquivos, diagramas,
desenhos, testes etc) a não ser aqueles realmente especificados e não
implemente nenhum recurso (por exemplo cache, paginação, ordenação,
classificação etc) além daqueles que realmente forem requisitados.

O problema-chave desse ponto é que existe a possibilidade de o
candidato perder pontos entregando recursos extras. Por que quanto mais
código ele entregar, maior a probabilidade de se perder preciosos
pontos. Por isso, tente evitar qualquer complicador de reprovação.

D. Escreva o documento de arquitetura (choices.txt) e o JavaDoc no
mesmo momento em que se idealiza e implementa as funcionalidades
. A dica
é:

Nunca os deixe para o final do projeto, porque o candidato
pode perder a linha do raciocínio deixando-os muitos mais complicados,
difíceis e sem argumentos de explicação e, consequentemente, documentação.

O único documento que deve ser deixado por último é a documentação do
usuário final, que deve conter os descritos e os print-screens das
interfaces gráficas.

E. Implemente seu projeto de forma logicamente sequencial e
relacionada
para que se minimize as probabilidades de bugs e manutenções, uma
vez que existe tempo determinado para a entrega. Por isso, implemente o
projeto na seguinte sequência:

1. Server

  • Acessos ao arquivo
  • Bloqueio
  • Identificadores de Bloqueio
  • Acessos locais
  • Acessos Remotos

2. GUIs

  • Classes GUIs
  • Comunicação local
  • Comunicação remota
  • Documentação Usuário Final

F. Em minha opinião, o principal foco dessa prova é a parte
arquitetural.
Ou seja, o candidato estará sendo criticamente avaliado em
como ele elaborou e relacionou as estruturas de classes existentes na
solução proposta. Baseado nessa idéia, seguem algumas dicas básicas
relacionado com a elaboração de um código claro, limpo, flexível e
manutenível:

  • Reduza os escopos das variáveis. Ou seja, declare e aloque as variáveis dentro dos menores escopos possíveis.
  • Atribua ponteiro nulo para as referências de objetos que não
    apresentarem mais utilizações no ciclo de execução do sistema, deixando
    claro na programação que o determinado objeto está elegível para o
    coletor de lixo.
  • Faça seu código o mais claro possível nomeando corretamente as
    classes como substantivos, métodos como verbos, atributos como
    características e variáveis locais como recursos.
  • Classes devem ser criadas e colocadas em pacotes que possuam
    funções similares ou dependentes entre elas. Os pacotes devem ser
    considerados “unidades autônomas” ou “blocos funcionais” que possam
    ser compilados de forma independente, minimizando o impacto numa
    futura substituição.
  • Use os princípios do encapsulamento reduzindo ao máximo a
    visibilidade dos recursos das classes. Ou seja, atributos sempre
    privados com acesso get/set e nunca deixe público aqueles métodos
    de controle interno.
  • Não dê margem para código procedural escrevendo classes gigantes com
    métodos enormes abusando de recursos estáticos. Passe o pente fino nas
    classes usando o princípio da responsabilidade única. Caso aconteça
    positivo para múltiplas responsabilidades, crie outras classes e/ou
    outros métodos que separe as responsabilidades corretamente.
  • Identifique as abstrações das classes e faça-as de forma granular.
    Um projeto flexível é basicamente composto de varias pequenas classes
    com relacionamentos entre si.
  • Evite longa lista de argumentos nos métodos. Veja a possível
    necessidade da criação de uma classe que reflita o encapsulamento dos
    determinados dados envolvidos no contexto.
  • Prefira usar padrões de projeto em vez de inventar soluções
    próprias. Lembre-se de que os padrões acontecem na infraestrutura do
    projeto e, assim sendo, eles não podem de maneira nenhuma ser encaixados à
    força sem o devido contexto.
  • Uma classe reflete uma abstração que encasula características e
    comportamentos, por isso, evite escrever classes sem atributos e/ou
    comportamentos. Exceto em casos embasados na utilização de padrões
    devidamente reconhecidos e documentados.
  • Use sobrecarga em vez de lógicas. Ou seja, prefira fazer métodos
    sobrecarregados que recebam diferentes números de parâmetros em vez de
    um contendo uma serie de comando de decisões.
  • Use preferencialmente soluções da API JSE para resolver questões de
    projeto em vez de ficar inventando ou projetando soluções próprias.
  • Não invoque métodos potenciais à sobreposição em construtores.
  • Não crie mais objetos de que necessite.
  • Evite códigos aninhados, recursos e complexos.
  • Use preferencialmente tipos enumerados em vez de constantes finais, valorizando assim os princípios OO.
  • Faça reutilização código de forma certa, aplique herança para
    classes pertencentes à mesma hierarquia e associação nas outras.
    Identifique e impeça a herança para as classes finais concretas.
  • Sempre procure usar a solução mais simples que funcione, lembrando
    que não se ganham pontos extras por coisas mirabolantes. Muito pelo
    contrario, na maioria dos casos elas acabam deixando o código complexo e
    confuso, perdendo em clareza e manutenibilidade.
  • Esconda detalhes de execução codificando com interfaces quando o
    caso acontecer. Ou seja, sempre use referencias polimórficas ao invés de
    concretas.

G. O candidato necessitará de tomar uma série de decisões relacionadas
à elaboração da infraestrutura das classes.
As decisões deverão ser
descritas e anexadas no projeto como parte avaliativa da solução. Por
isso, seguem algumas dicas relacionadas com a confecção desse documento:

  • Ressaltando novamente, documente as decisões arquiteturais durante o
    desenvolvimento, não deixando para o final. Ou seja, documente no
    momento em que for decidido.
  • Podem aparecer circunstâncias de projeto que poderão nortear algumas
    decisões arquiteturais baseadas em funcionalidades que não foram 100%
    especificadas ou explicitamente definidas no documento de requisitos.
    Nesses casos, documente tanto as opções arquiteturais feitas juntamente
    com suas respectivas suposições que o levaram a chegaram até ela.
  • Descreva todas alternativas consideradas, o porquê da sua escolha e motivo de a outra ser desconsiderada. Exemplos:

“Decidi usar ArrayList ao invés de Vector pelo(s) motivo(s) X, Y e Z”.

“Decidi usar a interface Runnabled ao invés de herdar diretamente da classe Thread pelo(s) motivo(s) X, Y e Z”.

“Decidi usar Socket na comunicação ao invés de RMI pelo(s) motivo(s) X, Y e Z”.

“Decidi usar RandowAcessFile para ler arquivos ao invés de um DataInputStream e DataOutputStream pelo(s) motivos X, Y e Z”.

Descreva todos os padrões de projetos que aconteceram e onde eles se encaixaram.

Seguem abaixo algumas dicas que podem ajudar a “abrir a cabeça” do
candidato, oferecendo direcionamento dentro das decisões arquiteturais
envolvidas na criação arquitetural do projeto:

Exceptions

  • Não faça as condições excepcionais com códigos de erros ou
    booleanos. A forma OOP correta que trata condições excepcionais é o
    lançamento de objetos exceptions com o objetivo de estabelecer
    baixo acoplamento e isolamento entre as camadas da solução.
  • Somente crie tipos proprietários de exceptions caso não exista
    alguma na API padrão que satisfaça sua necessidade. Construa suas
    classes exceptions de forma que propague a descrição do erro
    lançado nas condições excepcionais seguintes ao padrão da hierarquia
    da classe “java.langException”.
  • Não use tratamento de exceções para controle fluxos.
  • Faça o protótipo dos métodos declarando todos os possíveis
    tipos de exceção, e não somente a exceção pai. Esse é um dos raros
    momentos na OOP em que não se deve usar polimorfismo.
  • Nunca “engula” uma exceção nas instruções de tratamentos. Ou
    seja, nunca deixe uma instrução de “catch” em branco. Seguem algumas
    “regrinhas de bolo” sobre o assunto:
  1. Propague a determinada exceção ocorrida internamente dentro de outra
    elegida como padrão, fazendo com que o método jogue o erro para o
    chamador, notificando-o da condição de erro. Nunca exponha para fora de
    classe/camada detalhamentos de erros ocorridos internamente.
  2. Podem acontecer casos em que não faça sentido fazer a propagação de
    uma exceção como descrita na opção 1. Para esses casos, lance uma
    “Runtime Exception” da API padrão ou até uma proprietária notificando o
    chamador da condição de erro. Não se esqueça de documentar os motivos no
    JavaDoc.
  3. Podem acontecer casos em que não faça sentido fazer a propagação da
    exceção nem como a opção 1 e nem como a opção 2. Se, por alguma razão,
    isso for comprovado, não deixe a instrução “cach” em branco! Para esses
    casos, use algum mecanismo de “LOGGER”, gerando registros  para esses
    determinados casos. Não se esqueça de documentar os motivos no JavaDoc.

Cache

A grande dúvida está em volta da questão: “Devo ou não devo usar?”.
Para tomar uma decisão sensata, verifique se no seu documento de
requisitos existe algum tópico relacionado com performance. Leve em
conta as seguintes questões para decidir se realmente compensar optar
pela solução de cache:

  1. O sistema de cache realmente vai evitar o acesso ao mecanismo persistente? Quantas vezes e em quais situações?
  2. Esse cache vai requerer muita RAM da máquina?
  3. Qual o impacto no código usando o mecanismo de cache? Ou seja, o
    código pode se tornar complexo ao ponto de comprometer a clareza e
    a manutenção da solução?
  4. Vale realmente a pena o esforço comparado com os riscos?

Comunicação

A solução final entregue pelo candidato poderá ser executada
localmente ou em maquinas separadas. Isso quer dizer que a solução
deverá contemplar o conceito de aplicações distribuídas.

A péssima noticia é que o candidato deverá escolher somente entre
Socket ou RMI, que são as duas opções básicas de programações
distribuídas existentes na API JSE padrão. Tecnologias como Web
Services, CORBA ou EJB são proibidos pelos requisitos, forçando assim o
candidato a resolver e a implementar certas circunstâncias de projetos bem
interessantes, uma vez que ele não pode usar nenhum produto que resolva
isso. Baseado nessa situação da prova, seguem algumas dicas com objetivo
de nortear o candidato na sua decisão:

Socket

  • Difícil, demorado e bastante trabalhoso, uma vez que existe a
    necessidade de elaborar e de implementar manualmente o protocolo e a
    infra de comunicação juntamente com o controle de acesso
    multi-thread.
  • Em contrapartida ao volume de trabalho, a solução possui controle total e liberdade de gerenciamento das threads.
  • Melhor performance e escalabilidade principalmente se usado com
    a abordagem de pools de Thread que reduz em grande parte o
    overhead de criação de destruição de threads no controle das
    conexões dos clientes.
  • Fácil para ser configurando para trafegar no firewall, uma vez
    que a solução tem controle total das conexões dos sockets.

RMI

  • Fácil e muito rápido de usar, sendo que o RMI encapsula toda a automação:

Transparência: objetos remotos se comportam como se fossem locais. Todas as complexidades de comunicação de rede ficam escondidas como infra.

Protocolo: não existe necessidade de se idealizar e de se implementar um protocolo  de comunicação.

Acréscimo de serviço: fácil de acrescentar novos objetos remotos ou expor novos métodos de serviços.

  • Clientes de outras tecnologias podem acessar a camada RMI usando IIOP.
  • Baixa performance, porque dentro do RMI existe um overhead
    devido aos mecanismos de buscas, serviços de stubs e proxys que
    fazem a transparência da tecnologia.
  • A solução fica sem o controle das threads das conexões, pois o
    gerenciamento delas é encapsulado pelo RMI, não sendo
    thread-safety.
  • Bem mais difícil para ser configurando para trafegar no
    firewall, uma vez que o administrador tem que saber detalhes
    específicos da tecnologia.

Se o candidato decidir pelo RMI, seguem algumas dicas:

  • Não misture os detalhes de comunicação do RMI nas classes de
    serviços que serão expostas como objeto remoto. Implemente uma
    camada lógica de separação que forneça uma abstração entre o
    “serviço de comunicação” adotado e o “serviço de sistema” exposto
    pela solução.
  • Existe a possibilidade de contornar o problema do controle de
    threads aplicando o padrão Factory na criação dos objetos pelos
    usados pelas conexões RMI.

Bloqueio

Juntamente com execução distribuída, o candidato deverá implementar
um sistema de bloqueios que não permita a ocorrência de inconsistências
no acesso concorrente.

Gostaria de lembrar que esse é um dos pontos
mais difíceis da prova, no qual se pode ganhar ou perder a maior nota.
Com isso, seguem algumas dicas:

  • Existem apenas duas formas de se implementar bloqueios
    sincronização de objetos ou usando a API java.util.concurrent.
  • Sincronização de objetos é feita com a utilização de método ou
    de bloco de código synchronized para efetuar bloqueios, junto com o
    uso dos métodos wait(), notify() e notifyAll() da classe
    java.lang.Object e os métodos da classe java.lang.Thread para
    controlar as notificações. Essa opção é a mais fácil, rápida e
    intuitiva, mas com baixa performance, uma vez que o bloqueio é exclusivo
    tanto para leitura e como para escrita.
  • A API java.util.concurrent possui classes de controle de
    semáforos e notificações que podem ser usados no lugar de blocos de
    código synchronized e métodos da Objetc e Thread. Essa opção é a
    mais difícil e demorada e complexa, mas com alta performance, uma
    vez que existe a possibilidade de implementar bloqueios de
    “leitura” concorrente e de “escrita” exclusivo.

Em minha opinião, o candidato tem que evitar qualquer complicação e
sempre preferir soluções simples. Sendo assim, a sincronização
de objetos pode ser considerada na maioria dos casos. Somente considere
a API concurrent se caso existir em seus requisitos algumas questões de
perfomance que realmente a justifique.

Dentro do assunto de bloqueios, existe a questão de Dead-Lock
a ser considerada. Ou seja, o candidato terá que tomar alguma posição em
relação a essa possibilidade em seu projeto. Por isso, seguem algumas
dicas:

  • O candidato tem dois posicionamentos a tomar em relação ao Dead-Lock -  tratar ou não tratar.
  1. O assunto pode ser completamente ignorado na solução se caso o
    documento de requisitos não mencionar diretamente ou indiretamente a
    questão. Eu conheço algumas pessoas que passaram bem na prova simplesmente
    escrevendo no documento de arquitetura que não fizeram nada sobre
    Dead-Lock, porque os requisitos não os mencionam.
  2. Implementar uma solução que resolva o problema. Para esses casos, seguem minhas dicas:
  • Evite o Dead-Lock não implementando bloqueios compostos. Ou seja, não bloqueie mais que um recurso por vez.
  • Se a solução realmente precisar bloquear mais de um recurso por
    vez, infelizmente ela está sujeita ao Dead-Lock. Nesses casos,
    seguem algumas opções de soluções:
  1. Lock-Time-Out -  implemente um mecanismo que
    controle um tempo máximo de bloqueio, liberando quando esse tempo
    expirar. Opção simples, fácil e razoável.
  2. Check-Lock – implemente um mecanismo que verifique a
    disponibilidade de todos os recursos antes de bloquear qualquer coisa.
    Opção intermediária.
  3. Thread-Tracking – implementar mecanismos avançados
    para checar a ocorrência de dead-lock através de um “tracking” dos
    bloqueios ocorridos dentro do sistema. Opção difícil, demorada e
    complexa. Pode comprometer a clareza do código.

Continuando no assunto de bloqueios, o candidato terá que tomar
alguma posição em relação ao travamento ou à morte das conexões clientes.
A situação ocorre quando as conexões remotas são bruscamente desconectadas
devido a qualquer situação depois que houve um bloqueio de registro na
aplicação. Seguem as opções de solução:

  1. Fazer as chamadas de bloqueio e de desbloqueio dentro dos métodos de
    serviço na camada servidor, removendo-os da camada GUI. Isso evita o
    acontecimento.
  2. No uso de Socket, pode-se usar a exceção lançada pela conexão quando a
    camada GUI é desconectada da camada servidora, transformando-se assim
    em um ponto de chave para liberar os recursos quer foram bloqueados.
  3. No uso de RMI, pode-se usar o padrão Factory para identificar as
    chamadas das diferentes camadas GUI para armazenar os registros
    bloqueados que serão automaticamente coletados pelo GC quando o cliente
    desconectar.
  4. Outra opção de uso RMI é usar a interface java.rmi.Unreferenced como “listener” para a perda de conexão do a camada GUI.
  5. Ignorar completamente o caso, uma vez que ele pode nunca acontecer. A
    implementação desse mecanismo pode aumentar a complexidade do seu
    código a ponto de se perder pontos na clareza e na manutenibilidade do
    código.

Princípios de GUI

O candidato deverá implementar algumas interfaces gráficas para
usuário final com o objetivo de fornecer algumas operações. Seguem
algumas dicas:

  • A responsabilidade de uma GUI é fazer com que as tarefas do
    usuário sejam as mais fáceis possíveis. Por isso, planeje as
    interfaces gráficas de modo que o usuário tenha que fazer o
    menor número de passos para realizar seu objetivo.
  • Toda GUI deve organizar o seu layout de forma que a
    distribuição das informações seja clara e concisa, balanceada e bem
    organizada, direcionando corretamente a sequência de fluxo de
    eventos das tarefas.
  • Toda GUI deve ser clara a ponto de não fazer o usuário final ter que tentar adivinhar as coisas.
  • Toda GUI deve possibilitar o usuário final a fazer uma ação de
    múltiplas formas diferentes, usando um botão, um menu ou uma tecla
    de atalho. Isso é considerado um ótimo design de GUI, sendo que o
    usuário final irá se adaptar na ação que melhor lhe caiba.
  • Cada funcionalidade oferecida deve possuir uma tecla de atalho
    mapeada correspondente, de forma que a GUI funcione
    independentemente da presença do mouse e da plataforma de execução. O
    usuário final precisa ter uma opção de navegar e de efetuar as
    operações via teclado.
  • Fluxo de execução da GUI é de cima para baixo, da esquerda para direita.
  • Faça uma lista de tarefas ou de ações que o usuário tem que fazer
    na determinada GUI e construa o protótipo inicial como rascunho no
    papel.
  • Ordene os componentes visuais que o usuário final deverá dar a
    primeira atenção inicialmente quando interagir com a GUI. Ou seja, a
    ordem dos componentes visuais sugere a sequência das ações
    operadas.
  • Agrupe os elementos que sugerem similaridade de funcionalidade.
    Ou seja, não coloque o botão de sair perto do gravar porque eles
    oferecem funcionalidades opostas! Isso é baseado no princípio de
    que o usuário pode fazer uma serie de erros acidentais na GUI. Um
    bom design de interface gráfica agrupa elementos com
    funcionalidades semelhantes, minimizando qualquer possibilidade de
    acidentes.
  • Coloque os componentes visuais em lugares padrões e imóveis. O
    usuário final se acostuma com localização, tamanho, cor e forma dos
    itens de uma GUI e subconscientemente acaba já sabendo usar as
    funcionalidades padrões repetidas que podem existir em varias
    interfaces semelhantes. Isso faz com que a GUI fique intuitiva,
    fácil e natural.
  • Nunca permita o sistema fazer uma operação drástica sem a
    confirmação prévia do usuário final, oferecendo a opção de cancelar
    o pedido.
  • É preferível implementar uma lógica que desabilite as funções
    de uma GUI impedindo o usuário final de cometer o erro do que
    implementar a lógica que emita mensagem de erros com validações.
    Isso fica bem mais fácil e intuitivo para o usuário.

Mensagens de GUI

As interfaces gráficas podem apresentar dois tipos de mensagens para o usuário final:

  1. Avisos e erros informando que está acontecendo ou que aconteceu algo errado.
  2. Feedback informando alguma informação sobre o resultado gerado pelo sistema.

Dentro desse contexto, seguem algumas dicas:

  • Use palavras curtas e positivas.
  • Use a voz ativa “Faça isso e tal”.
  • Não use jargões ou abreviações.
  • Quando o sistema estiver processando algo, mostre algum indicador de progresso.
  • Se o usuário fizer algum erro, é apropriado dar a ele algum tipo de mensagem ou barulho de aviso.

Testes de GUI

Para validar as interfaces gráficas, é interessante usar algum amigo
ou conhecido que não seja da área de TI e que nunca tenha usado a
aplicação. Faça-o operar em todas as GUI, observando suas dificuldades,
seus questionamentos e tempo de uso. Essa experiência te mostrará qual é o
grau de clareza, facilidade e intuição de suas GUI, podendo gerar base
para as melhorias necessárias.

Manual do Usuário Final

O candidato deverá criar o manual de usuário final descrevendo como
usar o determinado sistema requisitado pela prova. Seguem então algumas
dicas básicas:

  • Documentar os seguintes tópicos
  1. Como instalar cada parte da aplicação servidor e cliente.
  2. Como iniciar cada parte da aplicação servidor e cliente.
  3. Como fazer cada operação e quais são seus possíveis resultados.
  4. Coloque “print-screen” de todas as interfaces gráficas, pontuando cada explicação de operação.
  5. Coloque “print-screen” de todas as possíveis mensagens de erros, confirmações e sucesso durante as operações.
  6. Documente cada situação assumindo que o usuário final nunca teve
    contato o sistema. Tudo tem que ser simples, claro e consistente.

Depois de tudo feito, passe o manual para algum conhecido que não seja da
área, para que ele teste o sistema como todo usando o material. Veja o que ele
não consegue fazer e procure especificar melhor no manual.

Qual a sua opinião?