sexta-feira, 21 de novembro de 2014

Software: Gestão Semântica de Versões (Semantic Versioning)

Introdução

Quem anda no mundo do desenvolvimento de software, sabe que uma das maiores dores de cabeça é a Gestão de Dependências.

Quanto mais um sistema cresce, mais componentes são adicionados, maior é a complexidade e dificuldade de se gerirem as dependências entre os módulos e maior é a probabilidade de se atingirem bloqueios ou inconsistências.

É aqui que entra a Gestão de Versões!

A adopção de um sistema de atribuição de números de versão aos componentes de software, que seja claro e lógico, dá um preciosa ajuda a mitigar os problemas colocados pela gestão de dependências.

Neste artigo, vamos analisar um sistema de gestão de versões, amplamente utilizado, a Gestão Semântica de Versões (Semantic Versioning).

Este sistema é da autoria de Tom Preston-Werner e a sua especificação detalhada pode ser encontrada aqui.

Conteúdo

O que é a Gestão Semântica de Versões (SemVer)?

A Gestão Semântica de Versões, no original Semantic Versioning (referida abreviadamente como SemVer), é um sistema de controlo de versões que tem vindo a ganhar adeptos ao longo dos últimos anos. Com novos plugins, extensões e bibliotecas a ser construídas quase diariamente e com uma forma universal de atribuição de versões a projectos de desenvolvimento de software, é uma ferramenta muito útil para nos ajudar a gerir e manter o registo das evoluções de um sistema.

A especificação SemVer define que o número da versão de um componente de software é composto por 3 partes: Major.Minor.Patch

Cada um destes números é incrementado da seguinte forma:

  • MAJOR (Principal): quando são feitas alterações incompatíveis com a API existente;
  • MINOR (Secundário): quando são adicionadas funcionalidades de forma compatível com versões anteriores;
  • PATCH (Emenda): quando são corrigidos bugs de forma compatível com versões anteriores.

Existem rótulos adicionais para metadados de pré-lançamento (pre-release) e compilação (build), que são disponibilizados como extensões do formato Major.Minor.Patch.

Como funciona a Gestão Semântica de Versões?

Num sistema com muitas dependências, o lançamento de novas versões pode tornar-se um pesadelo muito rapidamente. Isto é conhecido como o “inferno de dependências”. Se as especificações de dependência forem muito apertadas, corre-se o risco de atingir um bloqueio de versão (incapacidade de actualizar um componente, sem ter que actualizar todos os componentes dependentes). Por outro lado, se as dependências forem especificadas de forma muito vaga, então, inevitavelmente, vai-se atingir alguma promiscuidade de versões (é assumida a compatibilidade com mais versões futuras do que seria razoável). Atingiu o inferno de dependências quando um bloqueio de versão e/ou uma promiscuidade de versão o impedem de avançar com o projecto de forma fácil e segura.

A proposta da SemVer é composta por um conjunto simples de regras e requisitos, que determinam a forma como os números de versão são atribuídos e incrementados. Estas regras são baseadas em algumas práticas comuns já existentes. Para que o sistema funcione, é necessário declarar uma API pública, clara e precisa. Esta pode consistir em documentação ou ser forçada pelo próprio código da aplicação. Após a declaração, as alterações à API são comunicadas através de incrementos específicos a cada uma das partes constituintes do número de versão (major, minor ou patch).

O funcionamento da SemVer assenta em saber determinar o momento certo para incrementar o componente correcto, i.e., determinar qual ou quais os números da versão devem ser alterados para cada lançamento do software.

Porquê a Gestão Semântica de Versões?

Porquê usar um sistema de gestão de versões? Porque faz sentido!

Há algo que devemos ter a noção: o controlo de versões sem directrizes, basicamente, não tem significado. Incrementar para a versão 3.2? OK. Mas porquê? Porque não 4? Porque não 3.1.1? Porque não 3.11? Porque não 3.1.nova-versao?

Seguir orientações rigorosas ajuda a dar significado aos números de versão.

Por exemplo, se tivermos a versão 1.3.37, sabemos que este é o primeiro grande lançamento, mas já houve três versões secundárias, com novos recursos. No entanto, também notamos que se trata da emenda 37 a esta versão secundária, o que significa que havia um número razoável de bugs (maiores ou menores) envolvidos.

A SemVer também nos ajuda na gestão de dependências. Vamos imaginar que estamos a construir uma biblioteca chamada Veículos e que nesta biblioteca temos uma dependência sobre o componente Motor. No primeiro lançamento da biblioteca para produção, o componente Motor tem a versão 2.3.8. Isso significa que podemos especificar o Motor como uma dependência da biblioteca Veículos, com versão maior ou igual a 2.3.0, mas menor que 3.0.0. A versão principal significa uma alteração sem compatibilidade com a API actual, pelo que se passarmos o Motor para a versão 3.0.0, nada nos garante que temos compatibilidade com o resto da biblioteca.

Especificação SemVer 2.0.0

A especificações actualizadas para a SemVer pode ser encontradas no endereço: http://semver.org/

