Lab5 Anderson Aiziro

aluno: Nome do Aluno
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 21/12/2008 (2)

Introdução

Neste laboratório utilizaremos novamente a linguagem Ruby com o propósito de desenvolver a parte do backend da aplicação cliente servidor, que seria completada no lab6.
Além de criar maior familiaridade com a linguagem Ruby, este laboratório possibilitou o estudo de assuntos interessantes como Threads, além de IO e os padrões de projeto Decorator e Builder.
Inicialmente, analisamos o código fornecido pelo professor, a fim de complementá-lo e corrigí-lo. Em seguida, após todos os testes estarem funcionando, realizamos o desenvolvimento de um interpretador que receberá protocolos de entrada e criará protocolos de saída de dados do backend. No protocolo, foi utilizado a biblioteca YAML.

Desenvolvimento

Thread Safety

A primeira parte do laboratório consistiu na melhoria dos Decoradores de Thread Safety.

Inicialmente, vamos criar o decorador MatchThreadSafeDecorator que irá decorar os métodos make_bet e finalize da classe Match. Isto é necessário pois, se acessarmos simultaneamente estes métodos através de várias Threads, podemos ter problemas de corrida, já que o conteúdo das instâncias é alterado nestes métodos.
O código da classe MatchThreadSafeDecorator segue abaixo:

require 'monitor'
module Decorators
 
  class MatchThreadSafeDecorator < Decorator
    include MonitorMixin
 
    def make_bet(score, user)
      synchronize do
        @decorated.make_bet(score, user)
      end
    end
 
    def finalize(*args)
      synchronize do
        @decorated.finalize(*args)
      end
    end
  end
end

Em seguida, vamos criar a classe MatchListThreadSafeDecorator que possibilitará ao método add, a criação de Matchs decorados. Esta classe será semelhante ao MatchListLoggerDecorator.
O código da classe MatchListThreadSafeDecorator segue abaixo:

module Decorators
 
  class MatchListThreadSafeDecorator < ListThreadSafeDecorator
    def initialize(decorated)
      super
      old_match_decorator = self.decorated.item_decorator
      self.decorated.item_decorator = lambda do |match|
        MatchThreadSafeDecorator.new( old_match_decorator.call(match) )
      end
    end
 
    def add(name,end_time)
      match = @decorated.add(name,end_time)
      $logger.info "Match '#{name}' was added successfully"
      match
    end
  end
end

Persistência

A segunda parte do laboratório consistiu na melhoria dos decoradores de Persistência.

Esta parte foi mais difícil. Foi necessária uma inspeção mais longa do código, observando as heranças, por causa da necessidade de uma referência ao objeto MatchList decorado para ser possível chamar o método save.

Inicialmente, vamos criar o decorador MatchPersistanceDecorator, que será responsável por gravar as informações do MatchList decorado no disco. Aqui, entra a referência ao MatchList decorado, para utilizarmos o método save.
O código de MatchPersistanceDecorator segue abaixo:

module Decorators
  class MatchPersistanceDecorator < Decorator
    def initialize(obj,lista)
      super(obj)
      @lista=lista
    end
 
    def make_bet(score, user)
      @decorated.make_bet(score, user)
      @lista.save
    end
 
    def  finalize(*args)
      @decorated.finalize(*args)
      @lista.save
    end
  end
end

Em seguida, criamos a classe MatchListPersistanceDecorator, que decora o método add de forma a criar matches decorados com MatchPersistanceDecorator. Aqui, passamos como referência à classe MatchPersistanceDecorator a referência a MatchList, representado pelo termo self ao construtor de MatchPersistanceDecorator.
O código de MatchListPersistanceDecorator segue abaixo:

module Decorators
 
  class MatchListPersistanceDecorator < ListPersistanceDecorator
    def initialize(decorated,filename)
      super
      old_match_decorator = self.decorated.item_decorator
      self.decorated.item_decorator = lambda do |match|
        MatchPersistanceDecorator.new( old_match_decorator.call(match),self )
      end
    end
  end
 
