Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Industrial Internet of Things >> Computação em Nuvem

SOLID: Dominando Design Orientado a Objetos para Código Limpo e Manutenível

SOLID é um acrônimo mnemônico para design de classes em programação orientada a objetos. Os princípios instituem práticas que ajudam a desenvolver bons hábitos de programação e código manutenível.

Ao considerar manutenção e extensibilidade de código a longo prazo, os princípios SOLID enriquecem o ambiente de desenvolvimento de código Ágil. Contabilizar e otimizar dependências de código ajuda a criar um ciclo de vida de desenvolvimento de software mais direto e organizado.

O Que São Princípios SOLID?

SOLID representa um conjunto de princípios para design de classes. Robert C. Martin (Uncle Bob) introduziu a maioria dos princípios de design e cunhou o acrônimo.
SOLID significa:

Os princípios SOLID representam uma coleção de melhores práticas para design de software. Cada ideia representa um framework de design, levando a melhores hábitos de programação, design de código aprimorado e menos erros.

SOLID: 5 Princípios Explicados

A melhor maneira de entender como os princípios SOLID funcionam é através de exemplos. Todos os princípios são complementares e se aplicam a casos de uso individuais. A ordem em que os princípios são aplicados é irrelevante, e nem todos os princípios são aplicáveis em todas as situações.

Cada seção abaixo fornece uma visão geral de cada princípio SOLID na linguagem de programação Python. As ideias gerais do SOLID se aplicam a qualquer linguagem orientada a objetos, como PHP, Java ou C#. Generalizar as regras as torna aplicáveis a abordagens modernas de programação, como microsserviços.

Princípio da Responsabilidade Única (SRP)

O princípio da responsabilidade única (SRP) afirma: "Nunca deve haver mais de um motivo para uma classe mudar."

Ao mudar uma classe, devemos mudar apenas uma única funcionalidade, o que implica que todo objeto deve ter apenas um trabalho.

Como exemplo, observe a seguinte classe:

# Uma classe com múltiplas responsabilidades
class Animal:
    # Construtor de propriedade
    def __init__(self, name):
        self.name = name
    
    # Representação de propriedade
    def __repr__(self):
        return f'Animal(name="{self.name}")'
    
    # Gerenciamento de banco de dados
    def save(animal):
        print(f'Saved {animal} to the database')

if __name__ == '__main__':
    # Instanciação de propriedade
    a = Animal('Cat')
    # Salvando propriedade no banco de dados
    Animal.save(a)

Ao fazer qualquer mudança no método save(), a mudança acontece na classe Animal. Ao fazer mudanças de propriedade, as modificações também ocorrem na classe Animal.

A classe tem dois motivos para mudar e viola o princípio da responsabilidade única. Mesmo que o código funcione como esperado, não respeitar o princípio de design torna o código mais difícil de gerenciar a longo prazo.

Para implementar o princípio da responsabilidade única, note que a classe exemplo tem dois trabalhos distintos:

Portanto, a melhor maneira de resolver o problema é separar o método de gerenciamento de banco de dados em uma nova classe. Por exemplo:

# Uma classe responsável por gerenciamento de propriedade
class Animal:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f'Animal(name="{self.name}")'

# Uma classe responsável por gerenciamento de banco de dados
class AnimalDB:
    def save(self, animal):
        print(f'Saved {animal} to the database')

if __name__ == '__main__':
    # Instanciação de propriedade
    a = Animal('Cat')
    # Instanciação de banco de dados
    db = AnimalDB()
    # Salvando propriedade no banco de dados
    db.save(a)

Mudar a classe AnimalDB não afeta a classe Animal com o princípio da responsabilidade única aplicado. O código é intuitivo e fácil de modificar.

Princípio Aberto-Fechado (OCP)

O princípio aberto-fechado (OCP) afirma: "Entidades de software devem ser abertas para extensão, mas fechadas para modificação."

Adicionar funcionalidades e casos de uso ao sistema não deve requerer modificar entidades existentes. A formulação parece contraditória — adicionar novas funcionalidades requer mudar código existente.

A ideia é simples de entender através do seguinte exemplo:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f'Animal(name="{self.name}")'

class Storage:
    def save_to_db(self, animal):
        print(f'Saved {animal} to the database')

A classe Storage salva as informações de uma instância Animal em um banco de dados. Adicionar novas funcionalidades, como salvar em arquivo CSV, requer adicionar código à classe Storage:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f'Animal(name="{self.name}")'

