Arquitetura de Software

Padrões de projeto: o que são e o que resolvem

Design Patterns (comumente relacionados na literatura de TI como “Padrões de Projeto”) são soluções para problemas comuns que encontramos no desenvolvimento ou manutenção de um software orientado a objetos (não são tão bem aplicáveis em outros paradigmas). Olhando assim parece ser algo realmente bem “requintado”, não é?

Vamos tentar entender sob outra perspectiva: imagine você no futuro, daqui 15 anos, certamente terá passado por N problemas, repetidamente e em diferentes projetos. A tendência natural é que você crie soluções comuns (mas elegantes, assim esperamos, né? :P) para resolvê-los de tal forma que você consiga aplicá-las no futuro em outros projetos. Essas soluções, criadas por você, mesmo que não compartilhadas à literatura (livros ou artigos acadêmicos), podem ser consideradas como sendo Design Patterns, sim, padrões de projeto criados por você para resolver os seus problemas. Nada mais justo, não?

Sorte do dia: Não precisamos esperar uma dezena ou mais de anos para acumularmos experiência (e, consequentemente, perda de cabelo) para que tenhamos desenvolvido eficientes soluções para os nossos softwares.

Isaac Newton certa vez dissera:

Se cheguei até aqui foi porque me apoiei no ombro de gigantes.

Lhe convido a absorver essa ideia que nos foi “presenteada” séculos atrás por esse brilhante cientista (alguns diriam que ele foi um “full stack” por ter sido físico, matemático, astrônomo, filósofo, teólogo, alquimista e coisas mais. :P).

Engenheiros de softwares por décadas desenvolveram padrões de projeto para resolver problemas comuns. Por que não dar uma chance de conhecê-los e, quem sabe, utilizá-los?

Parafraseando o filósofo e professor da USP, Clóvis de Barros Filho, em uma palestra muito especial:

Os caras escreveram as soluções, véio. Eles tiveram que tirar o negócio do zero! Só precisamos entender e aplicar a bagaça!

Ruby on Rails Intermediário
Curso de Ruby on Rails Intermediário
CONHEÇA O CURSO

Como tudo começou

Toda a inspiração por trás do nosso conceito de “padrões de projeto” veio, na realidade, da arquitetura (sim, àquela área de conhecimento que projeta e arquiteta ambientes), de um livro chamado “A Patter Language”, escrito por Christopher Alexander, Sara Ishikawa e Murray Silverstein, foi o que primeiramente lançou a ideia de “linguagem de padrões”. O livro teve como propósito apresentar centenas de padrões sobre como cidades, bairros, casas e ambientes no geral poderiam ser projetados.

O primeiro grande trabalho da área de desenvolvimento de software que absorveu tal ideia coletando e desenvolvendo dezenas de padrões para softwares se deu em 1994 em um livro chamado “Design Patterns: Elements of Reusable Object-Oriented Software”, escrito por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. Originalmente o livro discutiu 23 padrões de projetos. Os autores e o livro causaram tanto impacto que começaram a ser chamados e reconhecidos por Gang of Four (GoF) (gangue dos quatro) e tais padrões começaram a ser intitulados de GoF Patterns (Padrões GoF).

Olhando assim parece até que em uma noite chuvosa a “gangue dos quatro” reunida na garagem de um deles se acometeu de uma sobrenatural inspiração e então gritaram ==”Eureka! Eureka!”== e logo já escreveram os 23 padrões, não é? É, só parece. Os padrões, na realidade, são soluções de problemas retirados de ==códigos reais==.

Categorias dos padrões GoF

Os padrões GoF foram divididos e categorizados de acordo com a natureza do problema que eles resolvem (ou ao menos tentam, uma vez que software, como bem você sabe, é algo com “vida própria” haha):

Padrões de Criação: Tem como objetivo abstrair a instanciação de objetos. Com eles, o sistema vai solicitar um objeto de um determinado tipo e o terá prontinho, sob demanda, sem nem se preocupar com as nuances da criação. Fazendo um paralelo com o mundo real, uma empresa automobilística quando precisa de amortecedores, ela terceiriza (solicita-os) e então os instala em seus carros, sem se preocupar com o todo envolvido na criação desse componente.

