Conceptual Article

SOLID: os primeiros 5 princípios do design orientado a objeto

Published on February 20, 2021
    authorauthor

    Samuel Oloruntoba and Bradley Kouchi

    Português
    SOLID: os primeiros 5 princípios do design orientado a objeto

    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.

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the author(s)

    Category:
    Conceptual Article
    Tags:

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    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!

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.