À data da criação deste artigo, estamos na versão 2.0.0, com a seguinte especificação:

  1. O software que usa a SemVer TEM QUE declarar uma API pública. Esta API pode ser declarada no próprio código, ou existir explicitamente em documentação. No entanto, quando feita, deve ser precisa e abrangente.
  2. Um número de versão normal deve assumir a forma X.Y.Z, onde X, Y, e Z são inteiros não negativos e não podem conter zeros à esquerda. X é a versão principal (Major), Y é a versão secundária (Minor) e Z é a versão da emenda (Patch). Cada elemento deve aumentar numericamente. Por exemplo: 1.9.0 –> 1.10.0 -> 1.11.0.
  3. Sempre que um pacote seja lançado, o seu conteúdo NÃO PODE ser modificado. Qualquer modificação deve ser lançada como uma nova versão.
  4. A versão principal (Major) a zero (0,y,z) serve para o desenvolvimento inicial. Qualquer coisa pode mudar a qualquer altura. A API pública não deve ser considerada estável.
  5. A versão 1.0.0 define a API pública. A forma como o número de versão é incrementado a partir deste lançamento inicial, depende dessa API pública e de como ela é alterada.
  6. A versão de emenda (Patch) Z (x.y.z | x>0) SÓ PODE ser incrementada se for introduzida uma correcção de um bug compatível com a API pública. A correcção de um bug define-se como uma alteração interna que corrige um comportamento incorrecto.
  7. A versão secundária (Minor) Y (x.y.z | x>0) SÓ PODE ser incrementada se for introduzida uma nova funcionalidade compatível com a API pública. A versão secundária TEM QUE ser incrementada sempre que uma funcionalidade da API pública for marcada como obsoleta. A versão secundária PODE ser incrementada pela introdução de novas funcionalidades ou melhorias substanciais, no código privado. PODE ainda incluir alterações ao nível da emenda (Patch). A versão de emenda (Patch) TEM QUE ser reposta a zero quando a versão secundária (Minor) é incrementada (1.3.55 –> 1.4.0).
  8. A versão principal (Major) X (x.y.z | x>0) TEM QUE ser incrementada se for introduzida uma alteração incompatível com a API pública. PODE incluir alterações ao nível secundário (Minor) e emendas (Patch). Tanto a versão secundária (Minor), como a versão de emenda (Patch) TÊM QUE ser repostas a zero quando a versão principal (Major) é incrementada (1.3.55 –> 2.0.0).
  9. Uma versão de pré-lançamento (pre-release) PODE ser identificada acrescentando um hífen e uma série de identificadores, separados por pontos, imediatamente após a versão de emenda (Patch). Os identificadores terão as seguintes características:
    • SÓ PODEM incluir caracteres alfanuméricos ASCII e hífen [0-9A-Za-z];
    • Um identificador NÃO PODE estar vazio;
    • Um identificador numérico NÃO PODE conter zeros à esquerda.
    As versões de pré-lançamento têm uma precedência mais baixa que a versão normal associada. Uma versão de pré-lançamento indica que a versão é instável e pode não satisfazer todos os requisitos de compatibilidade pretendidos, conforme indicado na sua versão normal associada. Exemplos: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-beta, 1.0.0-RC, 1.0.0-0.3.7, 1.0.0-x.7.z.92.
  10. Podem ser identificados metadados de compilação (build) acrescentando um sinal de mais e uma série de identificadores, separados por pontos, imediatamente após a versão de emenda (Patch) ou pré-lançamento. Os identificadores terão as seguintes características:
    • SÓ PODEM incluir caracteres alfanuméricos ASCII e hífen [0-9A-Za-z];
    • Um identificador NÃO PODE estar vazio.
    Os metadados de compilação (build) DEVEM ser ignorados na determinação da precedência de versões. Assim  duas versões que diferem apenas nos metadados de compilação têm a mesma precedência. Exemplos: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.
  11. A Precedência refere-se ao modo como as versões são comparados umas com as outras, quando ordenadas. A precedência TEM QUE ser calculada através da separação da versão em principal (Major), secundária (Minor), emenda (Patch) e de pré-lançamento, nessa ordem (metadados de compilação (build) não figuram na precedência).
    A precedência é determinada pela primeira diferença quando se compara cada um destes identificadores, da esquerda para a direita, como se segue: as versões principal, secundária e emenda são sempre comparados numericamente. Exemplo: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. Quando as versões principal, secundária e emenda são iguais, a versão de pré-lançamento tem precedência menor do que a versão normal. Exemplo: 1.0.0-alpha < 1.0.0.
    A precedência de duas versões de pré-lançamento com as versões principal, secundária e emenda iguais, TEM QUE ser determinada comparando cada identificador separado por pontos, da esquerda para a direita, até que seja encontrada uma diferença, da seguinte forma: identificadores que consistem apenas em dígitos, são comparados numericamente e identificadores com letras ou hífens são comparados lexicalmente por ordem do código ASCII. Os identificadores numéricos têm sempre menor precedência que os identificadores alfanuméricos.
    Um conjunto maior de identificadores de pré-lançamento tem uma precedência maior do que um conjunto menor, se todos os identificadores anteriores forem iguais. Exemplo: 1.0.0-alfa < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.

