quinta-feira, 7 de agosto de 2008

Análise e Crítica: "Inversion of Control Containers and the Dependency Injection Pattern"

O fraco acoplamento é um dos elementos mais cruciais no desenvolvimento de software orientado a objetos, pois este permite que a mudança em um objeto não exija mudanças em outros objetos. Este problema ocorre quando a mudança de implementação em um objeto "A" força mudanças em um objeto "B", onde normalmente o objeto "A" é o responsável pela criação do objeto "B" (forte acoplamento). Assim, a manutenção do software, reúso, escalabilidade e testes passam a ser tarefas difíceis de serem concretizadas. Além disso, localizar serviços independente da implementação é importante para cumprir com o princípio supracitado.

Neste contexto, Fowler[1] apresenta como os padrões "Dependency Injection" (ou Inversão de Controle) e "Service Locator" podem desacoplar objetos fortemente relacionados e corrigir independências entre objetos. No primeiro caso, é feita a utilização dos princípios de orientação a objetos tais como interface, herança e polimorfismo. A utilização de construtores, interfaces (menos utilizada) ou métodos set(s) permitem "injetar" - através do "assembler" - um objeto "B" à um objeto "A" que precise usufruir de serviços disponibilizados pelo objeto "B". Desta forma, o objeto "A" não precisa mais ter conhecimento dos atributos necessários para a criação do objeto "B", permitindo baixo acoplamento. No segundo caso, a idéia básica é ter um objeto (localizador) responsável por registrar serviços e associá-lo à uma chave. Um objeto "A" deve apenas recuperar uma instância do objeto "B" através deste localizador de serviços, passando a chave (normalmente em string) que esta instância está associada.

Fowler também discute quando utilizar as abordagens apresentadas. Ambas as abordagens propõem o baixo acoplamento, porém, a grande diferença entre elas está em como a implementação do objeto é disponibilizada ao objeto que deseja utilizá-lo. Por um lado, ao utilizar "Service Locator", uma requisição explícita ao responsável pela localização do serviço deve ser realizada, mantendo uma dependência com este objeto localizador. Por outro lado, na inversão de controle, não é realizada nenhuma requisição, visto que um objeto "A" recebe uma referência de um objeto "B" diretamente de construtores ou métodos set. Normalmente é mais difícil de entender e leva a problemas relacionados a debug. Desta forma, a escolha por qual das abordagens reflete na observação de onde a dependência se encontra.

Finalmente, os padrões permitem um melhor design, facilitando o reúso, o fraco acoplamento, e testes de componentes de software. Entretanto, acho que (1) a utilização de métodos get e set expõem os atributos de um objeto, violando o encapsulamento. Além disso, (2) acho que a utilização de construtores para injetar objetos pode tornar a inicialização de um objeto bastante confusa devido ao número de parâmetros que devem ser referenciados, apesar de "aliviar" o número de métodos set na classe. Entretanto, acho as abordagens plausíveis no que tange o fraco acoplamento entre objetos e a localização de serviços independentemente da implementação do mesmo.

Thiago Sales

[1] Martin Fowler. Inversion of control containers and the dependecy injection pattern. www.martinfowler.com, Janeiro 2004.

quarta-feira, 6 de agosto de 2008

Análise e Crítica: "Software and the Concurrency Revolution'' e ``The Problem with Threads

A programação concorrente é uma dos pontos chaves a qual faculta o paralelismo de tarefas e, em tese, possibilita o aumento na performance de uma aplicação. Entretanto, as atuais soluções para programação paralela traz ao programador problemas sérios às suas aplicações. Threads e sincronização de blocos, apesar de dominar as abordagens para programação concorrente, tendem a ser na maioria das vezes bastante confuso e de grande risco devido ao não determinismo associado. Além disso, o uso de "locks" para acesso concorrente a dados aumenta as chances de ocorrência de deadlocks, além de exigir que programadores sigam convenções rígidas para garantir tal tarefa.

