Todo código deve estar sempre pronto para evoluir e essas evoluções devem ser naturais.
Alterações evolutivas não devem modificar muitos arquivos diferentes.
O conceito que nos ajuda a ter classes coesas e que evoluam mais fácil é pensar sempre em escrever classes que são “abertas para extensão”, mas “fechadas para modificação” (sim, esse é o famoso Open-Closed Principle). A ideia é que suas classes sejam abertas para extensão. Ou seja, estender o comportamento delas deve ser fácil. Mas, ao mesmo tempo, elas devem ser fechadas para alteração. Ou seja, ela não deve ser modificada (ter seu código alterado) o tempo todo.
Quando falamos em extensão o primeiro conceito que vem em mente é a Herança, mas essa não é a única solução para garantir o princípio aberto-fechado.
Quando alterações evolutivas modificam muitos arquivos e/ou com muita frequência, é possível criar uma abstração para o problema de evolução, e fazer com que essa abstração possa ser injetada na classe que a usa.
Estas abstrações devem ser intercambiáveis, incorporando estratégias específicas para tempo de execução via métodos construtor e setters.
Para além da herança, é possível combinar uma série de conceitos como interfaces, composição e agregação para flexibilizar as evoluções, de acordo com o cenário.
Códigos flexíveis são importantes, mas têm um custo agregado: eles, com certeza, são mais complexos do que códigos não tão flexíveis. Olhar para um código, entender as abstrações que o permeiam e como as dependências entre elas interagem entre si pode não ser simples. Muitas vezes um simples if resolve o problema. Portanto, flexibilize código que realmente precise disso, e seja simples em códigos que podem ser simples.