© 2019

Reactive Programming no dotnet core 3.0 (parte2)

No artigo anterior demonstrei uma visão geral de como consumir Observables com System.Reactive.
Agora vamos dar uma olhada como são essas classes:

Primeiro defini os eventos:

1
2
3
public interface IEvento { } 
public class ApitoInicio : IEvento { }
public class ApitoFinal : IEvento { }  

A interface IEvento serve para tipar todas as demais classes que implementam essa interface, ou seja, toda classe que implementa IEvento é um evento. Os eventos ApitoInico e ApitoFinal são simples não precisam trafegar nenhum tipo de informação, podem ser denominados como evento “oco”.

Abaixo temos eventos mais complexos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class ApitoGol : IEvento 
{
    public ApitoGol(Time timeEmCampo)
    {
        TimeEmCampo = timeEmCampo;
    }

    public Time TimeEmCampo { get; set; }
}

public class ApitoFalta : IEvento
{
    public ApitoFalta(Time timeEmCampo)
    {
        TimeEmCampo = timeEmCampo;
    }

    public Time TimeEmCampo { get; set; }
    
}
public class GolMarcado : IEvento
{
    public GolMarcado(Time timeEmCampo, int golsMarcados)
    {
        TimeEmCampo = timeEmCampo;
        GolsMarcados = golsMarcados;
    }

    public Time TimeEmCampo { get; set; }
    public int GolsMarcados { get; set; }
}

public class FaltaCometida : IEvento
{
    public FaltaCometida(Time timeEmCampo, int faltas)
    {
        TimeEmCampo = timeEmCampo;
        Faltas = faltas;
    }

    public Time TimeEmCampo { get; set; }
    public int Faltas { get; set; }
}

public class Derrota : IEvento 
{
    public Derrota(Time timeEmCampo)
    {
        TimeEmCampo = timeEmCampo;
    }

    public Time TimeEmCampo { get; set; }
}
public class Vitoria : IEvento 
{
    public Vitoria(Time timeEmCampo)
    {
        TimeEmCampo = timeEmCampo;
    }

    public Time TimeEmCampo { get; set; }
}

Todas essas classes são eventos pois implementam IEvento, e todos recebem em seu construtor um time dessa forma o evento “é de um time, ou é para um time”, semanticamente podemos dizer que:

ApitoGol é para um time
ApitoFalta é para um time
GolMarcado é de um time
FaltaCometida é de um time
Derrota é de um time
Vitória é de um time

Essa semântica é permitida graças ao construtor desses eventos que recebem o time como parametro de entrada

Note também que as classes GolMarcado e FaltaCometida recebem o total de faltas o que permite aos observadores desse evento receberem os totais.

Regras do jogo


1
2
3
4
5
6
7
8
9
10
11
12
13
public class Regras
{
    public Regras(int limiteFaltas, int limieteGols, int limiteTempo)
    {
        LimiteFaltas = limiteFaltas;
        LimieteGols = limieteGols;
        LimiteTempo = limiteTempo;
    }

    public int LimiteFaltas { get; set; }
    public int LimieteGols { get; set; }
    public int LimiteTempo { get; set; }
}

No artigo anterior vimos que no frontend as regras são adicionadas ao container de injeção de dependências e ditam o comportamento do jogo, conforme lembrado abaixo:

1
2
3
4
5
var serviceProvider = new ServiceCollection()
                .AddScoped(r => new Regras(limiteFaltas: 3,limieteGols: 1, limiteTempo: 60))
                .AddSingleton<CentralEventos>()
                .AddScoped<Juiz>()
                .BuildServiceProvider();

