Olá Habr! Aqui está a tradução de um artigo de Robert Martin, do Princípio Aberto-Fechado , que ele publicou em janeiro de 1996. O artigo, para dizer o mínimo, não é o mais recente. Mas no RuNet, os artigos do tio Bob sobre o SOLID são recontados apenas de forma truncada, então pensei que uma tradução completa não seria supérflua.

Decidi começar com a letra O, já que o princípio de abertura-fechamento é de fato central. Entre outras coisas, há muitas sutilezas importantes que merecem atenção:
- Nenhum programa pode ser "fechado" 100%.
- A programação orientada a objetos (OOP) opera não com objetos físicos do mundo real, mas com conceitos - por exemplo, o conceito de "ordenação".
Este é o primeiro artigo da minha coluna Engineer Notes para The C ++ Report . Os artigos publicados nesta coluna se concentrarão no uso de C ++ e OOP e abordarão as dificuldades no desenvolvimento de software. Vou tentar tornar os materiais pragmáticos e úteis para a prática de engenheiros. Para a documentação do design orientado a objetos nestes artigos, utilizarei a notação de Buch.
Existem muitas heurísticas associadas à programação orientada a objetos. Por exemplo, “todas as variáveis de membro devem ser privadas” ou “variáveis globais devem ser evitadas” ou “a determinação de tipo em tempo de execução é perigosa”. Qual o motivo de tais heurísticas? Por que eles são verdadeiros? Eles sempre são verdadeiros? Esta coluna explora o princípio de design subjacente a essas heurísticas - o princípio de abertura-fechamento.
Ivar Jacobson disse: “Todos os sistemas mudam durante o ciclo de vida. Isso deve ser lembrado ao projetar um sistema com mais de uma versão esperada. ” Como podemos projetar um sistema para que ele seja estável diante das mudanças e que tenha mais de uma versão esperada? Bertrand Meyer nos contou sobre isso em 1988, quando o agora famoso princípio da abertura-proximidade foi formulado:
As entidades do programa (classes, módulos, funções, etc.) devem estar abertas para expansão e fechadas para alterações.
Se uma alteração no programa envolve uma cascata de alterações nos módulos dependentes, sinais indesejáveis de um design "ruim" aparecem no programa.
O programa se torna frágil, inflexível, imprevisível e sem uso. O princípio da abertura-proximidade resolve esses problemas de maneira muito direta. Ele diz que é necessário projetar módulos que nunca mudam . Quando os requisitos mudam, você precisa expandir o comportamento de tais módulos adicionando novo código, em vez de alterar o código antigo que já está funcionando.
Descrição do produto
Módulos que atendem ao princípio de abertura-proximidade têm duas características principais:
- Aberto para expansão. Isso significa que o comportamento do módulo pode ser expandido. Ou seja, podemos adicionar um novo comportamento ao módulo de acordo com os requisitos variáveis do aplicativo ou para atender às necessidades de novos aplicativos.
- Fechado para mudança. O código fonte desse módulo é intocável. Ninguém tem o direito de fazer alterações.
Parece que esses dois sinais não se encaixam. A maneira padrão de estender o comportamento de um módulo é fazer alterações nele. Um módulo que não pode ser alterado geralmente é pensado como um módulo com comportamento fixo. Como essas duas condições opostas podem ser cumpridas?
A chave da solução é a abstração.
No C ++, usando os princípios do design orientado a objetos, é possível criar abstrações fixas que podem representar um conjunto ilimitado de comportamentos possíveis.
Abstrações são classes básicas abstratas e um conjunto ilimitado de comportamentos possíveis é representado por todas as classes possíveis possíveis. Um módulo pode manipular a abstração. Esse módulo está fechado para alterações, pois depende de uma abstração fixa. Além disso, o comportamento do módulo pode ser expandido criando novos descendentes da abstração.
O diagrama abaixo mostra uma opção de design simples que não atende ao princípio de abertura-proximidade. Ambas as classes, Client
e Server
, não são abstratas. Não há garantia de que as funções que são membros da classe Server
sejam virtuais. A classe Client
usa a classe Server
. Se queremos que o objeto da classe Client
use um objeto de servidor diferente, devemos alterar a classe Client
para se referir à nova classe de servidor.

Cliente fechado
E o diagrama a seguir mostra a opção de design correspondente, que atende ao princípio de abertura-proximidade. Nesse caso, a classe AbstractServer
é uma classe abstrata, cujas funções de membro são virtuais. A classe Client
usa abstração. No entanto, os objetos da classe Client
usarão objetos da classe sucessora do Server
. Se quisermos que objetos da classe Client
usem uma classe de servidor diferente, apresentaremos um novo descendente da classe AbstractServer
. A classe Client
permanecerá inalterada.

