Você aproveita os benefícios do princípio aberto-fechado?

11

O princípio de aberto-fechado (OCP) afirma que um objeto deve estar aberto para extensão, mas fechado para modificação. Acredito que entendo e uso em conjunto com o SRP para criar classes que fazem apenas uma coisa. E tento criar muitos métodos pequenos que possibilitam extrair todos os controles de comportamento em métodos que podem ser estendidos ou substituídos em alguma subclasse. Assim, termino com classes que têm muitos pontos de extensão, seja através de: injeção e composição de dependências, eventos, delegação, etc.

Considere o seguinte uma classe simples e extensível:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Agora diga, por exemplo, que as OvertimeFactoralterações para 1,5. Como a classe acima foi projetada para ser estendida, eu posso facilmente subclasses e retornar uma diferente OvertimeFactor.

Mas ... apesar da classe ser projetada para extensão e aderência ao OCP, modificarei o método único em questão, em vez de subclassificar e substituir o método em questão e depois reconectar meus objetos no meu contêiner de IoC.

Como resultado, violei parte do que o OCP tenta realizar. Parece que estou apenas com preguiça, porque o exposto acima é um pouco mais fácil. Estou entendendo mal o OCP? Eu realmente deveria estar fazendo algo diferente? Você aproveita os benefícios do OCP de maneira diferente?

Atualização : com base nas respostas, parece que este exemplo artificial é ruim por várias razões diferentes. O principal objetivo do exemplo era demonstrar que a classe foi projetada para ser estendida, fornecendo métodos que, quando substituídos, alterariam o comportamento de métodos públicos , sem a necessidade de alterar o código interno ou privado. Ainda assim, eu definitivamente não entendi o OCP.

Kaleb Pederson
fonte

Respostas:

9

Se você está modificando a classe base, ela não está realmente fechada, é isso!

Pense na situação em que você lançou a biblioteca para o mundo. Se você alterar o comportamento de sua classe base modificando o fator de horas extras para 1,5, você violou todas as pessoas que usam seu código, assumindo que a classe foi encerrada.

Realmente, para tornar a classe fechada, mas aberta, você deve recuperar o fator de horas extras de uma fonte alternativa (talvez arquivo de configuração) ou provar um método virtual que pode ser substituído?

Se a classe fosse realmente fechada, após a alteração, nenhum caso de teste falharia (supondo que você tenha 100% de cobertura com todos os seus casos de teste) e eu assumiria que existe um caso de teste que verifica GetOvertimeFactor() == 2.0M.

Não exagere no engenheiro

Mas não leve esse princípio de abertura e fechamento à conclusão lógica e tenha tudo configurável desde o início (que é sobre engenharia). Defina apenas os bits que você precisa atualmente.

O princípio fechado não o impede de reprojetar o objeto. Isso apenas impede que você altere a interface pública atualmente definida para o seu objeto ( membros protegidos fazem parte da interface pública). Você ainda pode adicionar mais funcionalidade, desde que a funcionalidade antiga não esteja quebrada.

Martin York
fonte
"O princípio fechado não o impede de reprojetar o objeto." Na verdade, sim . Se você ler o livro em que o Princípio Aberto-Fechado foi proposto pela primeira vez ou o artigo que introduziu o acrônimo "OCP", verá que ele diz que "Ninguém tem permissão para fazer alterações no código-fonte" (exceto por erros Conserta).
Rogério
@ Rogério: Isso pode ser verdade (em 1988). Mas a definição atual (popularizada em 1990 quando o OO se tornou popular) tem tudo a ver com manter uma interface pública consistente. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York
Obrigado pela referência da Wikipedia. Mas não tenho certeza de que a definição "atual" seja realmente diferente, pois ainda depende da herança de tipo (classe ou interface). E essa citação "sem alteração no código fonte" que mencionei vem do artigo da OCP 1996 de Robert Martin, que está (supostamente) alinhado com a "definição atual". Pessoalmente, acho que o Princípio Aberto-Fechado já estaria esquecido, se Martin não tivesse dado a ele um acrônimo que, aparentemente, tem muito valor de marketing. O princípio em si é ultrapassado e prejudicial, IMO.
Rogério
3

Portanto, o Princípio Aberto Fechado é uma tarefa difícil ... especialmente se você tentar aplicá-lo ao mesmo tempo que o YAGNI . Como aderir a ambos ao mesmo tempo? Aplique a regra de três . Na primeira vez que você fizer uma alteração, faça-a diretamente. E a segunda vez também. Na terceira vez, é hora de abstrair essa mudança.

Outra abordagem é "me engane uma vez ...", quando precisar fazer uma alteração, aplique o OCP para se proteger contra essa mudança no futuro . Eu quase chegaria ao ponto de propor que alterar a taxa de horas extras é uma nova história. "Como administrador da folha de pagamento, desejo alterar a taxa de horas extras para estar em conformidade com as leis trabalhistas aplicáveis". Agora você tem uma nova interface do usuário para alterar a taxa de horas extras, uma maneira de armazená-la e GetOvertimeFactor () apenas pergunta ao seu repositório qual é a taxa de horas extras.

Michael Brown
fonte
2