FAQ

Como devo lidar com revisões na fase de desenvolvimento inicial (0.y.z)?

A coisa mais simples a fazer é começar a versão de desenvolvimento inicial em 0.1.0 e incrementar a versão secundária a cada lançamento subsequente.

Como sei quando lançar a versão 1.0.0?

Se o software já está a ser usado em produção, então já deve estar na versão 1.0.0. Se possui uma API estável, da qual os utilizadores passaram a depender, então deve ser a 1.0.0. Se já se está a preocupar bastante com compatibilidade com versões anteriores, então já deve ser a 1.0.0.

Isto não desencoraja o desenvolvimento ágil (Agile) e iteração rápida?

A versão principal (Major) a zero tem o foco exatamente no desenvolvimento rápido. Se estiver a mudar a API todos os dias, provavelmente está na versão 0.y.z ou num branch separado de desenvolvimento, a trabalhar numa próxima versão principal.

Se mesmo a menor mudança incompatível com a API pública requer aumento da versão principal (Major), não vou acabar na versão 42.0.0 muito rapidamente?

Esta é uma questão de desenvolvimento responsável e conhecimento antecipado. Mudanças incompatíveis não devem ser introduzidas de forma ligeira em software que tem muito código dependente. O custo necessário à atualização pode ser significativo. Ter que aumentar a versão principal para introduzir mudanças incompatíveis, significa que pensará no impacto dessas mudanças e que avaliará a relação custo/benefício envolvida.

Documentar toda a API pública dá muito trabalho!

É sua responsabilidade como programador profissional documentar corretamente o software que vai ser usado por outros. Gerir a complexidade do software é uma parte extremamente importante de manter um projeto eficiente e isso é difícil de fazer se ninguém sabe como usar o seu software, ou que métodos são seguros de chamar. A longo prazo, a SemVer e a insistência numa API pública bem definida, podem manter tudo e todos a funcionar suavemente.

O que eu faço se, acidentalmente, lançar uma mudança incompatível com versões anteriores como uma versão secundária (Minor)?

Assim que perceber que não cumpriu a especificação da SemVer, repare o problema e lance uma nova versão secundária, que corrige o problema e restaura a compatibilidade. Mesmo nesta circunstância, é inaceitável modificar versões já lançadas. Se for apropriado, documente a versão incumpridora e informe os seus utilizadores do problema, de forma a que eles fiquem cientes da versão em questão.

O que devo fazer se atualizar as minhas próprias dependências sem modificar a API pública?

Isso seria considerado compatível, uma vez que não afeta a API pública. Software que depende explicitamente das mesmas dependências que o seu pacote, deve ter a sua própria especificação de dependência e o autor notificará quaisquer conflitos. Para determinar se a mudança é ao nível de emenda (Patch) ou ao nível secundário, dependente se atualizou as dependências a fim de corrigir um bug ou introduzir uma nova funcionalidade.

E se alterei inadvertidamente a API pública de forma incompatível com a mudança no número de versão (ex.: o código introduz, incorretamente, uma grande mudança incompatível, no lançamento de um patch)

Use o bom senso. Se tiver uma audiência enorme, que será drasticamente afectada pela mudança de comportamento de voltar ao que a API pública pretendia, então pode ser melhor realizar um lançamento de uma versão principal (Major), mesmo que a correção pudesse ser considerada estritamente uma versão de emenda (Patch). Lembre-se, SemVer trata de transmitir significado à forma como os números de versão mudam. Se estas mudanças são importantes para os seus utilizadores, utilize o número da versão para informá-los.

Como devo lidar com descontinuação de funcionalidades?

Descontinuar funcionalidades (deprecating) é um processo comum no desenvolvimento de software e muitas vezes é necessário para poder haver progresso. Quando descontinua partes da sua API pública, deve fazer duas coisas: (1) atualizar a sua documentação, para que os utilizadores saibam das mudanças, (2) lançar uma versão secundária (Minor) que anuncie a descontinuação. Antes de remover completamente a funcionalidade numa versão principal (Major), deve haver, pelo menos, uma versão secundária (Minor) que possui a descontinuação anunciada, permitindo com que os utilizadores façam uma transição tranquila para a nova API.

A SemVer tem um limite de tamanho para o número de versão?

Não, mas use o bom senso. Uma string de versão com 255 caracteres, por exemplo, é provavelmente um exagero. Porém, sistemas específicos podem definir seus próprios limites para o tamanho da string.

Licença

A Especificação da SemVer é da autoria de Tom Preston-Werner, criador do Gravatar e co-fundador do GitHub.

A SemVer é disponibilizada sob a licença Creative Commons - CC BY 3.0.

Caso queira deixar feedback, por favor abra um issue no GitHub.

Referências

Semantic Versioning 2.0.0

"Semantic Versioning: Why You Should Be Using it", de Hugo Giraudel para o SitePoint.

Sem comentários:

Enviar um comentário