A noção de invariante está fortemente ligada a "efeitos colaterais". Acredito que foi promovido pela abordagem 'Design by Contract (DbC)' de Bertrand Meyer para design de software.
O DbC enriquece tipos de dados abstratos (coluna vertebral de classes) com 3 noções importantes, pré-condições, pós-condições, invariantes . É facilmente explicado ao se referir a procedimentos, então tentarei explicar com referência a ele:
Uma pré-condição representa os dados de entrada da condição para um procedimento que deve ser respeitado para chamar esse procedimento. Essa pré-condição deve ser respeitada e aplicada pelo cliente desse procedimento específico. O designer do procedimento pode, no entanto, defender de clientes que não respeitam a pré-condição, afirmando essa condição como primeiras linhas no procedimento. Por exemplo, ter um método double divide(double dividend, double divisor)
pode ser uma pré-condição divisor != 0
.
Uma pós-condição representa a condição a nos dados de saída após o retorno do procedimento; é dever do projetista do procedimento respeitar essa pós-condição, desde que a pré-condição seja respeitada; em um estilo de programação de defesa antes de retornar, a pós-condição pode ser afirmada.
Um invariante pode ser considerado uma pré-condição e uma pós-condição, mas com um entendimento diferente para a pré-condição e pós-condição dos conceitos acima. Um invariante basicamente diz que, se a entrada tiver uma condição específica atendida antes do procedimento ser chamado, essa condição específica será válida após o procedimento ser chamado. Por exemplo, uma invariável válida para um procedimento boolean search(int term, int array[])
pode dizer que o estado array
anterior à chamada é o mesmo que é após a chamada.
Impor os invariantes aos procedimentos (e não apenas aos procedimentos) é uma grande coisa, pois reduz os efeitos colaterais ; isso é útil, pois os efeitos colaterais são um grande mal na programação. Um procedimento específico pode alterar o estado dos argumentos de entrada, ou alterar o estado de algumas variáveis globais, ou depender de algumas variáveis globais; isso pode levar a situações desagradáveis em que duas chamadas idênticas no mesmo procedimento (com a mesma entrada) podem produzir resultados diferentes. Isso leva a conhecer o histórico das chamadas e é muito difícil depurar, especialmente em um contexto de multithreading.