No exemplo que você postou, o fator horas extras deve ser uma variável ou uma constante. * (Exemplo Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

OU

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Então, quando você estender a classe, defina ou substitua o fator. Os "números mágicos" devem aparecer apenas uma vez. Isso é muito mais no estilo de OCP e DRY (não se repita), porque não é necessário criar uma nova classe para um fator diferente se estiver usando o primeiro método, e apenas precisar alterar a constante em um idiomático coloque no segundo.

Eu usaria o primeiro nos casos em que haveria vários tipos de calculadora, cada um precisando de valores constantes diferentes. Um exemplo seria o padrão da Cadeia de Responsabilidade, que geralmente é implementado usando tipos herdados. Um objeto que só pode ver a interface (ou seja getOvertimeFactor()) a utiliza para obter todas as informações necessárias, enquanto os subtipos se preocupam com as informações reais a serem fornecidas.

O segundo é útil nos casos em que a constante provavelmente não será alterada, mas é usada em vários locais. Ter uma constante para alterar (no improvável caso) é muito mais fácil do que defini-lo em todo o lugar ou obtê-lo de um arquivo de propriedades.

O princípio Aberto-fechado é menos uma chamada para não modificar um objeto existente do que um cuidado para deixar a interface inalterada. Se você precisar de algum comportamento ligeiramente diferente de uma classe ou funcionalidade adicional para um caso específico, estenda e substitua. Mas se os requisitos para a própria classe mudarem (como mudar o fator), você precisará alterar a classe. Não há sentido em uma enorme hierarquia de classes, a maioria das quais nunca é usada.

Michael K
fonte
Esta é uma alteração de dados, não uma alteração de código. A taxa de horas extras não deveria ter sido codificada.
Jim C
Você parece ter o seu Get e seu Set para trás.
Mason Wheeler
Ops! deveria ter testado ...
Michael K
2

Realmente não vejo seu exemplo como uma ótima representação do OCP. Eu acho que o que a regra realmente significa é isso:

Quando você deseja adicionar um recurso, você deve adicionar apenas uma classe e não precisa modificar nenhuma outra classe (mas possivelmente um arquivo de configuração).

Uma má implementação abaixo. Toda vez que você adiciona um jogo, você precisa modificar a classe GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

A classe GamePlayer nunca deve precisar ser modificada

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Agora, assumindo que meu GameFactory também cumpra o OCP, quando eu quiser adicionar outro jogo, eu precisaria criar uma nova classe que herda da Gameclasse e tudo deve funcionar.

Com freqüência, classes como a primeira são criadas após anos de "extensões" e nunca são refatoradas corretamente da versão original (ou pior, o que deveriam ser várias classes continua sendo uma grande classe).

O exemplo que você fornece é OCP-ish. Na minha opinião, a maneira correta de lidar com alterações nas taxas de horas extras seria em um banco de dados com taxas históricas mantidas para que os dados pudessem ser reprocessados. O código ainda deve estar fechado para modificação, pois sempre carregava o valor apropriado da pesquisa.

Como um exemplo do mundo real, usei uma variante do meu exemplo e o Princípio Aberto-Fechado realmente brilha. É fácil acrescentar funcionalidade, porque eu apenas tenho que derivar de uma classe base abstrata e minha "fábrica" ​​a pega automaticamente e o "player" não se importa com a implementação concreta que a fábrica retorna.

Austin Salonen
fonte
1

Neste exemplo em particular, você tem o que é conhecido como "Valor Mágico". Essencialmente, um valor codificado que pode ou não mudar ao longo do tempo. Vou tentar resolver o enigma que você expressa genericamente, mas este é um exemplo do tipo de coisa em que criar uma subclasse é mais trabalhoso do que alterar um valor em uma classe.

Muito provavelmente, você especificou o comportamento muito cedo na sua hierarquia de classes.

Digamos que temos o PaycheckCalculator. É OvertimeFactormuito provável que essas informações sejam excluídas das informações sobre o funcionário. Um funcionário por hora pode desfrutar de um bônus de horas extras, enquanto um funcionário assalariado não receberia nada. Ainda assim, alguns funcionários assalariados terão direito a tempo devido ao contrato em que estavam trabalhando. Você pode decidir que existem certas classes conhecidas de cenários de pagamento, e é assim que você construiria sua lógica.

Na PaycheckCalculatorclasse base, você o torna abstrato e especifica os métodos que você espera. Os cálculos principais são os mesmos, mas apenas alguns fatores são calculados de maneira diferente. Você HourlyPaycheckCalculatorimplementaria o getOvertimeFactormétodo e retornaria 1,5 ou 2,0, conforme o caso. Você StraightTimePaycheckCalculatorimplementaria o getOvertimeFactorpara retornar 1.0. Finalmente, uma terceira implementação seria uma NoOvertimePaycheckCalculatorque implementaria o getOvertimeFactorretorno 0.

A chave é descrever apenas o comportamento na classe base que se destina a ser estendido. Os detalhes de partes do algoritmo geral ou valores específicos seriam preenchidos por subclasses. O fato de você ter incluído um valor padrão para os getOvertimeFactorleads para a rápida e fácil "correção" de uma linha, em vez de estender a classe conforme desejado. Também destaca o fato de que há um esforço envolvido na extensão de classes. Também há um esforço envolvido no entendimento da hierarquia de classes no seu aplicativo. Você deseja projetar suas classes de forma a minimizar a necessidade de criar subclasses e ainda fornecer a flexibilidade necessária.

Alimento para reflexão: quando nossas aulas englobam certos fatores de dados, como OvertimeFactorno exemplo, você pode precisar de uma maneira de extrair essas informações de alguma outra fonte. Por exemplo, um arquivo de propriedades (já que isso se parece com Java) ou um banco de dados conteria o valor e você PaycheckCalculatorusaria um objeto de acesso a dados para extrair seus valores. Isso permite que as pessoas certas alterem o comportamento do sistema sem a necessidade de reescrever o código.

Berin Loritsch
fonte