DEV Community

Higor Anjos
Higor Anjos

Posted on

Introdução a Programação Orientada a Objetos com C#

A Programação Orientada a Objetos (POO) é um paradigma de programação amplamente utilizado para o desenvolvimento de sistemas robustos, organizados e reutilizáveis. Esse paradigma busca aproximar a modelagem do software com o mundo real, representando conceitos e comportamentos através de objetos.

A POO é baseada em quatro pilares fundamentais:

  • Abstração: Permite representar entidades do mundo real com seus atributos e comportamentos essenciais, omitindo detalhes desnecessários.

  • Encapsulamento: Garante a proteção dos dados, restringindo o acesso direto aos atributos e controlando as interações por meio de métodos.

  • Herança: Possibilita criar novas classes baseadas em classes já existentes, promovendo a reutilização de código e a organização hierárquica.

  • Polimorfismo: Permite que métodos com o mesmo nome se comportem de maneiras diferentes conforme o contexto.

Linguagens de programação como Java, C#, Python e C++ são fortemente orientadas a objetos, oferecendo recursos para implementar esses conceitos de forma eficiente. A POO também facilita a manutenção e escalabilidade de sistemas, tornando o código mais modular e flexível.

1.1 Classes

Uma classe é uma estrutura que define as características (atributos) e comportamentos (métodos) de um objeto. Ela funciona como um molde para criar objetos, especificando quais dados e funcionalidades estarão disponíveis. Por exemplo, a classe Person pode ter atributos como Name, Age e CPF, e métodos como Walk() e Talk().

public class Person {
    public string Name;
    public int Age;

    public void Walk() {
        Console.WriteLine(Name + " is walking.");
    }

    public void Talk() {
        Console.WriteLine(Name + " is talking.");
    }
}

Enter fullscreen mode Exit fullscreen mode

1.2 Objetos

Um objeto é uma instância de uma classe, ou seja, é a materialização de uma classe na memória. Cada objeto pode ter valores diferentes para seus atributos, mas compartilha os mesmos métodos definidos pela classe.

Person person1 = new Person();
person1.Name = "Alice";
person1.Age = 30;

Person person2 = new Person();
person2.Name = "Bob";
person2.Age = 25;

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, person1 representa uma pessoa chamada Alice com 30 anos, enquanto person2 representa Bob com 25 anos. Ambos são objetos distintos da classe Person.

1.3 Instanciação de Objetos

Instanciar um objeto é criar uma instância de uma classe na memória usando o operador new em C#.

Exemplo:

Person person1 = new Person();

Enter fullscreen mode Exit fullscreen mode

1.4 Comparação entre Objetos (Operador ==)

Ao comparar objetos em Programação Orientada a Objetos (POO), é importante entender como o operador == funciona.

  • Operador ==: O operador == compara se dois objetos referenciam o mesmo local na memória. Ou seja, mesmo que dois objetos tenham atributos com os mesmos valores, eles não serão considerados iguais se não apontarem para a mesma referência. Ele verifica diretamente se ambos apontam para o mesmo local na memória.

Exemplo:

using System;
public class Person {
    private int id;

    public Person(int id) => this.id = id;
}

public class Program {
    public static void Main() {
        var person1 = new Person(1);
        var person2 = new Person(1);
        var person3 = person1;

        Console.WriteLine(person1 == person2);  // False
        Console.WriteLine(person1 == person3);  // True
    }
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, mesmo que person1 e person2 sejam criados com o mesmo valor de id, a comparação retorna false porque ambos são objetos distintos na memória. Já person1 e person3 apontam para o mesmo local na memória, por isso a comparação retorna true.

1.5 Atributos

Atributos são variáveis declaradas dentro de uma classe que armazenam o estado de um objeto. Exemplo:

public class Person {
    public string Name;
    public int Age;
}

Enter fullscreen mode Exit fullscreen mode

1.5.1 Operadores de Acesso (Getters e Setters)

Getters e Setters são métodos usados para acessar e modificar atributos privados de uma classe, respeitando o princípio de encapsulamento.

public class Person {
    private string name;