end

Builders

A terceira parte consistiu na criação de Builders para MatchList e UserList, que simplificam a criação de objetos dessas classes. Os builders estão no diretório de Builders.

Os builders possuem as seguintes características: devem adicionar os 3 tipos de decoradores (Logger, ThreadSafe e Persistance) individualmente e juntos (com a chamada do método add_all). Além de possuir um método para retornar o objeto construido (match_list e user_list).
O código de MatchListBuilder segue abaixo:

module Builders
  class MatchListBuilder
    def initialize
      @matchlist = MatchList.new
    end
 
    def add_persistance(filename=$matchfile)
      @matchlist = MatchListPersistanceDecorator.new(@matchlist, filename )
    end
 
    def add_logging
      @matchlist = MatchListLoggerDecorator.new(@matchlist)
    end
 
    def add_thread_safety
      @matchlist = MatchListThreadSafeDecorator.new(@matchlist)
    end
 
    def add_all
      self.add_persistance
      self.add_logging
      self.add_thread_safety
    end
 
    def match_list
      @match_list.load_list if @added_persistance
      @match_list
    end
  end
end

O código de UserListBuilder segue abaixo:

module Builders
  class UserListBuilder
    def initialize
      @added_persistance=false
      @userlist = UserList.new
    end
 
    def add_persistance(filename=$userfile)
      @userlist = MatchListPersistanceDecorator.new(@userlist,filename)
      @added_persistance=true
    end
 
    def add_logging
      @userlist = MatchListLoggerDecorator.new(@userlist)
    end
 
    def add_thread_safety
      @userlist = MatchListThreadSafeDecorator.new(@userlist)
    end
 
    def add_all
      self.add_persistance
      self.add_logging
      self.add_thread_safety
    end
 
    def user_list
      @userlist.load_list if @added_persistance
      @userlist
    end
  end
end

Resultados obtidos

Após terminar esta parte, realizamos os testes para verificar o bom funcionamento do programa. Os resultados obtidos encontram-se na imagem abaixo:

rodartestes.bmp

Protocolo de entrada e saída

Esta parte do projeto consistiu na escolha de um protocolo e na utilização da biblioteca yaml para formatar os dados de entrada e saída. A implementação das operações que devem ser suportadas pela aplicação não é algo complexo, mas devido à falta de prática com a liguagem ruby, erros pequenos foram responsáveis por tomar muito tempo.

Entre os principais erros enfrentados durante o desenvolvimento: o método "load_list" de ListPersistanceDecorator utiliza o método "map" e isso estava ocasionando erros na aplicação. O erro causado era C:/Doc…lib/decorators/list_persistance_decorator.rb:24:in ‘load_list’: undefined method ‘map’ for nil:NilClass (NoMethodError). Por não ter prática com a linguagem Ruby, acreditava que este erro era ocasionado por algum erro de implementação nas classes. Alterei muitas coisas e, mesmo assim, o erro persistiu. Por sorte, exclui os arquivos userfile.yml e matchfile.yml utilizado para armazenar as informações dos usuários e das partidas. Isto resolveu o problema. Os arquivos, criados por mim, estavam inicialmente vazios e este era o motivo para o erro.

Além disso, em RequestReader, analisamos a operação escolhida na requisição. Para escolher a operação a ser realizada, estava utilizando if e comparando as strings com equal? , mas isto não funcionou. Para que a operação fosse escolhida corretamente, foi necessário alterar para case / when. Ainda não entendo a diferença e porque não funcionou com if / equal?

Após estes erros, acredito que a aplicação esteja funcionando corretamente! Realizei alguns testes e as respostas foram positivas.

A classe principal é RequestReader, onde lemos os arquivos e, se corretamente formatados, realizamos uma operação. O código de RequestReader segue abaixo:

