DEV Community

Cover image for (O): Aplicando o "Princípio do Aberto/Fechado" com Typescript e Java
Victor Lima Reboredo
Victor Lima Reboredo

Posted on • Edited on

(O): Aplicando o "Princípio do Aberto/Fechado" com Typescript e Java

Conceitos

Abstração

A Abstração nos conceitos da orientação a objeto é uma prática de definir apenas aspectos essenciais que uma classe deve possuir. As classes, devem por natureza, ser incompletas e imprecisas para que possamos modelar especificidades através de classes filhas. Assim surge o conceito de classes filhas, classes mães e herança.

Herança

Herança é a representação de relacionamento entre classes em que uma classe extende a outra de modo a herdar os comportamentos da classe mãe.

SOLID

SOLID é um acrônimo que representa cinco princípios fundamentais da programação orientada a objetos, propostos por Robert C. Martin - o uncle Bob. Aqui você pode ler mais sobre o artigo dele.
Esses princípios têm como objetivo melhorar a estrutura e a manutenção do código, tornando-o mais flexível, escalável e fácil de entender. Tais princípios auxiliam o programador a criar códigos mais organizados, dividindo responsabilidades, reduzindo dependências, simplificando o processo de refatoração e promovendo a reutilização do código.

Open/Closed Principle

O "O" do acrônimo significa "Open/Closed Principle". A frase que o uncle bob utilizou para definir esse princípio foi:

"Uma classe deve estar aberta para extensão, mas fechada para modificação"

Segundo o Princípio do Aberto/Fechado, devemos desenvolver uma aplicação garantindo que escreveremos classes ou módulos de maneira genérica de modo que sempre que sentir a necessidade de estender o comportamento da classe ou do objeto, você não precisará alterar a classe propriamente dita. Extensão aqui pode-se ler como adição ou alteração de procedimentos.

O objetivo é permitir a adição de novas funcionalidades sem a necessidade de alterar o código existente. Isso minimiza o risco de introduzir bugs e deixa o código mais manutenível.

Aplicação prática

Imagine que você tem uma classe DiscountCalculator que calcula descontos de produtos. Inicialmente, temos duas categorias de produtos: Electronics e Clothing. Vamos começar sem aplicar o OCP (Open/Closed Principle):

Java

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

class DiscountCalculator {
    public double calculateDiscount(Product product) {
        if (product.getName().equals("Electronics")) {
            return product.getPrice() * 0.9; // 10% de desconto
        } else if (product.getName().equals("Clothing")) {
            return product.getPrice() * 0.8; // 20% de desconto
        }
        return product.getPrice();
    }
}

public class Main {
    public static void main(String[] args) {
        Product electronics = new Product("Electronics", 100);
        Product clothing = new Product("Clothing", 50);

        DiscountCalculator calculator = new DiscountCalculator();

        System.out.println(calculator.calculateDiscount(electronics)); // 90
        System.out.println(calculator.calculateDiscount(clothing)); // 40
    }
}
Enter fullscreen mode Exit fullscreen mode

Typescript

class Product {
    private _name: string;
    private _price: number;

    constructor(name: string, price: number) {
        this._name = name;
        this._price = price;
    }

    public get name() { return this.name };

    public set name(value: string) { this.name = value };

    public get price() { return this.price };

    public set price(value: number) { this.price = value };
}

class DiscountCalculator {
    public calculateDiscount(product: Product): number {
        if (product.name === 'Electronics') {
            return product.price * 0.9; // 10% de desconto
        } else if (product.name === 'Clothing') {
            return product.price * 0.8; // 20% de desconto
        }
        return product.price;
    }
}

const electronics = new Product('Electronics', 100);
const clothing = new Product('Clothing', 50);

const calculator = new DiscountCalculator();

console.log(calculator.calculateDiscount(electronics)); // 90
console.log(calculator.calculateDiscount(clothing)); // 40
Enter fullscreen mode Exit fullscreen mode

Problemas ao Não Aplicar o OCP

Violação do encapsulamento: Toda vez que um novo tipo de produto precisar de um desconto diferente, será necessário modificar o método calculateDiscount, incluindo uma nova condicional no if.