Cliente aberto
Resumo da Shape
Considere um aplicativo que deve desenhar círculos e quadrados em uma GUI padrão. Círculos e quadrados devem ser desenhados em uma ordem específica. Na ordem correspondente, uma lista de círculos e quadrados será compilada, o programa deve passar por essa lista na ordem e desenhar cada círculo ou quadrado.
Em C, usando técnicas de programação procedural que não atendem ao princípio de abertura / fechamento, podemos resolver esse problema, como mostra a Listagem 1. Aqui vemos muitas estruturas de dados com o mesmo primeiro elemento. Este elemento é um código de tipo que identifica a estrutura de dados como um círculo ou quadrado. A função DrawAllShapes
passa por uma matriz de ponteiros para essas estruturas de dados, reconhecendo o código de tipo e chamando a função correspondente ( DrawCircle
ou DrawSquare
).
A função DrawAllShapes
não atende ao princípio do fechamento de abertura, porque não pode ser "fechado" a partir de novos tipos de formas. Se eu quisesse expandir essa função com a capacidade de desenhar formas de uma lista que inclui triângulos, precisaria alterar a função. Na verdade, preciso alterar a função para cada novo tipo de forma que preciso desenhar.
Obviamente, este programa é apenas um exemplo. Na vida real, o operador do DrawAllShapes
função DrawAllShapes
seria repetido várias vezes em várias funções em todo o aplicativo e cada uma faria algo diferente. Adicionar novas formas a esse aplicativo significa encontrar todos os locais onde essas switch
(ou if/else
cadeia) são usadas e adicionar uma nova forma a cada uma delas. Além disso, é muito improvável que todas as switch
e cadeias if/else
sejam tão bem estruturadas quanto no DrawAllShapes
. É muito mais provável que predicados em if
sejam combinadas com operadores lógicos ou blocos de casos de switch
de switch
sejam combinados de forma a "simplificar" um local específico no código. Portanto, o problema de encontrar e entender todos os locais em que você precisa adicionar uma nova figura pode não ser trivial.
Na Listagem 2, mostrarei o código que demonstra uma solução quadrada / circular que atende ao princípio do fechamento de abertura. Uma classe Shape
abstrata é introduzida. Esta classe abstrata contém uma função Draw
pura e virtual. As classes Circle
e Square
são descendentes da classe Shape
.
Observe que, se quisermos estender o comportamento da função DrawAllShapes
na Listagem 2 para desenhar um novo tipo de forma, tudo o que precisamos fazer é adicionar um novo descendente da classe Shape
. Não há necessidade de alterar a função DrawAllShapes
. Portanto, DrawAllShapes
atende ao princípio de abertura-proximidade. Seu comportamento pode ser expandido sem alterar a própria função.
No mundo real, a classe Shape
conteria muitos outros métodos. Ainda assim, adicionar uma nova forma ao aplicativo ainda é muito simples, pois tudo que você precisa fazer é inserir um novo herdeiro e implementar essas funções. Não há necessidade de vasculhar todo o aplicativo em busca de locais que exigem mudanças.
Portanto, os programas que atendem ao princípio da abertura-proximidade são alterados adicionando novo código, e não alterando o existente; eles não transformam em cascata as alterações características dos programas que não correspondem a esse princípio.
Estratégia de entrada fechada
Obviamente, nenhum programa pode ser 100% fechado. Por exemplo, o que acontece com a função DrawAllShapes
na Listagem 2 se decidirmos que círculos e quadrados devem ser desenhados primeiro? A função DrawAllShapes
não DrawAllShapes
fechada para esse tipo de alteração. Em geral, não importa o quão "fechado" o módulo esteja, sempre há algum tipo de alteração da qual ele não está fechado.
Como o fechamento não pode ser completo, ele deve ser introduzido estrategicamente. Ou seja, o designer deve escolher os tipos de alterações das quais o programa será fechado. Isso requer alguma experiência. Um desenvolvedor experiente conhece os usuários e o setor suficientemente bem para calcular a probabilidade de várias alterações. Ele então garante que o princípio da abertura-proximidade seja respeitado nas mudanças mais prováveis.
Uso de abstração para obter proximidade adicional
Como podemos fechar a função DrawAllShapes
de alterações na ordem do desenho? Lembre-se de que o fechamento é baseado na abstração. Portanto, para fechar DrawAllShapes
do pedido, precisamos de algum tipo de "abstração do pedido". Um caso especial de pedido, apresentado acima, é desenhar figuras de um tipo na frente de figuras de outro tipo.
A política de pedidos implica que, com dois objetos, você pode determinar qual deles deve ser desenhado primeiro. Portanto, podemos definir um método para a classe Shape
chamada Precedes
, que pega outro objeto Shape
como argumento e retorna um valor booleano true
se o objeto Shape
que recebeu essa mensagem precisar ser classificado antes do objeto Shape
que foi passado como argumento.
No C ++, essa função pode ser representada como uma sobrecarga do operador "<". A Listagem 3 mostra a classe Shape
com métodos de classificação.
Agora que temos uma maneira de determinar a ordem dos objetos da classe Shape
, podemos classificá-los e desenhá-los. A Listagem 4 mostra o código C ++ correspondente. Ele usa as classes Set
, OrderedSet
e Iterator
da categoria Components
desenvolvida em meu livro (Designing Applications C ++ Orientados a Objetos usando o Método Booch, Robert C. Martin, Prentice Hall, 1995).
Portanto, implementamos a ordem dos objetos da classe Shape
e os desenhamos na ordem apropriada. Mas ainda não temos uma implementação da abstração do pedido. Obviamente, todo objeto Shape
deve substituir o método Precedes
para determinar a ordem. Como isso pode funcionar? Que código precisa ser escrito em Circle::Precedes
para que os círculos sejam desenhados em quadrados? Preste atenção na lista 5.
É claro que essa função não atende ao princípio da abertura-proximidade. Não há como fechá-lo dos novos descendentes da classe Shape
. Cada vez que um novo descendente da classe Shape
aparece, essa função precisa ser alterada.
Usando uma abordagem orientada a dados para obter o fechamento
A proximidade dos herdeiros da classe Shape
pode ser obtida usando uma abordagem tabular que não provoca alterações em cada classe herdada. Um exemplo dessa abordagem é mostrado na Listagem 6.
Usando essa abordagem, fechamos com êxito a função DrawAllShapes
das alterações relacionadas à ordem e a cada descendente da classe Shape
- da introdução de um novo descendente ou de uma alteração na política de ordenação de objetos da classe Shape
dependendo do tipo (por exemplo, objetos da classe Squares
ser desenhado primeiro).
O único elemento que não está impedido de alterar a ordem das formas de desenho é uma tabela. A tabela pode ser colocada em um módulo separado, separado de todos os outros módulos e, portanto, suas alterações não afetarão outros módulos.
Fechamento adicional
Este não é o fim da história. Fechamos a hierarquia da classe Shape
e a função DrawAllShapes
de alterar a política de pedidos com base no tipo de formas. No entanto, os descendentes da classe Shape
não estão fechados das políticas de pedidos que não estão associadas aos tipos de Shape
. Parece que precisamos organizar o desenho das formas de acordo com uma estrutura de nível superior. Um estudo completo desses problemas está além do escopo deste artigo; no entanto, um leitor interessado pode pensar em como resolver esse problema usando a classe abstrata OrderedObject
contida na classe OrderedShape
, que herda das OrderedObject
Shape
e OrderedObject
.
Heurísticas e Convenções
Como já mencionado no início do artigo, o princípio de abertura-proximidade é a principal motivação por trás de muitas heurísticas e convenções que surgiram ao longo dos muitos anos de desenvolvimento do paradigma OOP. A seguir, são os mais importantes.
Tornar todas as variáveis de membro privadas
Esta é uma das convenções mais duradouras da OLP. As variáveis de membro devem ser conhecidas apenas pelos métodos da classe em que estão definidas. Membros variáveis não devem ser conhecidos por nenhuma outra classe, incluindo classes derivadas. Portanto, eles devem ser declarados com um modificador de acesso private
, não public
ou protected
.
À luz do princípio da abertura-proximidade, a razão de tal convenção é compreensível. Quando as variáveis dos membros da classe mudam, cada função que depende delas deve mudar. Ou seja, a função não está fechada de alterações nessas variáveis.
Em OOP, esperamos que os métodos de uma classe não sejam fechados para alterações nas variáveis que são membros dessa classe. No entanto, esperamos que qualquer outra classe, incluindo subclasses, seja fechada de alterações nessas variáveis. Isso é chamado de encapsulamento.
Mas e se você tiver uma variável sobre a qual tenha certeza de que ela nunca mudará? Faz sentido torná-lo private
? Por exemplo, a Listagem 7 mostra a classe Device
que contém o bool status
membro variável. Ele armazena o status da última operação. Se a operação foi bem-sucedida, o valor da variável status
será true
, caso contrário, false
.
Sabemos que o tipo ou significado dessa variável nunca será alterado. Então, por que não torná-lo public
e dar ao cliente acesso direto a ele? Se a variável realmente nunca mudar, se todos os clientes seguirem as regras e apenas lerem essa variável, não haverá nada de errado com o fato de a variável ser pública. No entanto, considere o que acontecerá se um dos clientes aproveitar a oportunidade para gravar nessa variável e alterar seu valor.
De repente, esse cliente pode afetar a operação de qualquer outro cliente da classe Device
. Isso significa que é impossível fechar os clientes da classe Device
das alterações neste módulo incorreto. Isso é muito risco.
Por outro lado, suponha que tenhamos a classe Time
, mostrada na Listagem 8. Qual é o perigo de publicidade das variáveis que são membros dessa classe? É muito improvável que eles mudem. Além disso, não importa se os módulos do cliente alteram os valores dessas variáveis ou não, pois é assumida uma alteração nessas variáveis. Também é muito improvável que classes herdadas possam depender do valor de uma variável de membro específica. Então, há algum problema?
A única reclamação que eu poderia apresentar ao código da Listagem 8 é que a mudança de horário não é atômica. Ou seja, o cliente pode alterar o valor da variável de minutes
sem alterar o valor da variável de hours
. Isso pode fazer com que um objeto da classe Time
contenha dados inconsistentes. Eu preferiria introduzir uma única função para definir a hora, que levaria três argumentos, o que tornaria a configuração da hora uma operação atômica. Mas este é um argumento fraco.
É fácil criar outras condições sob as quais a publicidade dessas variáveis possa levar a problemas. Em última análise, no entanto, não há motivos convincentes para torná-los private
. Eu ainda acho que tornar essas variáveis públicas é um estilo ruim, mas talvez não seja um design ruim. Eu acredito que esse é um estilo ruim, porque não custa quase nada entrar nas funções apropriadas para acessar esses membros, e definitivamente vale a pena se proteger do pequeno risco associado à possível ocorrência de problemas com o fechamento.
Portanto, em casos tão raros, quando o princípio de abertura-fechamento não é violado, a proibição de variáveis public
- e protected
depende mais do estilo e não do conteúdo.
Nenhuma variável global ... de jeito nenhum!
O argumento contra variáveis globais é o mesmo que o argumento contra variáveis membro públicas. Nenhum módulo que depende de uma variável global pode ser fechado a partir de um módulo que possa gravar nela. Qualquer módulo que use essa variável de uma maneira não pretendida por outros módulos quebrará esses módulos. É muito arriscado ter muitos módulos, dependendo dos caprichos de um único módulo malicioso.
Por outro lado, nos casos em que variáveis globais têm um pequeno número de módulos dependentes delas ou não podem ser usadas da maneira errada, elas não causam danos. O designer deve avaliar quanta privacidade é sacrificada e determinar se a conveniência fornecida pela variável global vale a pena.
Aqui, novamente, problemas de estilo entram em cena. Alternativas ao uso de variáveis globais geralmente são baratas. Nesses casos, o uso de uma técnica que introduz, embora pequena, mas um risco de fechamento, em vez de uma técnica que elimina completamente esse risco, é um sinal de mau estilo. No entanto, às vezes o uso de variáveis globais é realmente conveniente. Um exemplo típico são as variáveis globais cout e cin. Nesses casos, se o princípio de abertura-proximidade não for violado, você poderá sacrificar o estilo por conveniência.
RTTI é perigoso
Outra proibição comum é o uso de dynamic_cast
. Freqüentemente, dynamic_cast
ou alguma outra forma de determinação do tipo de tempo de execução (RTTI) é acusada de ser uma técnica extremamente perigosa e, portanto, deve ser evitada. Ao mesmo tempo, eles geralmente dão um exemplo da Lista 9, que obviamente viola o princípio de abertura-proximidade. No entanto, a Listagem 10 mostra um exemplo de programa semelhante que usa dynamic_cast
sem violar o princípio de abertura e fechamento.
A diferença entre eles é que, no primeiro caso, mostrado na Listagem 9, o código precisa ser alterado toda vez que um novo descendente da classe Shape
aparece (sem mencionar que essa é uma solução absolutamente ridícula). No entanto, na Listagem 10, nenhuma mudança é necessária neste caso. Portanto, o código na Listagem 10 não viola o princípio de abertura e fechamento.
Nesse caso, a regra geral é que o RTTI pode ser usado se o princípio do fechamento da abertura não for violado.
Conclusão
Eu poderia falar por um longo tempo sobre o princípio da abertura-proximidade. De muitas maneiras, esse princípio é mais importante para a programação orientada a objetos. A conformidade com esse princípio específico fornece as principais vantagens da tecnologia orientada a objetos, como a reutilização e o suporte.
, - -. , , , , , .