Padrões estruturais: Os padrões dessa categoria se preocupam em melhor organizar a estrutura das classes e os relacionamentos entre classes e objetos.

Padrões comportamentais: Os padrões dessa categoria atuam diretamente na delegação de responsabilidades, definindo como os objetos devem se comportar e se comunicar.

Padrões GoF

Abaixo a relação dos padrões GoF.

Nome do padrãoCategoria
Abstract FactoryCriacional
BuilderCriacional
Factory MethodCriacional
PrototypeCriacional
SingletonCriacional
AdapterEstrutural
BridgeEstrutural
CompositeEstrutural
DecoratorEstrutural
FacadeEstrutural
FlyweightEstrutural
ProxyEstrutural
Chain of ResponsibilityComportamental
CommandComportamental
InterpreterComportamental
IteratorComportamental
MediatorComportamental
MementoComportamental
ObserverComportamental
StateComportamental
StrategyComportamental
Template MethodComportamental
VisitorComportamental

Concluindo

Os padrões GoF foram descritos há décadas e, software é quase como um organismo vivo, ele passa por transformações para se adaptar às novas realidades do “ambiente”.

Nem todos os padrões GoF são bem aceitos nas comunidades de desenvolvimento, por exemplo, você já deve ter ouvido de algum colega desenvolvedor que você deveria passar longe de usar um Singleton por ser um “anti pattern”. Não tenho como propósito aqui fazer julgamento de valor mas, nem tudo é “preto no branco”. Conheço grandes softwares open sources (Frameworks de linguagens bem estabelecidas) que usam Singleton em alguma parte de seus códigos e nem por isso deixaram de ser bons.

Abusar no uso de padrões de projeto pode nos levar a caminhos tortuosos. Na realidade, supervalorizar qualquer coisa nessa nossa complexa vida pode ser perigoso, pois tendemos a criar determinamos “vícios” nas resoluções dos nossos problemas. O bom senso é sempre fundamental. E no final, como sempre, nunca teremos todas as respostas. Nossos softwares nunca serão “perfeitos e impecáveis” e nunca estaremos em plenitude satisfeitos com o código que escrevemos. O desenvolvimento intelectual tem que ser constante e a paranóia precisa ser moderada. Então, vai uma cerveja (ou coca) aí? 😛

Em futuros artigos veremos a aplicação de alguns desses padrões.

Ruby on Rails Intermediário
Curso de Ruby on Rails Intermediário
CONHEÇA O CURSO

Gerenciamento de memória no C#: stack, heap, value-types e reference-types

Opa pessoal, tudo certinho?

Neste post, eu vou abordar uma dúvida que aparece com uma certa frequência em nosso suporte: o que são, afinal de contas, as benditas memórias stack e heap? O que são afinal de contas os value-types e os reference-types?

Para entendermos melhor estes conceitos, precisamos também verificar um pouco sobre a criação de objetos e de variáveis primitivas, assim como os conceitos de value-types e reference-types… Então, vamos lá! o/

O compilador, de maneira geral, divide a memória em duas grandes áreas: a stack (uma área bem menor) e a heap (uma área bem maior). Seria algo simiar à ilustração abaixo:

Áreas de memória - compilador .NET

Na configuração padrão do .NET Framework, para que você tenha uma idéia melhor de como a stack é muito menor que a heap, o tamanho padrão para a memória stack é de apenas 1MB!

Ambas trabalham como pilhas, porém, a maneira como cada uma provê acesso a seu conteúdo é diferente. A stack é bem mais eficiente para localizar as coisas em seu interior com relação a heap, mesmo porque ela é bem menor.

As variáveis de alguns tipos de dados leves (tipos primitivos – int, double, bool etc. – e structs) são armazenadas diretamente na stack, a área menor e mais eficiente para localização dos conteúdos. Elas ficam diretamente nessa área justamente por serem tipos de dados que não ocupam tanto espaço na memória. O mais interessante é que o valor que elas contêm também fica junto com elas na stack. Ou seja, quando você faz a declaração abaixo:

int numero = 3;

O compilador armazena essa variável diretamente na memória stack, como na ilustração abaixo:

Alocação de memória - Value-Type

C# (C Sharp) Avançado
Curso de C# (C Sharp) Avançado
CONHEÇA O CURSO

