By Samuel Oloruntoba
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).
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:
$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.
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.
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.
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.
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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!