Veja que adicionamos ao container uma instância de Regras, CentralEventos e Juiz, vamos dar uma olhada na classe Juiz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Juiz : Ator
{
    private Regras _regras;
    public Juiz(CentralEventos central,Regras regras) : base(central)
    {
        _regras = regras;

        central.OfType<GolMarcado>()
            .Timeout(TimeSpan.FromSeconds(regras.LimiteTempo))
            .Subscribe(
                e   => ApitarGol(e.TimeEmCampo,e.GolsMarcados),
                ex  => FinalizarPorTempo(ex)
            );

        central.OfType<FaltaCometida>()
            .Timeout(TimeSpan.FromSeconds(regras.LimiteTempo))
            .Subscribe(
                e  => ApitarFalta(e.TimeEmCampo, e.Faltas),
                ex => FinalizarPorTempo(ex)
            );

    }
    private void FinalizarPorTempo(Exception ex)
    {
        Console.WriteLine($"Juiz: Fim de jogo, o tempo acabou! {ex.Message}");
        FinalizarPartida();
    }

    public void IniciarPartida()
    {
        central.Notificar(new ApitoInicio());
    }

    private void ApitarFalta(Time time, int faltas)
    {
        Console.WriteLine($"Juiz: Infração cometida pelo time {time.Nome}");
        central.Notificar(new ApitoFalta(time));
        if (faltas == _regras.LimiteFaltas)
        {
            Console.WriteLine($"Juiz: Fim de partida o time {time.Nome} atingiu o nr. máximo de faltas");
            central.Notificar(new Derrota(time));
            FinalizarPartida();
        }
        
    }

    private void ApitarGol(Time time,int golsMarcados)
    {
        Console.WriteLine($"Juiz: Gol para o time {time.Nome}");
        central.Notificar(new ApitoGol(time));
        if (golsMarcados == _regras.LimieteGols)
        {
            Console.WriteLine($"Juiz: Fim de partida o time {time.Nome} atingiu o nr. máximo de gols");
            central.Notificar(new Vitoria(time));
            FinalizarPartida();
        }
    }

    private void FinalizarPartida()
    {
        central.Notificar(new ApitoFinal());
        central.Parar();
    }
}

Podemos dizer que o Juiz é um Ator, pois implementa a classe Ator (linha 1) e o juiz recebe a CentralEventos e as Regras através do construtor da classe Juiz (linhas 4-22).
O próprio container de injeção de dependência garante que a instância de Regras recebida seja a instância adicionada no container anteriormente, caso o container não encontre essa instância haveria uma Exception por null reference

Agora vamos dar uma olhada na classe Ator

1
2
3
4
5
6
7
8
9
public class Ator
{
    protected CentralEventos central;

    public Ator(CentralEventos central)
    {
        this.central = central ?? throw new ArgumentNullException(paramName: nameof(central));
    }
}

Todo ator recebe uma instância de CentralEventos, essa instância também é resolvida pelo container de injeção de dependência, caso a instância seja nula teremos uma Exception do tipo ArgumentNullException e voltando a classe Juiz veja que a instância de CentralEventos é repassada para a classe Ator através da palavra chave :base(central)