require 'yaml'
module Readers
 
  class RequestReader
    attr_reader :encerrar
 
    def initialize(user)
      @user = user
      @matchlist=$matchlist
      @userlist=$userlist
      @encerrar = false
    end
 
    def read_yaml(fin = $stdin,fout=$stdout)
 
      #
      #Como explicado nas instruções do lab
      #Leitura da requisição em yaml
      #Guarda em requisicao o hash
      #
      loop do
        string = fin.gets
        break if string && string.include?("INICIO")
      end
      yaml = fin.gets("\nFIM")
      yaml = yaml.chomp("FIM")
      requisicao = YAML.load(yaml)
 
      #
      #em operacao estamos lendo a operacao a ser realizada pela requisicao
      #
      operacao = requisicao['request']['operation']
 
      #
      #em todas as funções passamos o arquivo de saída para escrevermos a saída yaml.
      #além disso, nas funções precisamos de matchlist ou userlist
      #
 
      case operacao
      #para criar o usuário precisamos do login e da senha
      when "criar_usuario"  
        #if(operacao.equal?("criar_usuario"))
        CreateUser.new(requisicao,fout)
 
      #para autenticar o usuário, precisamos verificar o login e a senha
      #se autenticado, o usuário está conectado.
      when "autenticar_usuario"
      #elsif(operacao.equal?("autenticar_usuario"))
        @user = AuthenticateUser.new(requisicao,fout)
 
      #lista todas as partidas do momento.
      when "listar_partidas_atuais"
      #elsif(operacao.equal?("listar_partidas_atuais"))
        ListCurrentMatches.new(fout)
 
      #lista todas as partidas encerradas
      when "listar_partidas_encerradas"
      #elsif(operacao.equal?("listar_partidas_encerradas"))
        ListEndedMatches.new(fout)
 
      #listar as informações de determinada partida, requer a especificação
      #de qual partida presente no hash.
      when "informacoes_partida"
      #elsif(operacao.equal?("informacoes_partida"))
        InfoMatch.new(requisicao,fout)
 
      #para listar as apostas de um determinado usuário, precisamos do usuário conectado
      when "listar_minhas_apostas"
      #elsif(operacao.equal?("listar_minhas_apostas"))
        if(@user)
          ListMyBets.new(requisicao,@user,fout)
        else
          RespostaPrinter.new(fout).listar_need_user
        end
 
      #para fazer uma aposta, precisamos do usuário que faz a aposta e das informacoes da partida.
      when "apostar"
      #elsif(operacao.equal?("apostar"))
        if(@user)
          NewBet.new(requisicao,@user,fout)
        else
          RespostaPrinter.new(fout).apostar_need_user
        end
 
      #encerrar a conexao
      when "encerrar_conexao"
      #elsif(operacao.equal?("encerrar_conexao"))
        FinishConnection.new(fout)
        @encerrar = true
 
      end
 
    end
  end
end

De acordo com o código de RequestReader, criamos códigos para cada uma das operações possíveis. O hash jah foi formado por RequestReader, agora precisamos somente verificar quais parâmetros cada função necessita. Com isso, criamos as classes AuthenticateUser, CreateUser, FinishConnection, InfoMatch, ListCurrentMatches, ListEndedMatches, ListMyBets, NewBet e RequestReader, todas estas, dentro do diretório Readers, junto com RequestReader.
Todos as classes precisam do arquivo fout, que é onde escrevemos as mensagens, dados e informações para o usuário.

AuthenticateUser

Para autenticar um usuário, precisamos de seu 'username' e a sua 'password'. Assim, precisamos do hash de requisição. O código de AuthenticateUser segue abaixo.