Neste domínio, Sutter[1] e Lee[2] discutem os problemas associados e os desafios para alcançar aplicações concorrentes mais eficientes e sem gargalos. O foco está nas implicações da concorrência para os softwares e suas consequências para os desenvolvedores, linguagens e ferramentas de programação, além do estudo profundo das threads e suas alternativas. Uma das soluções propostas é a utilização de lock-free programming para permitir que múltiplas threads leiam e escrevam em dados compartilhados sem a preocupação de inconsistências. Entretanto, exige que o programador possua conhecimento profundo do modelo de memória utilizado, além de ser de difícil implementação. Sutter também considera que a programação paralela exige melhores ferramentas para sistematicamente dar suporte a descoberta de erros, debug, descoberta de gargalos e auxílio nos processos de testes das aplicações. Sem essas ferramentas, a concorrência será um fator que poderá reduzir a produtividade do desenvolvedor e do responsável pelos testes, tornando os sistemas concorrentes mais caros e de baixa qualidade. Em contrapartida, Lee enfatiza as diversas abordagens para corrigir problemas de concorrências em threads através da enorme eliminação do comportamento não determinístico, sendo um melhor processo de engenharia de software um dos fatores para suprir esta deficiência. Uma das soluções citadas é o projeto Ptolemy II Kernel, um ambiente de modelagem com suporte a modelos computacionais concorrentes. Lee também propõe outras alternativas à threads. Entre elas, destaca-se o método Rendezvous Director o qual provou-se eliminar possíveis deadlocks.

Por fim, ambos os autores concordam que o não determinismo deve ser cuidadosamente empregado e determinar onde de fato este precisa ser aplicado. Devido a esta característica intrísica das threads, acho que as soluções propostas e discutidas nos papers são de fundamental relevância no que tange alcançar eficientes técnicas de paralelismo e ferramentas de auxílio em tarefas para programação concorrente. Além disso, acredito que as tentativas das atuais linguagens de programação em incorporar técnicas para dar suporte à aplicações multithreads não conseguiram evoluir de forma plausível para acompanhar o crescimento das tecnologias de hardware.

Thiago Sales

[1] Herb Sutter and James Larus. Software and the concurrency revolution. ACM Queue, 3:54–62, 2005
[2] Edward A. Lee. The problem with threads. Computer IEEE, 39:33–42, 2005.

terça-feira, 5 de agosto de 2008

Análise e Crítica: Double-Checked Locking: Clever, but Broken

Um dos fatores primordiais em sistemas computacionais está associado a performance de uma aplicação, a qual pode ser atingida através do uso de paralelismo de tarefas. Entretanto, se não realizado um gerenciamento eficiente dos dados compartilhados entre as diferentes linhas de execução, a performance desejada pode ser significativamente afetada além de facultar o acesso a dados inconsistentes por essas tarefas. Uma das abordagens utilizadas por programadores Java é o uso da sintaxe synchronized, a qual objetiva o acesso controlado a pontos críticos entre as diferentes threads executantes. Entretanto, segundo Goetz[5], ao sincronizar um método Java através desta sintaxe, a performance de uma aplicação pode ser afetada consideravelmente em certos domínios. Isto pode ser ainda mais crítico quando esta sincronização deve ser aplicada ao padrão de projeto Singleton. A técnica Double-checked locking (DCL) possibilita "aliviar" o gargalo no acesso ao método sincronizado, provendo Lazy Inicialization (ou inicialização tardia) sem o uso de blocos sincronizados. Porém, devido a mecanismos utilizados pela JMM (Java Memory Model) para permitir melhor performance em tempo de execução, tal como a reordenação das instruções a serem executadas, a criação de uma única referência a um objeto pode ser drasticamente afetada.