1
2
3
4
5
6
public class Juiz : Ator
{
    private Regras _regras;
    public Juiz(CentralEventos central,Regras regras) : base(central)
    {
...

CentralEventos


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CentralEventos : IObservable<IEvento>
{
    private Subject<IEvento> assinatura = new Subject<IEvento>();
    public IDisposable Subscribe(IObserver<IEvento> observer)
    {
        return assinatura.Subscribe(observer);
    }

    public void Notificar(IEvento evento)
    {
        assinatura.OnNext(evento);
    }

    public void Parar()
    {
        assinatura.Dispose();
    }
}

Está é a classe responsável por Notificar todos os Observadores dos eventos que ocorrem no campo!
Então veja que esta classe implementa a interface IObservable onde Observable seja do tipo IEvento.
Lembra do IEvento??
Nossa interface que apenas tipa todos os eventos que foram criados.
Então basicamente nessa linha 1 da classe CentralEventos estamos construindo a seguinte semântica:

Toda classe que implementa IEvento é uma classe Observável (OBSERVABLE) e a CentralEventos é responsável em notificar todos os Observadores (OBSERVERS).

Para facilitar o tratamento dos Observers utilizo a classe Subject (linha 3), indicando que Subject tratará o tipo IEvento e por implementar IObservable, a CentralEventos é obrigada a implementar o método Subscribe (linha 4) Sendo assim, a instância de Subject (assinatura) é associada ao Observer. E quando a central de eventos é notificada de algum evento através do método Notificar (linha 9) a reação ao evento no Observador é disparada através do método OnNext(), bem similar a um delegate! Não?

Voltando para a classe Juiz podemos ler nas linhas abaixo que estão no nosso construtor:

1
2
3
4
5
6
7
8
9
10
11
12
13
central.OfType<GolMarcado>()
        .Timeout(TimeSpan.FromSeconds(regras.LimiteTempo))
        .Subscribe(
            e   => ApitarGol(e.TimeEmCampo,e.GolsMarcados),
            ex  => FinalizarPorTempo(ex)
        );

central.OfType<FaltaCometida>()
    .Timeout(TimeSpan.FromSeconds(regras.LimiteTempo))
    .Subscribe(
        e  => ApitarFalta(e.TimeEmCampo, e.Faltas),
        ex => FinalizarPorTempo(ex)
    );

a seguinte semântica:
Quando a central notificar um GolMarcado (linha 1) dentro da regra de limite de tempo (linha 2) o juiz está inscrito (linha 3) para **reagir** apitando Gol para o time do GolMarcado e seu total de gols marcados (linha 4) e caso o limite de tempo seja atingido o juiz está inscrito para finalizar a partida (linha 5)

Isso, meus amigos, é a magia da programação funcional, dizer o que se quer e não como se quer! (Obs. R.I.P. if) :pray:

Que tal agora você tentar identificar a semântica nas linhas 8-13?

Simples, não é?

Agora vamos dar uma olhada na classe Time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Time : Ator
{
    public string Nome { get; private set; }
    private int GolsMarcados = 0;
    private int FaltasCometidas = 0;
    public Time(CentralEventos central, string nome) : base(central)
    {
        Nome = nome;
        central.OfType<ApitoInicio>()
            .Subscribe(
                ini => Console.WriteLine($"{Nome}: Vamos lá pessoal, pra cima deles!")
            );
        central.OfType<ApitoGol>() // Gol contra o time
            .Where(gol => !gol.TimeEmCampo.Equals(this))
            .Subscribe(
                gol => Console.WriteLine($"{Nome}: Ânimo pessoal!!! Vamos marcar mais forte")
            );

        central.OfType<ApitoGol>() // Gol do Time
            .Where(gol => gol.TimeEmCampo.Equals(this))
            .Subscribe(
                gol => Console.WriteLine($"{Nome}: Muito bem pessoal!!!")
            );

        central.OfType<ApitoFalta>() //Falta do outro time
            .Where(falta => !falta.TimeEmCampo.Equals(this))
            .Subscribe(
                falta => Console.WriteLine($"{Nome}: pô seu juiz, cadê o cartão?")
            );

        central.OfType<ApitoFalta>() //Falta do time
            .Where(falta => falta.TimeEmCampo.Equals(this))
            .Subscribe(
                falta => Console.WriteLine($"{Nome}: foi na bola seu juiz, que injusto!")
            );

        central.OfType<Derrota>() // derrota por faltas do time
            .Where(falta => falta.TimeEmCampo.Equals(this))
            .Subscribe(
                falta => Console.WriteLine($"{Nome}: Esse nr de faltas bem que poderia ser maior :(")
            );

        central.OfType<Derrota>() // derrota por faltas do outro time
            .Where(falta => !falta.TimeEmCampo.Equals(this))
            .Subscribe(
                falta => Console.WriteLine($"{Nome}: Jogar com violência da nisso!")
            );

        central.OfType<Vitoria>() // vitoria do time
            .Where(v => v.TimeEmCampo.Equals(this))
            .Subscribe(
                vit => Console.WriteLine($"{Nome}: Viva o {Nome}!")
            );

        central.OfType<Vitoria>() // vitoria do outro time
            .Where(v => !v.TimeEmCampo.Equals(this))
            .Subscribe(
                vit => Console.WriteLine($"{Nome}: Na próxima {vit.TimeEmCampo.Nome} vocês vão ver!")
            );

        central.OfType<ApitoFinal>()
            .Subscribe(
                fim => central.Parar()
            );
    }

    public void MarcarGol()
    {
        GolsMarcados++;
        central.Notificar(new GolMarcado(this,GolsMarcados));
    }

    public void CometerFalta()
    {
        FaltasCometidas++;
        central.Notificar(new FaltaCometida(this,FaltasCometidas));
    }
}

Note que time também é um Ator, que utiliza variáveis para controlar o nr. de gols marcados e o nr. de faltas marcadas.
As reações aos eventos, é só mais do mesmo!
Uma atenção especial pode ser dada aos métodos MarcarGol() e CometerFalta()
Veja que os gols ou faltas são computados e a instância de CentralEventos é notificada dos respectivos eventos e assim tanto o Juiz quanto o time adversário recebem e reagem aos eventos conforme apresentado anteriormente.

Conclusão


A utilização das extensões de System.Reactive facilitam o trabalho de construir aplicações REATIVAS valendo-se da utilização de um paradigma mais funcional com sintaxe semelhantes ao LinQ e um conjunto de APIs para facilitar o uso de Threads síncronas e assíncronas com Schedulers.

Nota: O objetivo deste artigo é demonstrar o poder das extensões do System.Reactive mas não aborda todos os assuntos e complexidades que podem estar envolvidos no desenvolvimento de aplicações Reativas!

Desafio


Agora que tal você implementar esse jogo com o narrador, treinador e torcida?

Espero que todos gostem e aproveitem este conhecimento! Aguardo o seu pull request :laughing: