O que é design de software?

Nós desenvolvedores, artesãos de software ou simplesmente programadores, vivemos em uma jornada incessante em busca do aprimoramento em nosso ofício.

Afora o estudo de novas tecnologias, novos processos e melhores algoritmos para resolvermos os problemas de forma mais eficiente, uma das habilidades mais importantes a ser buscada por um programador é a de se fazer um bom design.

A existência de um bom design é independente de tecnologia ou problema, e é a razão por trás de todos os princípios de programação que se encontra por aí.

Mas o que é design?

Por ser uma palavra da língua inglesa vejamos primeiro sua definição nessa língua:


Merriam-webster:
– the arrangement of elements or details in a product or work of art
– deliberate purposive planning
– a preliminary sketch or outline showing the main features of something to be executed

Em português, design é geralmente traduzido como:


– projeto, plano, desenho

Vemos que o termo design está intimamente associado com o planejamento de uma atividade que, em geral, dará origem a algum artefato como resultado. Uma atividade de construção, digamos assim.

Dessa forma, antes de iniciar a execução da construção do artefato, realiza-se um plano que servirá como um guia nessa tarefa, delineando a forma, organização e detalhes dos elementos que compõem tal objeto.

Mas design é só isso?

De fato podemos ir além da noção de planejamento e pensar em design como algo mais concreto: desenho, forma, disposição de elementos, organização – como usualmente usa-se o termo nas artes plásticas.

Quando você ouve uma música, contempla uma pintura ou lê um poema, você pode perceber claramente sua forma: a escolha, disposição e interação entre elementos que compõem aquela obra e a caracterizam.

Então, após a execução de uma obra – e aqui englobamos qualquer ofício – pode-se dizer que o design fica concretizado, impresso no artefato criado, tornando-se parte dele, parte de sua história.

Portanto, podemos afirmar que design é a organização de elementos que ocorre tanto antes, durante ou depois da existência do artefato, e é um processo inevitável.

Por que se pensa em design?

Se o processo de design é inevitável, então posso simplesmente esquecê-lo, executar e parar de me preocupar com isso?

Claro que não. O design deve ser sempre cuidadosamente pensado, consciente e acima de tudo alinhado com o papel que ele desempenha na atividade. Caso isso não aconteça, o resultado será um artefato com um design ruim, onde bom e ruim são conceitos relativos ao contexto em que ele existe.

Precisamos ter em mente que para cada tipo de atividade o design tem um propósito diferente e é com base nesse propósito que classificamos um design como bom ou ruim. Alguns exemplos:

Na arte o objetivo do design é a estética.
Na construção civil o objetivo do design é dimensionar estruturas, mitigar riscos, reduzir custos.
No front-end web, o objetivo é a estética e a usabilidade.

O propósito do design no software

No desenvolvimento de software o objetivo do design é facilitar a mudança.

Danilo Sato, da Thoughtworks, em uma palestra explica que para ele Design é Código, e ele está certíssimo, porque o código é o artefato do programador, e o design está ali, concretizado no código. Cada decisão tomada antes e durante seu desenvolvimento será refletida no código.

Mas indo um pouco além, o design também é a atividade que ocorreu antes da existência do código, é também o diagrama de classes desenhado no quadro branco; design também é planejamento.

Beethoven fazendo Big-Upfront-Design

Beethoven fazendo um Big Design Upfront

Como na analogia com a música feita por Rich Hickey em uma palestra inspiradora, mais decisões de design podem ser tomadas antes da execução, em diagramas UML por exemplo, analogamente a um compositor que escreve uma sinfonia; ou mais decisões de design podem ser adiadas, basicamente deixando o programador livre para tomá-las durante a execução, como um jazzista improvisando sobre uma sequência de acordes apenas.

No mundo do software, como o objetivo do design é facilitar a mudança, e o rítmo das mudanças é alucinante, uma tendência é jogar o máximo das decisões para frente, até o ponto que chegamos no Design Emergente. Dessa forma, só inserimos complexidade no design do nosso sistema quando essa complexidade for de fato necessária, seguindo o bom e velho princípio do YAGNI.

Estudar Design de Software é essencial

A diferença crucial entre um grande design planejado – como uma composição orquestral, e um design ágil – como uma improvisação de Jazz, é que para esse último caso é obrigatório que o executor, seja ele o músico ou o programador, tenha um grande conhecimento sobre como fazer um bom design.

No caso ágil, as decisões de design são tomadas na hora, pelo executor, baseado no contexto daquele exato momento, com as informações mais atuais possíveis e com o feedback imediato, por isso é preciso que o executor tenha interiorizado os princípios que regem um bom design.

