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:
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:
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.





