• Nenhum resultado encontrado

PRINCÍPIO DO ABERTO/FECHADO

FIG 4 Herança x Composição Fonte: Bigonha.

3.4 PRINCÍPIO DO ABERTO/FECHADO

De acordo com Meyer (1997), classes deverão ser abertas para extensão, mas fechadas para modificação. Esse é o OCP, Princípio do Aberto/Fechado. Esse princípio estabelece que as classes que estendam alguma outra classe devem utilizar as funcionalidades da classe estendida sem que essa superclasse precise ser alterada. Isso

32 significa que se o princípio for obedecido, o acréscimo de novas funcionalidades em um software não implica em alteração de suas classes, mas apenas na introdução de novas. Veja o exemplo do método totalPrice, desenvolvido para calcular preço de alguns tipos de peças:

public class Part {

public double totalPrice(Part[ ] parts) { double total = 0.0;

for (int i = 0; i < parts.length; i++) { total += parts[i].getPrice( ); }

return total; }

}

public class MotherBoard extends Part{ public double getPrice( ){

super.getPrice(); }

}

public class Memory extends Part{ public double getPrice( ){

super.getPrice(); }

}

E se devido ao aumento de imposto repentino de alguns produtos fosse necessário cobrar um preço adicional sobre certas peças? Por exemplo, se os preços de memória e de placa mãe tivessem que ser majorado em 30% e 45%, respectivamente, uma solução seria alterar o método totalPtrice para:

public double totalPrice(Part[ ] parts) { double total = 0.0;

for (int i = 0; i < parts.length; i++) { if (parts[i] instanceof Motherboard)

total += (1.45* parts[i].getPrice( )); else if (parts[i] instanceof Memory)

total += (1.30* parts[i].getPrice( )); else total += parts[i].getPrice( ); }

return total; }

Imagine que fosse necessário agora diferenciar uma nova peça, seria necessário inserir um novo if na condição e ela poderia ficar cada vez maior a medida que a lista de produtos com preços diferenciados aumentasse. Essa nova estrutura funciona como um

switch e deve ser evitada, pois, prejudica a manutenibilidade do código uma vez que, a necessidade de uma alteração pode desencadear uma série de outras alterações e tornar o código cada vez mais complexo.

33 Uma melhor solução é evitar o uso do switch, preservar o totalPrice original e embutir as variações de preço nos métodos getPrice de cada peça, como mostra o código a seguir.

public class Part {

private double basePrice;

public void setPrice(double price){basePrice=price;} public double getPrice( ){ return basePrice;}

public double totalPrice(Part[ ] parts) { double total = 0.0;

for (int i = 0; i < parts.length; i++) { total += parts[i].getPrice( ); }

return total; }

}

public class MotherBoard extends Part{ public double getPrice( ){

return 1.45*super.getPrice(); }

}

public class Memory extends Part{ public double getPrice( ){

return 1.30*super.getPrice(); }

}

Na solução acima, o acesso ao atributo que contém o preço base foi restringido e as subclasses passaram a utilizar o método de forma específica. Nesse caso, a classe Part

ficou aberta para extensão e fechada para modificação e não precisa ser alterada a cada vez que uma peça sofrer alteração de preço ou uma nova peça com um preço diferente começar a ser vendida. Nessa nova implementação, foi utilizando um switch implícito também conhecido como dynamic binding. Como mostrado nesse código, nesse tipo de implementação, existirão ainda cases para a obtenção dos preços, mas, eles serão especificados diretamente nas classes que calculam o preço de cada peça.

Podemos concluir que devemos utilizar o princípio do aberto/fechado para que sempre que precisarmos estender um comportamento de uma classe, seja criado um novo código em vez de alterar o código já existente. Assim, diminuímos o grau de acoplamento, já que quando uma classe estende o comportamento de outra sem alterá- lo, essas classes ficam mais independentes e as alterações realizadas em uma delas não afetarão fortemente a outra. É como mostramos no exemplo com a solução do problema. Caso haja alteração somente no preço de uma MotherBoard, não será necessário alterar a classe Part. Diminuímos também os custos de manutenção já que, como as classes não estarão fortemente acopladas, ao realizar uma alteração em um método de uma delas, não será necessário alterar a(s) outra(s).

34 3.5 PRINCÍPIO DA SUBSTITUIÇÃO DE LISKOV