module Readers
  class AuthenticateUser
    attr_accessor :user
 
    def initialize(requisicao,fout)
      @requisicao = requisicao
      @fout = fout
      @userlist = $userlist
 
      username = @requisicao['request']['parameters']['username']
      password = @requisicao['request']['parameters']['password']
 
      #
      #ao autenticarmos um usuário, retornamos o usuário adicionado
      #em caso de sucesso (pelo método authenticate de UserList) ou nil.
      #Assim, através do retorno, conseguimos definir a mensagem a
      #ser carregada no arquivo de resposta ao cliente
      #
 
      @user = @userlist.authenticate(username, password)
 
      if(@user)
        #mensagem de sucesso na autenticação do usuário
        RespostaPrinter.new(@fout).authenticate_success
      else
        #mensagem de falha na autenticação do usuário
        RespostaPrinter.new(@fout).authenticate_failure
 
      end
    end
  end
end

CreateUser

Para criar um usuário, precisamos de seu 'username' e a sua 'password' para cadastrá-lo. Assim, precisamos do hash de requisição. O código de CreateUser segue abaixo.

module Readers
  class CreateUser
    def initialize(requisicao,fout)
      @userlist = $userlist
      @fout = fout
      @requisicao = requisicao
 
      username = @requisicao['request']['parameters']['username']
      password = @requisicao['request']['parameters']['password']
 
      #
      #ao adicionarmos um usuário, retornamos o usuário adicionado
      #em caso de sucesso (pelo método add de List). Assim, através
      #do retorno, conseguimos definir a mensagem a ser carregada
      #no arquivo de resposta ao cliente
      #
 
      user = @userlist.add(username,password)
 
      if(user)
        #mensagem de sucesso na criação do usuário
        RespostaPrinter.new(@fout).create_user_success
      else
        #mensagem de falha na criação do usuário
        RespostaPrinter.new(@fout).create_user_failure
      end
 
    end
  end
end

FinishConnection

Aqui, encerramos a conexão. Utilizamos uma variável em RequestReader "encerrar" que nos indica quando devemos encerrar a conexão. FinishConnection apenas informa ao usuário que a conexão foi encerrada.

module Readers
  class FinishConnection
    def initialize(fout)
      @fout = fout
      RespostaPrinter.new(@fout).finish_connection
    end
  end
end

InfoMatch

InfoMatch é responsável por mostrar os dados de uma determinada partida. Assim, precisamos da requisição para passar a partida que queremos informações. Para imprimir os dados, utilizamos o método ja implementado em YamlMatchListPrinter, passando como parâmetro somente uma partida, ao invés de uma lista de partidas.

module Readers
  class InfoMatch
    def initialize(requisicao,fout)
      @requisicao = requisicao
      @fout = fout
      @matchlist = $matchlist
 
      name = @requisicao['request']['parameters']['name']
 
      match = @matchlist.find_by_name(name)
 
      YamlMatchListPrinter.new(match,@fout).print_matches if (match)
 
    end
  end
end

ListCurrentMatches, ListEndedMatches

Estas classes são semelhantes, alterando somente o tipo de lista que iremos imprimir. Ambas utilizam métodos já implementados em YamlMatchListPrinter.

module Readers
  class ListCurrentMatches
 
    def initialize(fout)
      @fout = fout
      @matchlist = $matchlist
 
      #
      #vamos utilizar o método print_current_matches de YamlMatchListPrinter
      #que imprime somente as partidas atuais
      #
 
      YamlMatchListPrinter.new(@matchlist, @fout).print_current_matches
 
    end
  end
end
module Readers
  class ListEndedMatches
    def initialize(fout)
      @fout = fout
      @matchlist = $matchlist
 
      #
      #vamos utilizar o método print_finalized_matches de YamlMatchListPrinter
      #que imprime somente as partidas encerradas
      #
 
      YamlMatchListPrinter.new(@matchlist,@fout).print_finalized_matches
 
    end
  end
end

ListMyBets

Assim como as duas classes anteriores, esta também imprime uma lista. Porém, requer algumas outras modificações para formarmos a lista somente com as apostas do usuário logado.

module Readers
  class ListMyBets
    def initialize(requisicao,user,fout)
      @requisicao = requisicao
      @fout = fout
      @user = user
      @matchlist = $matchlist
 
      apostas = []
 
      @matchlist.to_a.map do |m|
        m.bets.each{ |b| b.holders.each do |h|
            apostas << m  if
            (h.username == @user.username)
          end}
 
      end
      YamlMatchListPrinter.new(apostas,@fout).print_matches
    end
  end