O “artefato software” não é tão efêmero quanto uma improvisação de uma música, que só acontece uma vez, nem tão imutável quanto uma estátua de mármore, ele vive em algum lugar no meio desse espectro. Por ter essa natureza mais “flexível”, temos a possibilidade de remodelá-lo constantemente com mais facilidade, criando o design mais apropriado durante a construção, e adaptando-o a cada iteração, através da refatoração.

Portanto, como um artesão de software, interiorize a “harmonia”, pratique as “escalas”, estude os melhores “solos” e, acima de tudo, nunca se esqueça do propósito do design que você está criando.

Miles Davis fazendo design emergente

Miles Davis fazendo design emergente

Anúncios

Type-safe null usando Option

Reconhecendo o erro bilionário

Em 1965, Sir Charles Antony Richard Hoare (Tony Hoare para os chegados), inventava o null, as referências nulas, enquanto projetava a linguagem orientada a objetos ALGOL W.

Muitos anos depois, em 2009 ele mesmo retoma esse fato em uma palestra:

I call it my billion-dollar mistake.

Ele chama o null de seu erro bilionário! Fonte de inúmero erros, vulnerabilidades e problemas em sistemas no mundo todo. De fato uma grande cagada!

O Problema e sua Consequências

Certamente o null é tratado com muita cautela por nós programadores quando escrevemos nosso código. Sendo profissionais cautelosos, sempre pensamos que uma ou outra referência pode estar nula, desconfiados de toda e qualquer variável.

O programador Java incauto, faria coisas do tipo:

Animal elefante = animais.get("elefante"); 
elefante.anda(); // Pode dar merda
Pessoa p = PessoaDAO.findByName("Felipe");
Documento d = p.getDocumento(); // Vai dar merda
d.getNumero(); // Já deu merda

Já o programador cauteloso e experiente não se arriscaria a tomar um NullPointerException na cara:

Animal elefante = animais.get("elefante");
if(elefante != null)
  elefante.anda();  // Agora é seguro
Pessoa p = PessoaDAO.findByName("Felipe");
if(p != null) {
  Documento d = p.getDocumento();
  if(d != null) { 
    d.getNumero();  
  }
}

Ser desconfiado dá trabalho, e deixa nosso código cheio de if-guards, que são feios e chatos de ler.

Claro que, com um bom design, é possível diminuir a quantidade de ifs, mas o grande problema é que eventualmente alguém vai esquecer do maldito if e um NullPointerException (ou Segmentation Fault) poderá explodir a qualquer momento em produção!

O problema é que o compilador não nos diz nada. Se fosse possível fazer o compilador trabalhar para gente, e pegar todos esses erros em compilação seria ótimo…

Evitando o erro

Muitas pessoas inteligentes perceberam o risco de se ficar manipulando nulls e inventaram maneiras para evitar seu uso.

Null Objects

Um exemplo é o pattern Null Object, que sugere a construção de objetos “sem comportamento” como uma alternativa.

O grande problema desses Null Objects é que se forem usado sem cuidado, podem introduzir bugs difíceis de se encontrar, pois eles de certa forma disfarçam o problema. Imagine: você tem um objeto na mão, chama alguns métodos sem nenhum problema só que na verdade esses métodos não estão fazendo nada.

Option

Um conceito muito interessante e bastante simples, trazido diretamente do mundo da programação funcional é o do tipo Option, também chamado de Maybe em algumas linguagens.

O Option é um tipo abstrato parametrizado (portanto Option[T]) que tem apenas dois filhos: Some[T] e None. De maneira muito simplificada, em Java seria algo do tipo:

public abstract class Option<T> {
	  public static <T> Option<T> of(final T conteudo) {
		if(conteudo == null) {
			return new None<T>();
		} else {
			return new Some<T>() {
		    	public T get() {
		    		return conteudo;
		    	}
		    };	
		}
	  }
}

abstract class Some<T> extends Option<T> {
  public abstract T get();
}

class None<T> extends Option<T> {}

Usamos o Option assim:

Option<String> some = Option.of("Tem coisa"); // devolve Some<String>
Option<String> none = Option.of(null); // devolve None

Portanto, sempre que uma função tem a possibilidade de devolver um valor inválido ou inexistente para determinado argumento, podemos embrulhar o resultado em um Option.

Por exemplo:

Option<Pessoa> pessoa = pessoaDAO.findById(1234L); 

Legal, dessa maneira pessoa nunca será null, mesmo que o registro 1234 não exista no banco de dados.

Só que quando eu quiser o conteúdo do Option, teria que fazer isso:

if(pessoa instanceof Some<?>) { 
  String nome = ((Some<Pessoa>) pessoa).get().getNome(); 
  System.out.println("Encontrei pessoa com nome: " +  nome); 
} else {
  System.out.println("Não encontrei ninguém");
}

Esse código, apesar de terrivelmente feio, é mais type-safe, já que o compilador nos força a fazer uma verificação antes de usar a instância de Pessoa.

Outra desvantagem dessa abordagem é que, além de ser desengonçado, é mais prático fazer um if(pessoa != null) . Por isso é difícil convencer um programador a usar isso. E agora?

Bom, em Scala e em outras linguagens mais funcionais temos Pattern Matching para melhorar as coisas:

Option<Pessoa> pessoa = pessoaDAO.findByPK(1234L); 
pessoa match {
  case Some(p) => println("Encontrei pessoa com nome: " + p.nome)
  case None => println("Não encontrei ninguém")
}

Ainda um pouco verboso, mas bem melhor!

Pattern Matching é ideal quando temos dois fluxos completamente diferentes dependendo se o Option é Some ou None. Para casos em que só se quer passar o valor para uma função, ou manipulá-lo de maneiras mais simples, podemos usar as construções apresentadas na próxima seção.

Indo um pouco além do óbvio

Option surgiu no mundo funcional, e é muito mais do que um simples container de coisas.

Na realidade, é possível manipular um Option de maneiras muito práticas e poderosas, especialmente em linguagens em que conseguimos passar funções como parâmetro. Essas construções geralmente são mais enxutas do que fazer Pattern Matching.

Tony Morris fez um post em seu blog mostrando algumas formas mais funcionais de se substituir diversos casos implementados com Pattern Matching, usando a própria API do Option.

Veja alguns exemplos em Scala:

Option<Pessoa> pessoa = pessoaDAO.findByPK(1234L);  // mesmo exemplo

// foreach
// Se for Some executa a função passada, se for None não faz nada.
pessoa.foreach(p => println(p.nome))

// map
// Se for Some executa a função e embrulha o retorno em um Option. Se for None devolve None.
val endereco:Option[Endereco] = pessoa.map(_.endereco) 

// getOrElse
// Se for Some devolve o conteúdo, se for None devolve o resultado do bloco passado como parâmetro
val p = pessoa.getOrElse(new Pessoa())

Note que muitos métodos de Option são os mesmos que existem nas Collection em Scala.
E pasmem, é possível usar Option em for-comprehensions, podendo até misturar listas com options.

val lista:List[Integer] = List(1,2,3)
val option:Option[Integer] = Some(1)

// Conhecemos for em listas
for( num <- lista) {
  println(num)
}

// Mas é possível usar Option
for(num <- option) {
  println(num)
}

Imagine que você quer navegar por uma estrutura de objetos cujos métodos retornam Option. É possível usar uma construção for sofisticada em Scala:

for { 
  pessoa <- pessoaDAO.findByPK(1234L)  
  endereco <- pessoa.getEndereco
  numero <- endereco.getNumero 
} {
  println("O número da casa da pessoa de ID 1234 é " + numero)
}

Se em alguma dessas linhas, o objeto for um None, o corpo do for não será executado. É como se um Option fosse uma lista de um único elemento.

Por fim…

Option é uma poderosa abstração para substituir o famigerado null, causa de tantos NullPointerExceptions inesperados.

Seu objetivo é tornar o código mais type-safe e, mesmo adicionando uma camada extra para alcançar isso, sua API engenhosa permite que o programador manipule os Option de maneiras muito eficientes, concisas e poderosas.

Quando estiver projetando uma API, considere o uso de Option em funções que podem ou não devolver um objeto. Use para tratar os casos de exceção, como por exemplo quando um registro não é encontrado no banco de dados, ou quando uma função não está definida para determinado argumento.

Design Patterns em Scala – Parte 1: The Observer

Todo programador moderno, hype, antenado, ligado nas tendências, curte mesmo um Design Pattern.

Aliás tudo que tem Design no nome fica chique né? “Design de Sombrancelhas”, “Hair Designer”. Mas enfim…

Design Patterns é de fato um assunto bastante interessante, e o conhecimento de alguns desses padrões de design pode facilitar bastante o desenho de soluções.

Nesse post não pretendo ficar explicando a fundo os Patterns mais conhecidos no mundo Java, tem muita coisa já escrita por aí.

Como o foco principal desse blog é a linguagem Scala, vamos dar uma olhada em como alguns Patterns manjados em Java ficam ainda mais interessantes em Scala. Em seguida, vamos ver alguns Patterns novos para Scala.

Singleton

object MySingleton {
 // Código vai aqui
}

Singleton foi elevado ao cargo de construção de linguagem! Usando a palavra-chave “object” estamos criando uma instância única de uma classe.

