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:
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
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.