end

NewBet

Aqui, realizamos uma nova aposta. Para que seja possível realizar a aposta, devemos ter um usuário logado no sistema. Além disso, a partida escolhida deve ser uma partida existente.

# To change this template, choose Tools | Templates
# and open the template in the editor.
module Readers
  class NewBet
    def initialize(requisicao,user,fout)
      @user = user
      @requisicao = requisicao
      @fout = fout
      @matchlist = $matchlist
 
      #leitura dos dados da aposta do usuario
      name = @requisicao['request']['parameters']['name']
      score = @requisicao['request']['parameters']['score']
 
      score = score.to_score
 
      #com os dados do jogo, vamos procurar a existencia da partida
      match = @matchlist.find_by_name(name)
 
      #só podemos fazer a aposta caso a partida exista
      if(match)
        @user.make_bet(score,name)
        RespostaPrinter.new(@fout).new_bet_success
      else
        RespostaPrinter.new(@fout).new_bet_failure
      end
 
    end
  end
end

Printers

No módulo printers, adicionamos a classe RespostaPrinter. É uma classe responsável somente por informar o usuário sobre o sucesso/falha em cada uma de suas operações, quando necessário.

require 'yaml'
module Printers
  class RespostaPrinter
    def initialize(fout)
      @fout = fout
    end
    def authenticate_success
      @fout.puts "INICIO"
      @fout.puts(
        {
        :response => "Usuario autenticado com sucesso."
        }.to_yaml
      )
      @fout.puts "FIM"
    end
    def authenticate_failure
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "Erro na autenticação. Verificar dados do usuario."
        }.to_yaml
      )
      @fout.puts "FIM"
    end
    def create_user_success
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "Usuario criado com sucesso."
        }.to_yaml
      )
      @fout.puts "FIM"
    end
    def create_user_failure
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "Erro na criação. O usuário já existe."
        }
      )
      @fout.puts "FIM"
    end
    def finish_connection
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "Conexão encerrada"
        }
      )
      @fout.puts "FIM"
    end
    def listar_need_user
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "É necessário selecionar um usuário para listar as apostas."
        }
      )
      @fout.puts "FIM"
    end
    def apostar_need_user
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "É necessário um usuário para realizar a aposta."
        }
      )
      @fout.puts "FIM"
    end
    def new_bet_success
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "Aposta realizada com sucesso."
        }
      )
      @fout.puts "FIM"
    end
    def new_bet_failure
      @fout.puts "INICIO"
      @fout.puts(
        {
          :response => "Erro na aposta. O jogo escolhido não existe."
        }
      )
      @fout.puts "FIM"
    end
  end
end

Realizando algumas operações para testar o protocolo criado:

Os arquivos de entrada são: 1.yml, 2.yml, 3.yml, 4.yml, 5.yml e 6.yml
Seus conteúdos são:

INICIO
---
request:  
 operation: criar_usuario
 parameters:  
   username: Joao  
   password: 123456  
FIM
INICIO
---
request:  
 operation: criar_usuario
 parameters:  
   username: Jose  
   password: 654321 
FIM
INICIO
---
request:  
 operation: listar_minhas_apostas
 parameters:  
   username: Joao  
FIM
INICIO
---
request:  
 operation: autenticar_usuario
 parameters:  
   username: Jose  
   password: 0000 
FIM
INICIO
---
request:  
 operation: autenticar_usuario
 parameters:  
   username: Jose  
   password: 654321 
FIM
INICIO
---
request:  
 operation: informacoes_partida
 parameters:  
   name: Sao Paulo x Gremio 
FIM

Para as entradas acima, obtivemos as seguintes saídas:

INICIO
--- 

:response: Usuario criado com sucesso.

FIM

INICIO
--- 