class Storage:
    def save_to_db(self, animal):
        print(f'Saved {animal} to the database')
    
    def save_to_csv(self, animal):
        print(f'Saved {animal} to the CSV file')

O método save_to_csv modifica uma classe Storage existente para adicionar a funcionalidade. Esta abordagem viola o princípio aberto-fechado ao mudar um elemento existente quando uma nova funcionalidade aparece.

O código requer remover a classe Storage de propósito geral e criar classes individuais para armazenamento em formatos de arquivo específicos.

O seguinte código demonstra a aplicação do princípio aberto-fechado:

class DB():
    def save(self, animal):
        print(f'Saved {animal} to the database')

class CSV():
    def save(self, animal):
        print(f'Saved {animal} to a CSV file')

O código cumpre o princípio aberto-fechado. O código completo agora parece assim:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f'"{self.name}"'

class DB():
    def save(self, animal):
        print(f'Saved {animal} to the database')

class CSV():
    def save(self, animal):
        print(f'Saved {animal} to a CSV file')

if __name__ == '__main__':
    a = Animal('Cat')
    db = DB()
    csv = CSV()
    db.save(a)
    csv.save(a)

Estender com funcionalidades adicionais (como salvar em arquivo XML) não modifica classes existentes.

Princípio da Substituição de Liskov (LSP)

O princípio da substituição de Liskov (LSP) afirma: "Funções que usam ponteiros ou referências para classes base devem ser capazes de usar objetos de classes derivadas sem saber disso."

O princípio afirma que uma classe pai pode substituir uma classe filha sem quaisquer mudanças perceptíveis na funcionalidade.

Confira o exemplo de escrita de arquivo abaixo:

# Classe pai
class FileHandling():
    def write_db(self):
        return f'Handling DB'
    
    def write_csv(self):
        return f'Handling CSV'

# Classes filhas
class WriteDB(FileHandling):
    def write_db(self):
        return f'Writing to a DB'
    
    def write_csv(self):
        return f"Error: Can't write to CSV, wrong file type."

class WriteCSV(FileHandling):
    def write_csv(self):
        return f'Writing to a CSV file'
    
    def write_db(self):
        return f"Error: Can't write to DB, wrong file type."

if __name__ == "__main__":
    # Instanciação da classe pai e chamadas de função
    db = FileHandling()
    csv = FileHandling()
    print(db.write_db())
    print(db.write_csv())
    
    # Instanciações das classes filhas e chamadas de função
    db = WriteDB()
    csv = WriteCSV()
    print(db.write_db())
    print(db.write_csv())
    print(csv.write_db())
    print(csv.write_csv())

A classe pai (FileHandling) consiste em dois métodos para escrita em banco de dados e arquivo CSV. A classe lida com ambas as funções e retorna uma mensagem.

As duas classes filhas (WriteDB e WriteCSV) herdam propriedades da classe pai (FileHandling). No entanto, ambas as filhas lançam um erro ao tentar usar a função de escrita inadequada, o que viola o princípio da substituição de Liskov, já que as funções de sobrescrita não correspondem às funções pai.

O seguinte código resolve os problemas:

# Classe pai
class FileHandling():
    def write(self):
        return f'Handling file'

# Classes filhas
class WriteDB(FileHandling):
    def write(self):
        return f'Writing to a DB'

class WriteCSV(FileHandling):
    def write(self):
        return f'Writing to a CSV file'

if __name__ == "__main__":
    # Instanciação da classe pai e chamadas de função
    db = FileHandling()
    csv = FileHandling()
    print(db.write())
    print(csv.write())
    
    # Instanciações das classes filhas e chamadas de função
    db = WriteDB()
    csv = WriteCSV()
    print(db.write())
    print(csv.write())

As classes filhas agora correspondem corretamente à função pai.

Princípio da Segregação de Interface (ISP)

O princípio da segregação de interface (ISP) afirma: "Muitas interfaces específicas do cliente são melhores do que uma interface de propósito geral."

Em outras palavras, interfaces de interação mais extensas são divididas em menores. O princípio garante que classes usem apenas os métodos que precisam, reduzindo redundância geral.

O exemplo a seguir demonstra uma interface de propósito geral:

class Animal():
    def walk(self):
        pass
    
    def swim(self):
        pass

class Cat(Animal):
    def walk(self):
        print("Struts")
    
    def fly(self):
        raise Exception("Cats don't swim")

class Duck(Animal):
    def walk(self):
        print("Waddles")
    
    def swim(self):
        print("Floats")

As classes filhas herdam da classe pai Animal, que contém métodos walk e fly. Embora ambas as funções sejam aceitáveis para certos animais, alguns animais têm funcionalidades redundantes.