Perceba que o valor da variável fica junto com a própria variável. Variáveis onde isso acontece são chamadas de Value-Types, justamente porque o valor delas fica junto com a própria variável na memória stack. Assim, quando você tem o seguinte código:

if (numero >= 3)
{
    //...
}

O compilador tem acesso direto ao conteúdo, pois ele está juntinho com a própria variável na memória stack:

Acesso à memória: value-type

Agora, outros tipos de dados ocupam muito mais espaço de memória do que estes tipos leves que são value-types. Por isso, eles não podem ser armazenados diretamente na stack (caso fossem, rapidamente a memória stack seria “estourada”, causando o famoso erro StackOverflowException). Sendo assim, estes dados são armazenados na memória heap.

Vamos imaginar que você tenha o seguinte código:

class Pessoa
{
    public int Id {get; set;}
    public string Nome {get; set;}
}

Quando você cria um objeto dessa classe, este objeto será armazenado na memória heap:

Pessoa minhaPessoa = new Pessoa();

Alocação de memória: reference-type

Porém, o compilador não acessa a heap diretamente. Por que ele não acessa? Justamente porque ela é muito grande… Se ele fosse procurar o objeto minhaPessoa dentro da heap, ele iria demorar um tantinho bom de tempo. O compilador precisaria ter um jeito de acessar pela stack (que é rápida pra encontrar as coisas até mesmo por ser bem menor) o que está alocado na heap (que é bem maior). Como o compilador contorna isso? Criando uma referência dentro da stack para o objeto minhaPessoa, apontando onde na memória heap que este objeto está de fato guardado!

Processo de referência stack-heap

Essa porção de memória que é alocada na stack para apontar para uma posição de memória da heap é chamada de ponteiro. Por isso ele tem esse asterisco (*) na frente do seu nome.

Repare então que é criada uma referência da stack para uma determinada posição de memória da heap, referência essa guardada por um ponteiro na stack. Esse tipo de variável (como no caso da variável minhaPessoa, do tipo Pessoa) é chamada de Reference-Type, já que é necessário uma referência da stack para a heap para que esta variável seja acessível. Variáveis reference-type geralmente precisam que seja chamado o respectivo construtor através da palavra-chave new, pois ele é que define que uma porção de memória da heap deverá ser utilizada para guardar aquele objeto.

Dessa maneira, quando temos o código abaixo:

if (minhaPessoa.Id > 2)
{
    //...
}

O compilador faz o acesso ao objeto minhaPessoa através da stack, ou seja, através do ponteiro. Esse ponteiro encaminha o compilador para a posição de memória da heap que contém de fato o objeto minhaPessoa.

Acesso à memória: reference-types

Resumindo:

  • Value-Type: são tipos leves (como os tipos primitivos e structs) que ficam armazenados diretamente na memória stack. Os valores das variáveis ficam armazenados juntamente com as próprias variáveis, sendo o acesso ao seu conteúdo feito de maneira direta;
  • Reference-Type: tipos pesados (objetos criados a partir de classes, etc.) que ficam armazenados na heap. Para não sacrificar a performance, é criada uma referência (ponteiro) na stack que aponta para qual posição de memória o objeto está armazenado na heap. O acesso é feito via essa referência na stack. Sendo assim, o acesso ao conteúdo é indireto, dependendo dessa referência;
  • Stack: porção de memória pequena onde os value-types e os ponteiros ficam;
  • Heap: porção maior de memória onde os reference-types ficam de fato alocados… Para se fazer o acesso a eles, precisamos de um ponteiro na stack que indique a posição de memória na heap onde o objeto está de fato alocado.

O detalhe interessante é que a imensa maioria das linguagens gerenciadas (como o Java, Swift e o próprio C#) seguem exatamente esta arquitetura de gerenciamento de memória… Isso quer dizer que o que vimos aqui para o C#, pode ser aplicado para outras linguagens com pequenas variações. o/

É isso aí pessoal. Obrigado por terem lido este post! =)

Nos próximos posts, continuaremos as discussões sobre os value-types e os reference-types e veremos um tipo de dado que foge um pouco da regra: as strings.

C# (C Sharp) - TDD
Curso de C# (C Sharp) - TDD
CONHEÇA O CURSO