    public string Name {
        get { return name; }
        set { name = value; }
    }
}

Enter fullscreen mode Exit fullscreen mode

1.5.2 Palavra-chave this

A palavra-chave this é usada para referenciar o objeto atual da classe, evitando ambiguidade entre atributos e parâmetros de métodos.

public Person(string name) {
    this.name = name;
}
Enter fullscreen mode Exit fullscreen mode

1.6 Métodos

Métodos representam comportamentos ou ações que um objeto pode executar.

1.6.1 Construtores e Destrutor (Constructor and Destructor)

  • Construtores (Constructors): São métodos especiais utilizados para inicializar uma classe. O nome do construtor deve ser o mesmo da classe e não possui tipo de retorno. O construtor é chamado automaticamente no momento em que o objeto é instanciado.
public class Person {
    private int[] data;

    public Person() {
        data = new int[100];
        Console.WriteLine("Memory allocated.");
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Destrutor (Destructor/Finalizer): É um método especial usado para liberar recursos quando um objeto não é mais necessário. Em C#, o destrutor tem o mesmo nome da classe precedido pelo símbolo ~. O destrutor é chamado automaticamente pelo coletor de lixo (Garbage Collector).
public class Person {
    private int[] data;

    public Person() {
        data = new int[100];
        Console.WriteLine("Memory allocated.");
    }

    ~Person() {
        data = null;
        Console.WriteLine("Memory deallocated.");
    }
}
Enter fullscreen mode Exit fullscreen mode

1.7 Modificadores de Acesso

Controlam a visibilidade de classes, atributos e métodos.

1.7.1 Público, Privado e Protegido (public, private, protected)

  • public (Público): Permite acesso irrestrito a partir de qualquer parte do código.
public class Person {
    public string Name;
}

Person person = new Person();
person.Name = "Alice"; // Acesso permitido
Enter fullscreen mode Exit fullscreen mode
  • private (Privado): Restringe o acesso apenas à própria classe.
public class Person {
    private int age;

    public void SetAge(int value) {
        age = value;
    }

    public int GetAge() {
        return age;
    }
}

Person person = new Person();
person.SetAge(30); // Acesso permitido
person.age = 30; // ERRO: 'age' é privado
Enter fullscreen mode Exit fullscreen mode
  • protected (Protegido): Permite o acesso dentro da própria classe e de subclasses. (Essa técnica utiliza herança, que será abordada em detalhes mais adiante.)
public class Person {
    protected string Address;
}

public class Employee : Person {
    public void SetAddress(string address) {
        Address = address; // Acesso permitido na subclasse
    }
}

Employee employee = new Employee();
employee.SetAddress("123 Main St"); // Acesso indireto permitido
employee.Address = "123 Main St"; // ERRO: 'Address' é protegido
Enter fullscreen mode Exit fullscreen mode

2. PILARES DA PROGRAMAÇÃO ORIENTADA A OBJETOS

Agora que já temos familiaridade com as ferramentas, vamos explorar os pilares que fortalecem a Programação Orientada a Objetos (POO) e entender como utilizá-los dentro de sua filosofia e paradigma.

2.1 Encapsulamento

O encapsulamento consiste em ocultar os detalhes internos de uma classe e expor apenas o necessário por meio de interfaces bem definidas. Esse conceito impede o acesso direto aos atributos de um objeto, permitindo que os dados sejam manipulados de forma segura e controlada.

Exemplo:

public class Person {
    private string name;
    private int age;

    public string GetName() {
        return name;
    }

    public void SetName(string newName) {
        if (!string.IsNullOrEmpty(newName)) {
            name = newName;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, os atributos name e age estão protegidos e só podem ser acessados ou modificados através dos métodos GetName() e SetName().

2.2 Herança

A herança permite criar novas classes baseadas em classes existentes, facilitando a reutilização de código e a organização hierárquica.

Exemplo:

public class Person {
    public string Name;
    public int Age;
}

public class Student : Person {
    public string School;
}

Enter fullscreen mode Exit fullscreen mode

Aqui, a classe Student herda atributos da classe Person e adiciona o atributo School.

2.2.1 Reutilização de Código e Acesso à Superclasse com base

A palavra-chave base permite acessar membros da classe base dentro da classe derivada, promovendo a reutilização de código. Em vez de replicar lógica ou inicialização da superclasse, utilizamos base para invocar diretamente os métodos ou construtores já definidos. Em C#, o base é usado para chamar métodos, construtores ou acessar membros da classe pai (superclasse). Esse conceito pode variar de acordo com a linguagem de programação. Por exemplo, linguagens como Java utilizam a palavra-chave super, enquanto em Python o método equivalente é super(). Independentemente da linguagem, o paradigma de Programação Orientada a Objetos (POO) sempre fornece uma maneira de reutilizar código e acessar a funcionalidade da superclasse.

Exemplo em C#:

public class Person {
    public Person(string name) {
        Console.WriteLine("Person: " + name);
    }
}

public class Student : Person {
    public Student(string name) : base(name) {
        Console.WriteLine("Student: " + name);
    }
}
Enter fullscreen mode Exit fullscreen mode

2.2.2 Classes Abstratas

Classes abstratas servem como base para outras classes e não podem ser instanciadas. Elas são úteis quando você deseja definir um contrato (métodos que devem ser implementados pelas subclasses) e também compartilhar um comportamento comum entre elas.

Exemplo:

public abstract class Person {
    public string Name { get; set; }

    public void Greet() {
        Console.WriteLine($"Hello, my name is {Name}.");
    }

    public abstract void Introduce();
}

public class Student : Person {
    public override void Introduce() {
        Console.WriteLine("I am a student.");
    }
}

public class Teacher : Person {
    public override void Introduce() {
        Console.WriteLine("I am a teacher.");
    }
}

// Uso:
Student student = new Student { Name = "Alice" };
student.Greet(); // Comportamento comum
student.Introduce(); // Comportamento específico

Teacher teacher = new Teacher { Name = "Mr. Smith" };
teacher.Greet(); // Comportamento comum
teacher.Introduce(); // Comportamento específico
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, a classe abstrata Person fornece um método comum, Greet(), que pode ser usado por todas as subclasses. As subclasses, como Student e Teacher, implementam o método abstrato Introduce() de maneiras específicas.

2.2.3 Interfaces

Interfaces definem contratos que as classes devem implementar. Diferentemente das classes abstratas, as interfaces não podem conter implementações de métodos (exceto em algumas linguagens que permitem métodos com implementação padrão, como as interfaces no C# 8.0 e posteriores). Interfaces são úteis quando você deseja garantir que diferentes classes sigam um mesmo conjunto de regras, mas não compartilhem comportamento.

Exemplo:

public interface IPerson {
    void Introduce();
}

public class Employee : IPerson {
    public void Introduce() {
        Console.WriteLine("I am an employee.");
    }
}

public class Manager : IPerson {
    public void Introduce() {
        Console.WriteLine("I am a manager.");
    }
}

// Uso:
IPerson employee = new Employee();
employee.Introduce(); // Saída: I am an employee.

IPerson manager = new Manager();
manager.Introduce(); // Saída: I am a manager.
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, tanto Employee quanto Manager implementam a interface IPerson e fornecem suas próprias versões do método Introduce. Isso permite que objetos diferentes sejam tratados de forma uniforme quando acessados por meio da interface.

2.3 Polimorfismo

O polimorfismo permite que objetos de diferentes classes respondam de maneira distinta a uma mesma chamada de método.

Exemplo:

public class Person {
    public virtual void Introduce() {
        Console.WriteLine("I am a person.");
    }
}

public class Student : Person {
    public override void Introduce() {
        Console.WriteLine("I am a student.");
    }
}

public class Employee : Person {
    public override void Introduce() {
        Console.WriteLine("I am an employee.");
    }
}

Enter fullscreen mode Exit fullscreen mode

2.3.1 Sobrescrita de Métodos (Overriding)

Permite redefinir um método da classe base em uma classe derivada, adaptando seu comportamento.

Exemplo:

public class Person {
    public virtual void Introduce() {
        Console.WriteLine("Hello, I am a person.");
    }
}

public class Student : Person {
    public override void Introduce() {
        Console.WriteLine("Hello, I am a student.");
    }
}

public class Employee : Person {
    public override void Introduce() {
        Console.WriteLine("Hello, I am an employee.");
    }
}

Enter fullscreen mode Exit fullscreen mode

Exemplo de uso:

Person person = new Person();
person.Introduce();  // Saída: Hello, I am a person.

Person student = new Student();
student.Introduce();  // Saída: Hello, I am a student.

Person employee = new Employee();
employee.Introduce();  // Saída: Hello, I am an employee.

Enter fullscreen mode Exit fullscreen mode

3. RELACIONAMENTO ENTRE CLASSES

Os relacionamentos entre classes são fundamentais para modelar interações e dependências entre objetos em sistemas orientados a objetos. Vamos explorar os principais tipos de relacionamentos: associação, agregação e dependência.

3.1 Associação Unidirecional

A associação unidirecional ocorre quando uma classe conhece outra, mas essa relação não é recíproca.

Exemplo:

public class Person {
    public string Name;
}

public class Car {
    public string Model;
    public Person Owner;
}

// Uso
Person person = new Person { Name = "Alice" };
Car car = new Car { Model = "Sedan", Owner = person };

Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, um objeto Car está associado a um objeto Person como seu proprietário.

3.2 Associação Bidirecional

A associação bidirecional ocorre quando ambas as classes conhecem e acessam os dados uma da outra, permitindo interação mútua.

Na associação bidirecional, ambas as classes conhecem a existência uma da outra.

Exemplo:

public class Person {
    public string Name;
    public Car Car;
}

public class Car {
    public string Model;
    public Person Owner;
}

Person person = new Person() { Name = "Alice" };
Car car = new Car() { Model = "Sedan", Owner = person };
person.Car = car;

Enter fullscreen mode Exit fullscreen mode

Aqui, tanto a Person conhece seu Car quanto o Car conhece seu Owner.

3.3 Agregação

A agregação representa uma relação onde uma classe contém outra, mas ambas podem existir independentemente. A classe "todo" tem uma referência para a classe "parte", porém a destruição do objeto "todo" não afeta a existência do objeto "parte".

Exemplo:

public class Person {
    public string Name;
}

public class Department {
    public string Name;
    public List<Person> Employees = new List<Person>();
}

Person person = new Person() { Name = "Alice" };
Department dept = new Department() { Name = "IT" };
dept.Employees.Add(person);
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, o Department contém uma lista de Person como empregados. Se o objeto Department for destruído, o objeto Person ainda existirá independentemente.

3.4 Composição

A composição representa um relacionamento "todo/parte" mais restrito, onde a parte não pode existir sem o todo. Se o objeto "todo" for destruído, todas as suas partes também serão destruídas. Essa relação é frequentemente implementada com objetos sendo criados e gerenciados exclusivamente pelo "todo".

Exemplo:

public class Person {
    public string Name;
}

public class House {
    private List<Person> residents = new List<Person>();

    public House() {
        residents.Add(new Person() { Name = "Alice" });
        residents.Add(new Person() { Name = "Bob" });
    }

    public void ListResidents() {
        foreach (var resident in residents) {
            Console.WriteLine(resident.Name);
        }
    }
}

House house = new House();
house.ListResidents(); // Outputs: Alice, Bob
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, os objetos Person são criados dentro do objeto House. Se o objeto House for destruído, os objetos Person associados também serão destruídos, já que não existem fora do contexto da House.

4. CONCEITOS COMPLEMENTARES

Além dos pilares e relacionamentos, existem conceitos avançados que enriquecem a Programação Orientada a Objetos (POO).

4.1 Herança Múltipla (via Interfaces)

C# não permite herança múltipla de classes para evitar conflitos e ambiguidade. No entanto, é possível implementar múltiplas interfaces, permitindo que uma classe combine contratos de diferentes fontes.

A herança múltipla via interfaces é útil para atribuir diferentes responsabilidades a uma classe, permitindo maior flexibilidade e modularidade no design de software, sem criar dependências fortes entre os componentes.

Exemplo: Uma Pessoa com Duas Responsabilidades

No exemplo abaixo, a classe Person implementa duas interfaces: IWorker e IStudent, que representam contratos para diferentes responsabilidades.

public interface IWorker {
    void Work();
}

public interface IStudent {
    void Study();
}

public class Person : IWorker, IStudent {
    public void Work() {
        Console.WriteLine("Working...");
    }

    public void Study() {
        Console.WriteLine("Studying...");
    }
}

// Uso:
Person person = new Person();
person.Work(); // Saída: Working...
person.Study(); // Saída: Studying...
Enter fullscreen mode Exit fullscreen mode

4.2 Métodos e Classes Genéricas

Métodos e classes genéricas permitem criar estruturas de código que funcionam com qualquer tipo de dado, promovendo reutilização e flexibilidade dos tipos. Esses conceitos são amplamente usados em bibliotecas e frameworks para lidar com diferentes tipos de dados de maneira uniforme, além de incentivar o princípio DRY (Don't Repeat Yourself), pois evitam a duplicação de lógica para diversos tipos.

Exemplo de Classe Genérica

public class Box<T> {
    public T Content;
}

// Uso:
Box<int> intBox = new Box<int>();
intBox.Content = 10;

Box<string> stringBox = new Box<string>();
stringBox.Content = "Hello";

Console.WriteLine(intBox.Content);    // Saída: 10
Console.WriteLine(stringBox.Content); // Saída: Hello
Enter fullscreen mode Exit fullscreen mode

Exemplo de Método Genérico

public class Utils {
    public static void Display<T>(T item) {
        Console.WriteLine(item);
    }
}

// Uso:
Utils.Display<int>(42);        // Saída: 42
Utils.Display<string>("Text"); // Saída: Text
Utils.Display<double>(3.14);   // Saída: 3.14
Enter fullscreen mode Exit fullscreen mode

Ao adotar classes e métodos genéricas, o código se torna mais modular e seguro em tempo de compilação. Isso contribui para o desenvolvimento de aplicações orientadas a objetos mais robustas, que lidam de forma eficiente com diversos tipos de dados em cenários variados.

4.3 Tratamento de Exceções em POO

O tratamento de exceções é uma técnica essencial em Programação Orientada a Objetos (POO) para capturar e lidar com erros durante a execução do programa, evitando falhas inesperadas e proporcionando uma experiência mais robusta para o usuário. No entanto, é importante ressaltar que exceções têm um custo computacional elevado e não devem ser usadas como fluxo de controle normal (conforme recomendado pela documentação da Microsoft).

Estrutura do Tratamento de Exceções

Em linguagens como C#, o tratamento de exceções é implementado com as palavras-chave try, catch e finally:

  • try: Define um bloco de código onde podem ocorrer exceções.
  • catch: Captura e trata exceções específicas ou gerais que ocorram no bloco try.
  • finally: (Opcional) Executa um bloco de código, independentemente de ter ocorrido ou não uma exceção. Usado frequentemente para liberar recursos.

Exemplo Básico

try {
    int result = 10 / 0; // Lança uma exceção DivideByZeroException
} catch (DivideByZeroException ex) {
    Console.WriteLine("Error: " + ex.Message);
} finally {
    Console.WriteLine("Operation finished.");
}
Enter fullscreen mode Exit fullscreen mode

Exemplo Avançado com Várias Exceções

try {
    string input = null;
    Console.WriteLine(input.Length); // Lança NullReferenceException
} catch (NullReferenceException ex) {
    Console.WriteLine("Null reference error: " + ex.Message);
} catch (Exception ex) {
    Console.WriteLine("General error: " + ex.Message);
} finally {
    Console.WriteLine("Cleanup complete.");
}
Enter fullscreen mode Exit fullscreen mode

Custo Computacional e Boas Práticas

  1. Use exceções para condições verdadeiramente excepcionais: Exceções devem indicar problemas imprevistos ou cenários raros. Evite usá-las como parte do fluxo normal do programa.
  2. Não use exceções para controle de fluxo: Além de prejudicar a legibilidade do código, lançar e capturar exceções repetidamente pode ocasionar alto consumo de recursos e impactos significativos no desempenho.
  3. Capture exceções específicas: Sempre que possível, use exceções específicas em vez de capturar exceções genéricas (Exception). Dessa forma, o tratamento fica mais claro e eficiente.
  4. Trate corretamente os recursos no bloco finally: Liberar conexões, fechar arquivos ou desfazer transações de forma adequada evita vazamento de recursos.

Benefícios do Tratamento de Exceções

  • Robustez: Reduz a probabilidade de o programa falhar inesperadamente.
  • Manutenção: Facilita a identificação e correção de erros.
  • Controle: Permite responder adequadamente a diferentes tipos de erros em tempo de execução.

O tratamento de exceções, embora indispensável, deve ser usado com parcimônia. A regra geral é não lançar exceções se for possível lidar com o cenário através de validações ou verificações prévias. Em um contexto de aplicações de alto desempenho — como em ASP.NET Core — o uso abusivo de exceções pode acarretar alto custo e diminuição de escalabilidade, conforme destacado nas boas práticas de desenvolvimento da Microsoft.

4.4 Reflexão (Reflection) – Inspeção e Manipulação em Tempo de Execução

Reflexão (Reflection) é um mecanismo poderoso que permite inspecionar, criar e modificar instâncias de classes em tempo de execução, bem como acessar e invocar seus membros (propriedades, métodos, construtores, etc.). É amplamente usada em cenários onde o código precisa ser dinâmico, como em frameworks, bibliotecas de serialização ou ferramentas de análise e geração de código.

Pontos Importantes

  • Flexibilidade: Permite descobrir tipos e membros em tempo de execução, criando soluções mais genéricas ou plugáveis.
  • Alto Custo: Reflexão pode ser mais lenta e complexa, exigindo cuidado com a performance.
  • Risco de Segurança: O acesso a detalhes internos pode expor áreas sensíveis do código se não houver controle adequado.
  • Uso Restrito: Geralmente, use apenas quando a flexibilidade dinâmica é essencial e não há alternativa mais simples.

Exemplo: Inspeção e Manipulação com Reflection

using System;
using System.Reflection;

public class Person {
    public string Name { get; set; }
    public int Age { get; set; }

    public void Speak() {
        Console.WriteLine($"Hello, my name is {Name}.");
    }
}

class Program {
    static void Main() {
        // Obtendo o tipo da classe Person
        Type type = typeof(Person);
        Console.WriteLine("Class: " + type.Name);

        // Listando propriedades da classe
        foreach (PropertyInfo prop in type.GetProperties()) {
            Console.WriteLine("Property: " + prop.Name);
        }

        // Criando uma instância da classe Person dinamicamente
        object obj = Activator.CreateInstance(type);

        // Definindo valor na propriedade 'Name'
        PropertyInfo nameProp = type.GetProperty("Name");
        nameProp.SetValue(obj, "Alice");
        Console.WriteLine("Name set via Reflection: " + nameProp.GetValue(obj));

        // Invocando o método 'Speak' dinamicamente
        MethodInfo method = type.GetMethod("Speak");
        method.Invoke(obj, null);
    }
}
Enter fullscreen mode Exit fullscreen mode

Saída:

Class: Person
Property: Name
Property: Age
Name set via Reflection: Alice
Hello, my name is Alice.
Enter fullscreen mode Exit fullscreen mode

Observação

Embora extremamente poderosa, a Reflexão deve ser usada apenas quando realmente necessária, pois pode prejudicar a legibilidade e o desempenho do seu código. Em cenários comuns, a abordagem mais simples e estatística é preferível.

4.5 Sobrecarga de Operadores (Operator Overloading)

Sobrecarga de operadores permite redefinir o comportamento de operadores para que funcionem com tipos definidos pelo usuário. Isso é útil para tornar objetos de classes personalizadas mais intuitivos de usar em operações comuns, como comparação ou soma.

Exemplo: Comparação de Objetos Person

public class Person {
    public string Name;
    public int Age;

    public static bool operator ==(Person p1, Person p2) {
        if (ReferenceEquals(p1, null) || ReferenceEquals(p2, null)) {
            return ReferenceEquals(p1, p2);
        }
        return p1.Name == p2.Name && p1.Age == p2.Age;
    }

    public static bool operator !=(Person p1, Person p2) {
        return !(p1 == p2);
    }

    public override bool Equals(object obj) {
        if (obj is Person other) {
            return this == other;
        }
        return false;
    }

    public override int GetHashCode() {
        return HashCode.Combine(Name, Age);
    }
}

// Uso:
Person person1 = new Person { Name = "Alice", Age = 30 };
Person person2 = new Person { Name = "Alice", Age = 30 };
Person person3 = new Person { Name = "Bob", Age = 25 };

Console.WriteLine(person1 == person2); // Saída: True
Console.WriteLine(person1 != person3); // Saída: True
Enter fullscreen mode Exit fullscreen mode

Observação

Antes do surgimento do tipo record em C#, essa era a forma mais comum de criar Value Objects, garantindo a imutabilidade e a igualdade baseada no valor das propriedades.

| Sobrecarga de operadores é uma técnica poderosa, mas deve ser usada com cautela para evitar confusão ou comportamento inesperado.

4.6 Delegates e Eventos (Implementando o Padrão Observer)

O padrão Observer (também conhecido como Publish/Subscribe) é uma forma de organizar a comunicação entre objetos de modo que, quando um objeto (o Sujeito ou Publisher) muda de estado ou tem informações importantes, ele notifique automaticamente outros objetos interessados (os Observadores ou Subscribers).

No C#, delegates e eventos oferecem uma forma simples de implementar esse padrão:

  • Delegates são tipos que armazenam referências a métodos, permitindo passar métodos como parâmetros e executá-los dinamicamente.
  • Eventos, baseados em delegates, encapsulam esse comportamento e seguem o modelo de publicação/assinatura, onde vários Observers podem se inscrever (assinar) para receber atualizações de um Subject.

Exemplo de Implementação do Padrão Observer

using System;

// Delegate que define a "assinatura" do método que receberá a notificação
public delegate void Notify(string message);

// Sujeito (Publisher)
public class Subject {
    // Evento baseado no delegate Notify
    public event Notify OnNotify;

    public void NotifyObservers(string message) {
        Console.WriteLine($"Subject: Notificando observadores com a mensagem '{message}'...");

        // Invoca todos os métodos inscritos
        OnNotify?.Invoke(message);
    }
}

// Observador (Subscriber)
public class Observer {
    private readonly string _name;

    public Observer(string name) {
        _name = name;
    }

    // Método que será chamado quando ocorrer uma notificação
    public void Update(string message) {
        Console.WriteLine($"Observer {_name}: Recebeu a mensagem '{message}'");
    }
}

class Program {
    static void Main() {
        // Criação do Sujeito
        Subject subject = new Subject();

        // Criação dos Observadores
        Observer observerA = new Observer("A");
        Observer observerB = new Observer("B");

        // Inscrição (Subscribe) dos observadores no evento do sujeito
        subject.OnNotify += observerA.Update;
        subject.OnNotify += observerB.Update;

        // Dispara a notificação
        subject.NotifyObservers("Olá, Observadores!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Saída esperada:

Subject: Notificando observadores com a mensagem 'Olá, Observadores!'...
Observer A: Recebeu a mensagem 'Olá, Observadores!'
Observer B: Recebeu a mensagem 'Olá, Observadores!'
Enter fullscreen mode Exit fullscreen mode

Como Funciona

  1. Delegate Notify Define o contrato (assinatura do método) que os observadores devem ter para serem notificados.
  2. Classe Subject
    • Possui um evento OnNotify baseado no delegate.
    • Fornece o método NotifyObservers que dispara o evento para todos os inscritos.
  3. Classe Observer
    • Possui um método Update compatível com o delegate, para ser chamado quando o evento for disparado.
  4. Inscrição (+=) Os observadores se registram no evento do sujeito, indicando que desejam receber atualizações.

Vantagens do Padrão Observer

  • Desacoplamento: O Subject não precisa conhecer detalhes dos Observers, apenas o delegate que eles implementam.
  • Extensibilidade: Facilita a adição de novos Observers sem mudar o código do Subject.
  • Organização: Mantém a comunicação entre objetos de forma clara e centralizada.

Observação

Existem várias maneiras de implementar o Observer Pattern. Além de delegates e eventos, é possível usar bibliotecas reativas (como o Reactive Extensions, Rx.NET) ou mesmo outros padrões de mensageria para propagar notificações de forma assíncrona ou distribuída. O essencial é manter o princípio: quando o Publisher muda, ele aciona seus Subscribers de maneira desacoplada.


Versão: 1.3.1

Top comments (0)