No exemplo acima estamos ao mesmo tempo declarando uma classe e instanciando um singleton para ela.

Um singleton pode naturalmente estender classes e implementar Traits.

// Nem preciso de corpo se eu não quiser
object MySingleton extends MyClass with MyTrait 

O ministério da saúde dos programadores adverte: Singleton faz mal aos testes. Use com consciência.

Observer

Como vocês já sabem, nesse padrão temos dois personagens principais: o Observer, e o Subject.

O Observer é o objeto que está interessado no que o Subject está fazendo, e o Subject é gentil em notificar os Observers quando algo nele muda.

Primeiro, vejamos o jeito mais conhecido, java-like:

trait Subject {
  private var observers: List[Observer] = Nil

  def addObserver(observer: Observer) = observers = observer :: observers

  def notifyObservers() = observers.foreach(_.receiveUpdate(this))
}

trait Observer {
  def receiveUpdate(s:Subject)
}

// Olha mãe, sou um Singleton
object MyObserver extends Observer {
  def receiveUpdate(s:Subject) {
    println("O subject mudou!")
  }
}

// Olha mãe, eu nem preciso de um corpo
object MySubject extends Subject

// Testa tudo isso aí
MySubject.addObserver(MyObserver)
MySubject.notifyObservers

Criamos uma Trait para o Subject e o Observer e um exemplar concreto para cada um apenas para ver funcionando.

Uma Trait é como se fosse uma interface Java que também pode ter campos concretos. Uma classe consegue “estender” múltiplas Traits.

Só que em Scala eu consigo deixar as coisas mais interessantes. Vamos dizer que você não queira uma interface para os Observers. Que tal se qualquer classe que tiver o método receiveUpdate(s:Subject) pudesse ser um Observer?

Podemos usar Structural Typing para isso, e jogar a Trait Observer fora:

// Subject agora aceita qualquer observer que se "encaixa" no type definido
trait Subject {
  // Adicionei esse Type Alias para dar um nome a esse tipo estrutural.
  type Observer = { def receiveUpdate(s:Subject) }

  private var observers: List[Observer] = Nil
  def addObserver(observer: Observer) = observers = observer :: observers
  def notifyObservers() = observers.foreach(_.receiveUpdate(this))
}

// Não estende nada
object MyObserver {
  def receiveUpdate(s:Subject) {
    println("O subject mudou!")
  }
}

// O resto é igual
object MySubject extends Subject

MySubject.addObserver(MyObserver)
MySubject.notifyObservers

Structural Typing é parecido com Duck Typing, se o objeto tiver aquele método, ele é chamado. Por baixo dos panos o compilador resolve tudo usando Reflection, e por isso talvez tenha um impacto na performance do código.

Agora, uma instância de qualquer, QUALQUER classe que possua o método receiveUpdates(s:Subject) pode se adicionado como um Observer.

Legal, mas podemos ir além! E se eu não quiser que meu Observer seja uma instância de uma classe? Que tal se ele fosse apenas uma função?

Sinta o poder:

trait Subject {
  // Type Alias novamente. O tipo Observer é qualquer função que recebe Subject como parâmetro e não devolve nada (Unit é tipo void).
  type Observer = (Subject => Unit)

  private var observers: List[Observer] = Nil

  def addObserver(observer: Observer) = observers = observer :: observers

  // Mudei aqui para executar a função ( .apply aplica uma função )
  def notifyObservers() = observers.foreach(_.apply(this)) 
}

// Olha mãe, sou uma função!
val updateHandler = (s:Subject) => println("O subject mudou!")

object MySubject extends Subject

// Testa
MySubject.addObserver(updateHandler)
MySubject.notifyObservers

Type Alias é uma construção que permite que você defina um tipo. Isso mesmo, você declara um tipo e dá um nome para ele. Por exemplo, você pode dar um nome diferente para o tipo String:
type MinhaString = String.
Isso é bastante útil com Structural Typing.

Eu sei, eu sei, foi uma viagem e tanto. Se você nunca tinha experimentado nada do mundo funcional, esse aqui foi um pequeno exemplo.

Isso aí, vimos como em Scala podemos escrever o Observer de maneiras diferentes, tornando-o mais flexível.

Se o seu Observer não puder por algum motivo implementar uma interface, ou estender uma classe específica, podemos usar Structural Typing!

Se você não quer criar uma classe só para ser um Observer, e quiser só passar uma função de callback para seu Subject, agora você também pode de maneira simples e concisa. (em Java até daria para fazer uma classe anônima, mas convenhamos que assim é 3430943498 vezes mais bonito).

No próximo capítulo!

Arquitetura e Decoração com o Decorator

E não, não estamos falando sobre jardim, sofás, paredes e decoração de interiores…