Dificuldade em manutenção: Se o método crescer com muitos if/else ou switch, ele se tornará difícil de manter e testar.

Risco de introdução de erros: Alterações no método podem introduzir bugs em outras partes do código que dependem desse método.

Como corrigir?

Agora, vamos aplicar o Open/Closed Principle refatorando o código para permitir a adição de novos tipos de descontos sem modificar o código existente.

Java

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

interface DiscountStrategy {
    double calculate(Product product);
}

class ElectronicsDiscount implements DiscountStrategy {
    @Override
    public double calculate(Product product) {
        return product.getPrice() * 0.9; // 10% de desconto
    }
}

class ClothingDiscount implements DiscountStrategy {
    @Override
    public double calculate(Product product) {
        return product.getPrice() * 0.8; // 20% de desconto
    }
}

class NoDiscount implements DiscountStrategy {
    @Override
    public double calculate(Product product) {
        return product.getPrice();
    }
}

class DiscountCalculator {
    private DiscountStrategy discountStrategy;

    public DiscountCalculator(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double calculateDiscount(Product product) {
        return discountStrategy.calculate(product);
    }
}

public class Main {
    public static void main(String[] args) {
        Product electronics = new Product("Electronics", 100);
        Product clothing = new Product("Clothing", 50);
        Product books = new Product("Books", 30);

        DiscountCalculator electronicsDiscount = new DiscountCalculator(new ElectronicsDiscount());
        DiscountCalculator clothingDiscount = new DiscountCalculator(new ClothingDiscount());
        DiscountCalculator booksDiscount = new DiscountCalculator(new NoDiscount());

        System.out.println(electronicsDiscount.calculateDiscount(electronics)); // 90
        System.out.println(clothingDiscount.calculateDiscount(clothing)); // 40
        System.out.println(booksDiscount.calculateDiscount(books)); // 30
    }
}
Enter fullscreen mode Exit fullscreen mode

Typescript

class Product {
    private _name: string;
    private _price: number;

    constructor(name: string, price: number) {
        this._name = name;
        this._price = price;
    }

    public get name() { return this.name };

    public set name(value: string) { this.name = value };

    public get price() { return this.price };

    public set price(value: number) { this.price = value };
}

interface DiscountStrategy {
    calculate(product: Product): number;
}

class ElectronicsDiscount implements DiscountStrategy {
    calculate(product: Product): number {
        return product.price * 0.9; // 10% de desconto
    }
}

class ClothingDiscount implements DiscountStrategy {
    calculate(product: Product): number {
        return product.price * 0.8; // 20% de desconto
    }
}

class NoDiscount implements DiscountStrategy {
    calculate(product: Product): number {
        return product.price;
    }
}

class DiscountCalculator {
    private discountStrategy: DiscountStrategy;

    constructor(discountStrategy: DiscountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public calculateDiscount(product: Product): number {
        return this.discountStrategy.calculate(product);
    }
}

const electronics = new Product('Electronics', 100);
const clothing = new Product('Clothing', 50);
const books = new Product('Books', 30);

const electronicsDiscount = new DiscountCalculator(new ElectronicsDiscount());
const clothingDiscount = new DiscountCalculator(new ClothingDiscount());
const booksDiscount = new DiscountCalculator(new NoDiscount());

console.log(electronicsDiscount.calculateDiscount(electronics)); // 90
console.log(clothingDiscount.calculateDiscount(clothing)); // 40
console.log(booksDiscount.calculateDiscount(books)); // 30
Enter fullscreen mode Exit fullscreen mode

Conclusão

Aplicar o Princípio do Aberto/Fechado é imprescindível se precisamos adicionar novos recursos ou comportamentos sem ter a necessidade de modificar tão profundamente a base de código existente. Na verdade, com o tempo, vemos que é praticamente impossível evitar 100% a mudança da base do código porém é possível sim mitigar a quantidade bruta de código a ser alterado para inserção de uma nova funcionalidade.

Esse princípio torna o código mais adaptável a mudanças, seja para atender novos requisitos ou corrigir erros.

Top comments (0)