:response: Usuario criado com sucesso.

FIM

INICIO
---
:response: É necessário selecionar um usuário para listar as apostas.

FIM

INICIO
--- 

:response: "Erro na autentica\xc3\xa7\xc3\xa3o. Verificar dados do usuario."

FIM

INICIO
--- 

:response: Usuario autenticado com sucesso.

FIM

INICIO
--- 

:response: 
- 
:end_time: 23/12/2008 00:43

:name: Sao Paulo x Gremio

:winners: no winners

:final_score: _ x _

:bet_count: 0 bets

:finalized: not finalized

FIM

No arquivo de log (bolao.log) obtivemos as seguintes saídas:

I, [2008-12-22T10:50:06.848668 #3540]  INFO -- : Match 'Sao Paulo x Gremio' was added successfully.
I, [2008-12-22T10:50:06.851083 #3540]  INFO -- : Match 'Sao Paulo x Gremio' was added successfully
I, [2008-12-22T10:50:06.921837 #3540]  INFO -- : User Joao was added successfully.
I, [2008-12-22T10:50:06.945959 #3540]  INFO -- : User Jose was added successfully.
W, [2008-12-22T10:50:06.956855 #3540]  WARN -- : User Jose (pass: 0)  couldn't log in.
I, [2008-12-22T10:50:06.967120 #3540]  INFO -- : User Jose logged in.

Conexão do Servidor MultiThread

Comparado com a criação do protocolo de entrada e saída, esta parte não apresentou muitos erros. A quantidade de código para esta parte foi pequena e foi possível realizar múltiplas conexões.
Um ponto interessante foi que, ao criar a classe server.rb, inicialmente, obtivemos o erro que precisávamos da classe user_list_builder e ao adicionar o require 'user_list_builder' passou a apresentar problemas. Uma solução encontrada, foi adicionar todos os requires manualmente, como estava sendo realizado no primeiro pacote do lab5. Desse modo, os erros sumiram.
O código para o servidor encontra-se abaixo:

require 'socket'
require 'initialize'
require 'main'
require 'builders/user_list_builder'
 
$stdout = File.new('C:\\out.yml',"w")
$matchfile = 'C:\\matchfile.yml'
$userfile = 'C:\\userfile.yml'
user_list_builder = UserListBuilder.new()
user_list_builder.add_all()
$userlist = user_list_builder.user_list
match_list_builder = MatchListBuilder.new()
match_list_builder.add_all()
$matchlist = match_list_builder.match_list
 
$matchlist.add("Sao Paulo x Gremio", Time.now + 50000)
 
server = TCPServer.new(4344)  # 4344 é a porta de conexão de rede usada
                               # utilize de preferência um número acima de
                               # 2000 (e menor que 64000).
while ( io_socket = server.accept ) #aguarda até que conexão seja recebida, retornando um TCPSocket com a conexão.
  # dispare uma nova thread e utilize io_socket para efetuar a entrada e saída do método main, de dentro desta thread
  Thread.new do
    io_socket.puts "conectado"
    #io_socket deve ser utilizado tanto para saída quanto para entrada, portanto é passado como parâmetro in e parâmetro out de main.
    main(io_socket,io_socket)
    io_socket.close
    io_socket.puts "desconectado"
  end
 
end

Exemplo de múltiplas conexões ao nosso servidor:

conexao-servidor.JPG

Conclusão

Este laboratório foi muito extenso e travou muitas vezes durante sua implementação.
Além de possuir mais código do que os labs passados, a insegurança e falta de prática com a linguagem Ruby, fez com que muito tempo fosse perdido em erros pequenos. Primeiro por não saber o que o erro significava e segundo por não saber onde procurar o erro.
Contudo, com essas muitas horas, acredito que foi possível entender melhor como funciona a liguagem ruby e ganhar alguma experiência sobre possíveis erros e suas causas.
Além disso, foi possível colocar em prática padrões de projetos interessantes como o Decorator.

Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License