Desta forma, diversos pesquisadores têm estudado e proposto algumas "artimanhas" como técnicas para evitar que o gerenciamento da JMM não efete o algorítimo desejado. Em [1], a construção do objeto a ser criado é colocado em um bloco (extra) synchronized interno. Ou seja, no momento em que a sincronização (interna) é finalizada, deve-se existir uma "barreira de memória", evitando assim que a JMM reordene as instruções para inicialização do objeto. Entretanto, ainda sim é possível que a reordenação aconteça devido a não existência de regras para impedir que a execução de instruções que estão após o bloco sincronizado seja antes de finalizar o "monitor". Uma outra solução, proposta por Goetz [3], utiliza uma variável temporária para garantir a construção total do objeto e posteriormente atribuída à referência original. Porém, ainda sim a JMM está livre para realizar otimizações nesta variável temporária. Goetz ainda propõe outra abordagem, utilizando uma variável booleana cujo valor depende diretamente da existência na memória do objeto a ser criado e posicionada fora do bloco synchronized. Porém, o compilador ou a JVM (Java Virtual Machine) pode reordenar tal instrução e adicioná-la ao bloco sincronizado para reduzir problemas associados a cache. Ou seja, o valor desta variável pode ser atribuída antes da construção do objeto. Em [4], Goetz discute a solução deste problema através da classe ThreadLocal. Em vez de verificar se o objeto possui referência nula, um objeto ThreadLocal (estático) armazena um valor (booleano, por exemplo) para garantir afirmar que uma outra thread já inicializou o objeto desejado. Por não compartilhar este valor com outras threads (pois este valor é local), problemas com reordenação de instruções não efetará a solução. Apesar de alcançar o objeto desejado pelo DCL, a performance da ThreadLocal é considerada pior que o custo existente em blocos sincronizados.

Apesar de existirem soluções plausíveis para alcançar o objetivo do Double-Checked Locking, a maior parte destas não funcionam ou possuem baixa performance/otimização. Acho que em qualquer domínio de aplicações que necessitem de alta performance sempre haverá trade-offs, ficando a cargo dos programadores decidir cuidadosamente quando um código deve ou não ser otimizado e, neste processo, se de fato haverá ganho considerável.

Ainda segundo Goetz, métodos sincronizados podem ser até 100 vezes mais lento do que métodos não-sincronizados. Para colocar em prova tal afirmação,
realizei alguns experimentos utilizando uma máquina com as seguintes configurações:

  • Processador Pentium-M, 1.86
  • 1GB Memória RAM

Utilizei um método estatístico intitulado de Distribuição Z [2]. Foram realizados 10 experimentos para cada tipo de método (sincronizado e não sincronizado), e alcancei os seguintes resultados com médias para 95% de nível de confiança:

  • Métodos sincronizados: 32071.90 milissegundos. Intervalo de confiança: entre 31743.89 e 32399.91 milissegundos.
  • Métodos não-sincronizados: 9691.00 milissegundos. Intervalo de confiança: entre 9419.32 e 9962.68 milissegundos.
Para ambos os casos, isso significa dizer que para "n" vezes que for executado esse experimento para o mesmo cenário das execuções (ou seja, com uma máquina igual ou similar as configuraçoes supracitadas), a média estará dentro do intervalo de confiança especificado. Desta forma, podemos concluir que o método sincronizado atribui maior tempo (apenas 4 vezes mais) de execução à aplicação do que o método não sincronizado.

Thiago Sales

[1] David Bacon, Joshua Bloch, Jeff Bogda, and Cliff Click. The ”double-checked locking is broken”declaration. http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html, Fevereiro 2001. Último acesso em 2 de Maio de 2008.

[2] Fisher. On a distribution yielding the error functions of several well known
statistics. Proceedings of the International Congress of Mathematics, 2:805–
813, 1924

[3] Brian Goetz. Can double-checked locking be fixed?
http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html, Maio 2001. Último acesso em 2 de Maio de 2008.

[4] Brian Goetz. Can threadlocal solve the double-checked locking problem?
http://www.javaworld.com/javaworld/jw-11-2001/jw-1116-dcl.html,
Novembro 2001. Último acesso em 2 de Maio de 2008.

[5] Brian Goetz. Double-checked locking: Clever, but broken.
http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html, Fevereiro 2001. Último acesso em 2 de Maio de 2008.

Análise e Crítica: A Pattern Language for Developing Object-Oriented Frameworks

No domínio da Orientação a Objetos (OO), um framework é um conjunto de classes que visa a reutilização de um design de software, oferecendo soluções padrões para aplicações de um mesmo domínio (framework vertical). Frameworks possibilitam também o aumento na produtividade e a diminuição no custo de desenvolvimento das aplicações. Entretanto, especificar um design para framework que ofereça também facilidade de aprendizagem tende a ser bastante complexo devido aos detalhes intrísecos no paradigma de Orientação a Objetos.