De acordo com Liskov (1987), na implementação de uma herança as funções que usam referências para objetos de uma classe devem ser capazes de referenciar objetos das subclasses desta classe sem o saberem. Esse é o LSP, Princípio da Substituição de Liskov. Esse princípio estabelece que as classes filhas devem implementar a superclasse sem alterar o contrato de implementação, ou seja, as classes filhas devem implementar o que a superclasse propõe em seu escopo.

Para mostrarmos um exemplo de violação do LSP, considere uma implementação parecida com o exemplo utilizado por Martin (2006), trata-se da seguinte classe

Retangulo:

public class Rectangle {

private double width, height;

public Rectangle(double w, double h) { width = w; height = h;

}

public double getWidth( ) {return width;} public double getHeight( ) {return height;} public void setWidth(double w){width = w;} public void setHeight(double h){height = h;} public double area( ) {return width*height;} }

Agora vejamos a seguinte classe Quadrado, considerando o ponto de vista matemático que todo quadrado é um retângulo:

public class Square extends Rectangle { public Square(double w) {

super(w,w); }

public void setWidth(double w) { // Violação do princípio aqui

super.setWidth(w); super.setHeight(w); }

public void setHeight(double h) { // Violação do princípio aqui

super.setHeight(h); super.setWidth(h); }

}

Conforme a indicação dos locais no código acima onde o princípio é violado, essas linhas da implementação desrespeitam o contrato de implementação da superclasse

Retangulo na qual quando se modifica a altura, só se modifica a altura e quando se modifica a largura, só se modifica a largura. Na classe Quadrado quando a altura é

35 modificada, a largura também é modificada e vice-versa. Verifique o efeito nas funções de teste abaixo:

static void testLSP(Rectangle r) { r.setWidth(4.0);

r.setHeight(8.0);

System.out print( 4.0X8.0 = " + r.area( )); }

public static void main(String [ ] args) { Rectangle r = new Rectangle(1.0, 1.0); Square s = new Square(1.0);

testLSP(r); // Funciona testLSP(s); // Nao Funciona! }

Saída:

4.0X8.0 = 32.0 (Certo) 4.0X8.0 = 64.0 (Errado)

Dizemos que testLSP(s) não funciona por que o resultado impresso não corresponde ao esperado pela leitura de seu código.

Vejamos a solução do problema:

public class Rectangle {

private double width, height;

public Rectangle(double w, double h) { width = w; height = h;

}

public double getWidth( ) {return width;} public double getHeight( ) {return height;} public double area( ) {return width * height;} }

public class Square extends Rectangle {

public Square(double w) { super(w,w); } }

No exemplo acima, são retirados os métodos setWidth e setHeight das duas classes e após criadas, a altura e a largura não mais podem ser alteradas. Agora a classe

Quadrado respeita o contrato de implementação de Retangulo. Novo método

testLps:

static void testLSP(Rectangle r) { double a = r.getWidth( ); double b = r.getHeight( );

System.out print(a+ "X "+b+" = " + r.area( )); }

36 Aplicação do teste:

public static void main(String [ ] args) { Rectangle r = new Rectangle(4.0, 8.0); Square s = new Square(4.0);

testLSP(r); // Funciona testLSP(s); // Funciona! } Saída: 4.0X8.0 = 32 (Certo) 4.0X4.0 = 16 (Certo)

Respeitar o contrato significa ter cuidado em respeitar as pré-condições e pós-condições definidas no contrato da superclasse. Não é possível que uma classe filha tenha uma restrição maior que a superclasse sobre as pré-condições, mas, é possível ter uma maior restrição sobre as pós-condições.

Como um exemplo de restrição sobre as pré-condições, podemos citar o exemplo de uma função declarada na superclasse que retorne um número inteiro com valores no intervalo de 1 a 15. A subclasse não poderia restringir esse mesmo atributo com os valores de 4 a 6, mas, poderia ampliar o intervalo, por exemplo, com os valores de 1 a 20. Como um exemplo de restrição sobre as pós-condições podemos citar uma função implementada na superclasse que retorne um valor inteiro no intervalo de 5 a 15. A subclasse não poderia ampliar o retorno desse método com um intervalo de 5 a 30, mas, poderia restringir, por exemplo, com um intervalo de 5 a 10.

O LSP é outro princípio importante para facilitar a manutenção e expansão do software. Uma vez que uma subclasse cumpra o contrato de implementação da superclasse, ela terá implementado de maneira organizada os métodos necessários e facilitará o reúso e a compreensão do código.

Documentos relacionados