URL: https://www.progressiverobot.com/s-o-l-i-d-the-first-five-principles-of-object-oriented-design-pt/

Introdução

_SOLID_ é uma sigla para os primeiros cinco princípios do design orientado a objeto (OOD) criada por Robert C. Martin (também conhecido como Uncle Bob).

Nota: embora esses princípios sejam aplicáveis a várias linguagens de programação, o código de amostra contido neste artigo usará o PHP.

Esses princípios estabelecem práticas que contribuem para o desenvolvimento de software com considerações de manutenção e extensão à medida que o projeto cresce. A adoção dessas práticas também pode contribuir para evitar problemas de código, refatoração de código e o desenvolvimento ágil e adaptativo de software.

SOLID significa:

  • [S – Single-responsibility Principle](#single-responsibility-principle) (Princípio da responsabilidade única)
  • [O – Open-closed Principle](#open-closed-principle) (Princípio do aberto-fechado)
  • [L – Liskov Substitution Principle](#liskov-substitution-principle) (Princípio da substituição de Liskov)
  • [I – Interface Segregation Principle](#interface-segregation-principle) (Princípio da segregação de interfaces)
  • [D – Dependency Inversion Principle](#dependency-inversion-principle) (Princípio da inversão de dependência)

Neste artigo, cada princípio será apresentado individualmente para que você compreenda como o SOLID pode ajudá-lo(a) a melhorar como desenvolvedor(a).

Princípio da responsabilidade única

princ illustration for: Princípio da responsabilidade única

O Princípio da responsabilidade única (SRP) declara:

> Uma classe deve ter um e apenas um motivo para mudar, o que significa que uma classe deve ter apenas uma função.

Por exemplo, considere um aplicativo que recebe uma coleção de formas — círculos e quadrados — e calcula a soma da área de todas as formas na coleção.

Primeiramente, crie as classes de formas e faça com que os construtores configurem os parâmetros necessários.

Para quadrados, será necessário saber o length (comprimento) de um lado:

				
					
class Square

{

 public $length;



 public function construct($length)

 {

 $this->length = $length;

 }

}

				
			

Para os círculos, será necessário saber o radius (raio):

				
					
class Circle

{

 public $radius;



 public function construct($radius)

 {

 $this->radius = $radius;

 }

}

				
			

Em seguida, crie a classe AreaCalculator e então escreva a lógica para somar as áreas de todas as formas fornecidas. A área de um quadrado é calculada pelo quadrado do comprimento. A área de um círculo é calculada por pi multiplicado pelo quadrado do raio.

				
					
class AreaCalculator

{

 protected $shapes;



 public function __construct($shapes = [])

 {

 $this->shapes = $shapes;

 }



 public function sum()

 {

 <^>foreach ($this->shapes as $shape) {<^>

 <^>if (is_a($shape, 'Square')) {<^>

 <^>$area[] = pow($shape->length, 2);<^>

 <^>} elseif (is_a($shape, 'Circle')) {<^>

 <^>$area[] = pi() * pow($shape->radius, 2);<^>

 <^>}<^>

 <^>}<^>



 <^>return array_sum($area);<^>

 }



 public function output()

 {

 return implode('', [

 '',

 'Sum of the areas of provided shapes: ',

 $this->sum(),

 '',

 ]);

 }

}

				
			

Para usar a classe AreaCalculator, será necessário criar uma instância da classe, passar uma matriz de formas e exibir o resultado no final da página.

Aqui está um exemplo com uma coleção de três formas:

  • um círculo com um raio de 2
  • um quadrado com um comprimento de 5
  • um segundo quadrado com um comprimento de 6
				
					
$shapes = [

 new Circle(2),

 new Square(5),

 new Square(6),

];



$areas = new AreaCalculator($shapes);



echo $areas->output();

				
			

O problema com o método de saída é que o AreaCalculator manuseia a lógica para gerar os dados.

Considere um cenário onde o resultado deve ser convertido em outro formato, como o JSON.

Toda a lógica seria manuseada pela classe AreaCalculator. Isso violaria o princípio da responsabilidade única. A classe AreaCalculator deve estar preocupada somente com a soma das áreas das formas fornecidas. Ela não deve se importar se o usuário quer JSON ou HTML.

Para resolver isso, crie uma classe separada chamada SumCalculatorOutputter e use essa nova classe para lidar com a lógica necessária para gerar os dados para o usuário:

				
					
class SumCalculatorOutputter

{

 protected $calculator;



 public function __constructor(AreaCalculator $calculator)

 {

 $this->calculator = $calculator;

 }



 public function JSON()

 {

 $data = [

 'sum' => $this->calculator->sum(),

 ];



 return json_encode($data);

 }



 public function HTML()

 {

 return implode('', [

 '',

 'Sum of the areas of provided shapes: ',

 $this->calculator->sum(),

 '',

 ]);

 }

}

				
			

A classe SumCalculatorOutputter funcionaria da seguinte forma:

				
					
$shapes = [

 new Circle(2),

 new Square(5),

 new Square(6),

];



$areas = new AreaCalculator($shapes);

<^>$output = new SumCalculatorOutputter($areas);<^>



<^>echo $output->JSON();<^>

<^>echo $output->HTML();<^>

				
			

Agora, a lógica necessária para gerar os dados para o usuário é manuseada pela classe SumCalculatorOutputter.

Isso satisfaz o princípio da responsabilidade única.

Princípio do aberto-fechado

O Princípio do aberto-fechado (S.R.P.) declara:

> Os objetos ou entidades devem estar abertos para extensão, mas fechados para modificação.

Isso significa que uma classe deve ser extensível sem que seja modificada.

Vamos revisitar a classe AreaCalculator e focar no método sum(soma):

				
					
class AreaCalculator

{

 protected $shapes;



 public function __construct($shapes = [])

 {

 $this->shapes = $shapes;

 }



 public function sum()

 {

 foreach ($this->shapes as $shape) {

 if (is_a($shape, 'Square')) {

 $area[] = pow($shape->length, 2);

 } elseif (is_a($shape, 'Circle')) {

 $area[] = pi() * pow($shape->radius, 2);

 }

 }



 return array_sum($area);

 }

}

				
			

Considere um cenário onde o usuário deseja a sum de formas adicionais, como triângulos, pentágonos, hexágonos, etc. Seria necessário editar constantemente este arquivo e adicionar mais blocos de if/else. Isso violaria o princípio do aberto-fechado.

Uma maneira de tornar esse método sum melhor é remover a lógica para calcular a área de cada forma do método da classe AreaCalculator e anexá-la à classe de cada forma.

Aqui está o método area definido em Square:

				
					
class Square

{

 public $length;



 public function __construct($length)

 {

 $this->length = $length;

 }



 <^>public function area()<^>

 <^>{<^>

 <^>return pow($this->length, 2);<^>

 <^>}<^>

}

				
			

E aqui está o método area definido em Circle:

				
					
class Circle

{

 public $radius;



 public function construct($radius)

 {

 $this->radius = $radius;

 }



 <^>public function area()<^>

 <^>{<^>

 <^>return pi() * pow($shape->radius, 2);<^>

 <^>}<^>

}

				
			

O método sum para AreaCalculator pode então ser reescrito como:

				
					
class AreaCalculator

{

 // ...



 public function sum()

 {

 foreach ($this->shapes as $shape) {

 <^>$area[] = $shape->area();<^>

 }



 return array_sum($area);

 }

}

				
			

Agora, é possível criar outra classe de formas e a passar ao calcular a soma sem quebrar o código.

No entanto, outro problema surge. Como saber que o objeto passado para o AreaCalculator é na verdade uma forma ou se a forma possui um método chamado area?

Programar em uma interface é uma parte integral do SOLID.

Crie uma ShapeInterface que suporte area:

				
					
<^>interface ShapeInterface<^>

<^>{<^>

 <^>public function area();<^>

<^>}<^>

				
			

Modifique suas classes de formas para implement (implementar) a ShapeInterface.

Aqui está a atualização para Square:

				
					
class Square <^>implements ShapeInterface<^>

{

 // ...

}

				
			

E aqui está a atualização para Circle:

				
					
class Circle <^>implements ShapeInterface<^>

{

 // ...

}

				
			

No método sum para AreaCalculator, verifique se as formas fornecidas são na verdade instâncias de ShapeInterface; caso contrário, lance uma exceção:

				
					
 class AreaCalculator

{

 // ...



 public function sum()

 {

 foreach ($this->shapes as $shape) {

 <^>if (is_a($shape, 'ShapeInterface')) {<^>

 $area[] = $shape->area();

 <^>continue;<^>

 <^>}<^>



 <^>throw new AreaCalculatorInvalidShapeException();<^>

 }



 return array_sum($area);

 }

}

				
			

Isso satisfaz o princípio do aberto-fechado.

Princípio da substituição de Liskov

O Princípio da substituição de Liskov declara:

> Seja q(x) uma propriedade demonstrável sobre objetos de x do tipo T. Então q(y) deve ser demonstrável para objetos y do tipo S onde S é um subtipo de T.

Isso significa que cada subclasse ou classe derivada deve ser substituível pela classe sua classe base ou pai.

Analisando novamente a classe de exemplo AreaCalculator, considere uma nova classe VolumeCalculator que estende a classe AreaCalculator:

				
					
class VolumeCalculator extends AreaCalculator

{

 public function construct($shapes = [])

 {

 parent::construct($shapes);

 }



 public function sum()

 {

 // logic to calculate the volumes and then return an array of output

 return [$summedData];

 }

}

				
			

Lembre-se que a classe SumCalculatorOutputter se assemelha a isto:

				
					
class SumCalculatorOutputter {

 protected $calculator;



 public function __constructor(AreaCalculator $calculator) {

 $this->calculator = $calculator;

 }



 public function JSON() {

 $data = array(

 'sum' => $this->calculator->sum();

 );



 return json_encode($data);

 }



 public function HTML() {

 return implode('', array(

 '',

 'Sum of the areas of provided shapes: ',

 $this->calculator->sum(),

 ''

 ));

 }

}

				
			

Se você tentar executar um exemplo como este:

				
					
$areas = new AreaCalculator($shapes);

<^>$volumes = new VolumeCalculator($solidShapes);<^>



$output = new SumCalculatorOutputter($areas);

<^>$output2 = new SumCalculatorOutputter($volumes);<^>

				
			

Quando chamar o método HTML no objeto $output2, você irá obter um erro E_NOTICE informando uma conversão de matriz em string.

Para corrigir isso, em vez de retornar uma matriz do método de soma de classe VolumeCalculator, retorne $summedData:

				
					
class VolumeCalculator extends AreaCalculator

{

 public function construct($shapes = [])

 {

 parent::construct($shapes);

 }



 public function sum()

 {

 // logic to calculate the volumes and then return a value of output

 <^>return $summedData;<^>

 }

}

				
			

O $summedData pode ser um float, duplo ou inteiro.

Isso satisfaz o princípio da substituição de Liskov.

Princípio da segregação de interfaces

O Princípio da segregação de interfaces declara:

> Um cliente nunca deve ser forçado a implementar uma interface que ele não usa, ou os clientes não devem ser forçados a depender de métodos que não usam.

Ainda utilizando o exemplo anterior do ShapeInterface, você precisará suportar as novas formas tridimensionais Cuboid e Spheroid, e essas formas também precisarão ter o volume calculado.

Vamos considerar o que aconteceria se você modificasse a ShapeInterface para adicionar outro contrato:

				
					
interface ShapeInterface

{

 public function area();



 <^>public function volume();<^>

}

				
			

Agora, qualquer forma criada deve implementar o método volume, mas você sabe que os quadrados são formas planas que não têm volume, de modo que essa interface forçaria a classe Square a implementar um método sem utilidade para ela.

Isso violaria o princípio da segregação de interfaces. Ao invés disso, você poderia criar outra interface chamada ThreeDimensionalShapeInterface que possui o contrato volume e as formas tridimensionais poderiam implementar essa interface:

				
					
interface ShapeInterface

{

 public function area();

}



<^>interface ThreeDimensionalShapeInterface<^>

<^>{<^>

 public function volume();

<^>}<^>



class Cuboid implements ShapeInterface, <^>ThreeDimensionalShapeInterface<^>

{

 public function area()

 {

 // calculate the surface area of the cuboid

 }



 public function volume()

 {

 // calculate the volume of the cuboid

 }

}

				
			

Essa é uma abordagem muito mais vantajosa, mas uma armadilha a ser observada é quando sugerir o tipo dessas interfaces. Ao invés de usar uma ShapeInterface ou uma ThreeDimensionalShapeInterface, você pode criar outra interface, talvez ManageShapeInterface, e implementá-la tanto nas formas planas quanto tridimensionais.

Dessa forma, é possível ter uma única API para gerenciar todas as formas:

				
					
<^>interface ManageShapeInterface<^>

<^>{<^>

 <^>public function calculate();<^>

<^>}<^>



class Square implements ShapeInterface, <^>ManageShapeInterface<^>

{

 public function area()

 {

 // calculate the area of the square

 }



 <^>public function calculate()<^>

 <^>{<^>

 <^>return $this->area();<^>

 <^>}<^>

}



class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, <^>ManageShapeInterface<^>

{

 public function area()

 {

 // calculate the surface area of the cuboid

 }



 public function volume()

 {

 // calculate the volume of the cuboid

 }



 <^>public function calculate()<^>

 <^>{<^>

 <^>return $this->area();<^>

 <^>}<^>

}

				
			

Agora, na classe AreaCalculator, substitua a chamada do método area por calculate e verifique se o objeto é uma instância da ManageShapeInterface e não da ShapeInterface.

Isso satisfaz o princípio da segregação de interfaces.

Princípio da inversão de dependência

O princípio da inversão de dependência declara:

> As entidades devem depender de abstrações, não de implementações. Ele declara que o módulo de alto nível não deve depender do módulo de baixo nível, mas devem depender de abstrações.

Esse princípio permite a desestruturação.

Aqui está um exemplo de um PasswordReminder que se conecta a um banco de dados MySQL:

				
					
class MySQLConnection

{

 public function connect()

 {

 // handle the database connection

 return 'Database connection';

 }

}



class PasswordReminder

{

 private $dbConnection;



 public function __construct(MySQLConnection $dbConnection)

 {

 $this->dbConnection = $dbConnection;

 }

}

				
			

Primeiramente, o MySQLConnection é o módulo de baixo nível, enquanto o PasswordReminder é de alto nível. No entanto, de acordo com a definição de D em SOLID, que declara _Dependa de abstrações e não de implementações_, Esse trecho de código acima viola esse princípio, uma vez que a classe PasswordReminder está sendo forçada a depender da classe MySQLConnection.

Mais tarde, se você alterasse o mecanismo do banco de dados, também teria que editar a classe PasswordReminder e isso violaria o _princípio do aberto-fechado_.

A classe PasswordReminder não deve se importar com qual banco de dados seu aplicativo usa. Para resolver esses problemas, programe em uma interface, uma vez que os módulos de alto e baixo nível devem depender de abstrações:

				
					
interface DBConnectionInterface

{

 public function connect();

}

				
			

A interface possui um método de conexão e a classe MySQLConnection implementa essa interface. Além disso, em vez de sugerir o tipo diretamente da classe MySQLConnection no construtor do PasswordReminder, você sugere o tipo de DBConnectionInterface. Sendo assim, independentemente do tipo de banco de dados que seu aplicativo usa, a classe PasswordReminder poderá se conectar ao banco de dados sem problemas e o princípio do aberto-fechado não será violado.

				
					
class MySQLConnection <^>implements DBConnectionInterface<^>

{

 public function connect()

 {

 // handle the database connection

 return 'Database connection';

 }

}



class PasswordReminder

{

 private $dbConnection;



 public function __construct(<^>DBConnectionInterface $dbConnection<^>)

 {

 $this->dbConnection = $dbConnection;

 }

}

				
			

Esse código estabelece que tanto os módulos de alto quanto de baixo nível dependem de abstrações.

Conclusão

Neste artigo, os cinco princípios do Código SOLID foram-lhe apresentados. Projetos que aderem aos princípios SOLID podem ser compartilhados com colaboradores, estendidos, modificados, testados e refatorados com menos complicações.

Continue seu aprendizado lendo sobre outras práticas para o desenvolvimento de software ágil e adaptativo.