Neste contexto, [1] propõe um modelo para a implementação de frameworks. Intitulado de "Evolving Frameworks", este modelo é constituído de um conjunto de padrões que devem ser utilizados conjuntamente de forma a alcançar um design bem definido. Dentre eles, destacam-se: White-Box Framework, o qual propõe a utilização de herança ao invés de polimorfismos aliado ao uso dos padrões de projeto Template Method e Factory Method, aumentando a reusabilidade de código; Pluggable Objects onde ao observar subclasses com comportamentos semelhantes, propõe-se utilizar subclasses genéricas, possibilitando o reúso sem o conhecimento a priori do objeto; Fine-Grained Objects, o qual deve ser utilizado no estágio de refatoração do código, dividindo o objeto que possui múltiplos comportamentos em pequenos objetos com comportamentos peculiares.

Por fim, acho que a solução do paper é plausível no que tange o desenvolvimento de um bom framework, ao começar provando que, se ao menos três aplicações desenvolvidas utilizam algo em comun, a criação do framework desejado poderá ser necessária e, a partir deste ponto, desenvolver bibliotecas de componentes e refatorações de códigos. Entretanto, o paper apenas restringe a proposta a frameworks verticais, ou seja, aqueles que são dependentes do domínio da aplicação, o que torna o paper ainda mais restrito. Apesar de oferecer boas soluções, o paper mostrou-se pobre no que se refere a exemplos associados com os problemas apresentados.

[1] Don Roberts and Ralph E. Johnson. Evolving frameworks: A pattern language
for developing object-oriented frameworks. In Pattern Languages
of Program Design 3. Addison Wesley, 1997.

segunda-feira, 4 de agosto de 2008

Análise e crítica: Architectural Blueprints - The "4+1'' View Model of Software Architecture

O desenvolvimento de um software exige aos projetistas (e toda equipe envolvida) modelos que facilitem a identificação e a compreensão dos principais componentes do sistema. Decompor requisitos funcionais - subdividindo-os em módulos -, estabelecer requisitos não-funcionais - identificando pontos críticos da aplicação, tais como performance e disponibilidade de serviços - e propor interfaces que ofereçam fácil usabilidade são tarefas complexas no que tange alcançar um sistema bem documentado e arquiteturalmente bem definido, o que tem trazido ao mundo da engenharia de software diversas abordagens visando facilitar a construção de modelos destes componentes. Entretanto, em alguns casos tais modelos em vez de oferecer clareza, são bastante confusos ao especificar qual o seu objetivo no sistema proposto.

Neste contexto, o paper Architectural Blueprints - The 4+1 View Model of Software Architecture[1] apresenta um modelo para descrição de arquiteturas de softwares baseado no uso de múltiplas visões concorrentes, permitindo subdividir e atribuir as diferentes responsabilidades aos demais participantes do "time" (usuário final, desenvolvedores, engenheiros, projetistas, etc) além de facilitar na decomposição dos requisitos funcionais e não-funcionais de uma aplicação. Intitulado de "4+1 View Model" este modelo descreve a arquitetura de um software fazendo uso de cinco visões concorrentes: A (1) visão lógica descreve o design de um objeto, a (2) visão de processos descreve os aspectos inerentes a concorrência e sincronização, a (3) visão física descreve o mapeamento do software para o hardware e apresenta os aspectos distribuídos do software em questão, e a (4) visão de desenvolvimento descrevendo a organização estática no ambiente de desenvolvimento. Arquitetos de software podem organizar a descrição de suas decisões arquiteturais utilizando estes quatro tipos de visões e então utilizar-se de "casos de uso" para representar os requisitos funcionais (sendo esta a quinta visão). Kruckten afirma que a importância dos "casos de uso" está na validação e ilustração dos requisitos após finalizar o design da arquitetura do software. Esta proposta de modelagem permite que os participantes supracitados encontrem o que eles precisam em uma arquitetura de software. Ou seja, os engenheiros da aplicação podem utilizar esta abordagem para a visão física e a visão de processos; usuários finais, clientes, focam a visão lógica; e os gerentes de projeto podem focar a visão de desenvolvimento.