Para lidar com a situação, divida a interface em seções menores. Por exemplo:

class Walk():
    def walk(self):
        pass

class Swim(Walk):
    def swim(self):
        pass

class Cat(Walk):
    def walk(self):
        print("Struts")

class Duck(Swim):
    def walk(self):
        print("Waddles")
    
    def swim(self):
        print("Floats")

A classe Fly herda da Walk, fornecendo funcionalidade adicional para classes filhas apropriadas. O exemplo satisfaz o princípio da segregação de interface.
Adicionar outro animal, como um peixe, requer atomizar a interface ainda mais, já que peixes não podem andar.

Princípio da Inversão de Dependência (DIP)

O princípio da inversão de dependência afirma: "Dependa de abstrações, não de concreções."

O princípio visa reduzir conexões entre classes adicionando uma camada de abstração. Mover dependências para abstrações torna o código robusto.

O exemplo a seguir demonstra dependência de classe sem uma camada de abstração:

class LatinConverter:
    def latin(self, name):
        print(f'{name} = "Felis catus"')
        return "Felis catus"

class Converter:
    def start(self):
        converter = LatinConverter()
        converter.latin('Cat')

if __name__ == '__main__':
    converter = Converter()
    converter.start()

O exemplo tem duas classes:

O princípio da inversão de dependência requer adicionar uma camada de interface de abstração entre as duas classes.

Uma solução de exemplo parece a seguinte:

from abc import ABC

class NameConverter(ABC):
    def convert(self, name):
        pass

class LatinConverter(NameConverter):
    def convert(self, name):
        print('Converting using Latin API')
        print(f'{name} = "Felis catus"')
        return "Felis catus"

class Converter:
    def __init__(self, converter: NameConverter):
        self.converter = converter
    
    def start(self):
        self.converter.convert('Cat')

if __name__ == '__main__':
    latin = LatinConverter()
    converter = Converter(latin)
    converter.start()

A classe Converter agora depende da interface NameConverter em vez de depender diretamente do LatinConverter. Atualizações futuras permitem definir conversões de nome usando uma linguagem e API diferentes através da interface NameConverter.

Por Que Existe Necessidade dos Princípios SOLID?

Os princípios SOLID ajudam a combater problemas de padrões de design. O objetivo geral dos princípios SOLID é reduzir dependências de código, e adicionar um novo recurso ou mudar uma parte do código não quebra todo o build.

Como resultado da aplicação dos princípios SOLID ao design orientado a objetos, o código se torna mais fácil de entender, gerenciar, manter e mudar. Como as regras são mais adequadas para projetos grandes, aplicar os princípios SOLID aumenta a velocidade e eficiência geral do ciclo de vida de desenvolvimento.

Os Princípios SOLID Ainda São Relevantes?

Embora os princípios SOLID tenham mais de 20 anos, eles ainda fornecem uma boa base para design de arquitetura de software. SOLID fornece princípios de design sólidos aplicáveis a programas e ambientes modernos, não apenas programação orientada a objetos.

Princípios SOLID se aplicam em situações onde código é escrito e modificado por pessoas, organizado em módulos, e contém elementos internos ou externos.

Conclusão

Os princípios SOLID ajudam a fornecer um bom framework e guia para design de arquitetura de software. Os exemplos deste guia mostram que até uma linguagem tipada dinamicamente como Python se beneficia da aplicação dos princípios ao design de código.

Em seguida, leia sobre os 9 princípios DevOps que ajudarão sua equipe a tirar o máximo proveito do DevOps.

Computação em Nuvem

  1. Palavra-chave Static em C#: Dominando Variáveis, Métodos e Classes Estáticas
  2. Entendendo Classes Aninhadas em C#: Definição, Uso e Herança
  3. Dominando Templates de Classe em C++: Um Guia Prático
  4. Dominando Classes Internas Anônimas em Java: Definição, Sintaxe e Exemplos Práticos
  5. Dominando Java ObjectOutputStream: Serialização, Métodos e Exemplos Práticos
  6. Dominando Java Generics – Construindo Código Reutilizável e Type-Safe
  7. Dominando Operações de Arquivo em Java com java.io – Criação, Leitura, Escrita e Exclusão
  8. 5 Princípios de Design para Interconexões Robustas em Aplicações de Dados Intensivos de Alta Velocidade
  9. Dominando Herança em C#: Construa Código Reutilizável e Manutenível
  10. Polimorfismo em C#: Binding Estático vs Dinâmico Explicado