Kruckten também deixa claro que estas visões não são independentes. Ou seja, elementos de uma visão podem estar interconectados com elementos de uma outra visão, obedecendo certas regras de design e heurísticas. Como exemplo, pode-se considerar a relação entre a visão lógica e a visão de processos. No primeiro caso, diversas características de uma classe podem ser observadas, tais como a (1) autonomia de um objeto, identificando se este é ativo, passivo ou protegido; (2) se este será armazenado ou não, entre outras. Por outro lado, na segunda visão supracitada, deve-se implementar cada objeto com sua própria thread de controle. Assim, o resultado é o mapeamento das classes e seus objetos (da primeira visão) em um conjunto de tarefas e processos (da segunda visão). É importante salientar também que o uso das visões depende diretamente da complexidade (ou da necessidade) da aplicação em questão. Por exemplo, se o sistema não for multiprocessado, não há necessidade de utilizar a visão física.

Em fim, acho que o paper aborda um assunto bastante importante no que tange alcançar uma boa arquitetura de software. A proposta de Kruckten é bem centrada e plausível e acredito que a separação dos modelos inerentes aos diferentes requisitos ajuda a cada "stakeholder" a focar na sua responsabilidade, sem ter que entender e criar modelos que não são de seu interesse, minimizando tempo de desenvolvimento (consequentemente diminuindo custos) e facilitando a manutenbilidade da documentação do sistema proposto, a qual é um dos problemas graves existentes no desenvolvimento de um software (documentação incompleta e/ou inconsistente).

Thiago Sales

[1] Philippe Kruchten. Architectural blueprints—The “4+1” View Model of Software Architecture. IEEE Software, 12(6):42–50, November 1995.

O que é Projeto de Software?

Contextualizar “Projeto de Software” requer compreender diferentes propostas apresentadas pela comunidade científica. Assim, de forma sintetizada, entende-se aqui por “Projeto de Software” como um processo de planejamento e resolução de problemas para a construção de um software. Ou seja, foca o concepção e o desenvolvimento de um software, onde desenvolvedores de softwares devem colaborativamente contribuir com artefatos (requisitos, documentação, etc) de maneira organizada, seguindo as regras definidas por um processo de desenvolvimento de software.

Segundo [1], o “Projeto de Software” ocorre quando os requisitos do software
são obtidos e o software é desenvolvido e testado. Ainda salienta que, uma das principais falhas no desenvolvimento de um software está relacionada coma inabilidade de projetistas lidar corretamente com clientes e suas necessidades de mudanças. Além disso, projetistas de software possuem dificuldades de analisar seus projetos e demonstrar que estes estão corretos e que representam os requisitos adquiridos. De fato, os problemas supracitados podem ser parcialmente “corrigidos” com novos processos de desenvolvimento de software, tais como Desenvolvimento de Software Orientado a Aspectos (AOSD) e Desenvolvimento Orientado a Modelos (Model-Driven Development - MDD). No primeiro caso, existe um ganho no poder de expressar requisitos não-funcionais. Já no segundo, projetistas passam a ter uma abstração maior das funcionalidades do software provendo maneiras automatizadas para zeração do código.

Reeves [2] tenta propor que o projeto de software apenas ocorre no processo de codificação do software. Ou seja, o código-fonte do software é, de fato, o projeto de software. Gerou-se muitas polêmicas em torno desta tentativa de definir “Projeto de Software”, pois, se o código-fonte é o “projeto”, entãodesenvolvedores são projetistas (o que não é verdade). Ou seja, o código-fonte não pode ser o projeto de software.

Apesar das controvérsias na definição de “Projeto de Software”, pode-se observar que projetar um software requer definir de forma consistente os processos do ciclo de desenvolvimento de software (an´alise, projeto, codificação, etc), gerando e atualizando artefatos em cada etapa do desenvolvimento e organizando as interações entre os envolvidos.

Thiago Sales

[1] Kruchten, P. 2005. Software Design in a Postmodern Era. IEEE Software.
22, 2 (Mar. 2005), 16-18.

[2] Reeves, J. W. 1992. What is Software Design. The C++ Journal. No. 2
Vol. 2.