aluno: Maykon Bergamaschi
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 07/12/2008 (2a exames)
Introdução
O objetivo deste laboratório é a implementação de melhorias de um backend de uma aplicação em Ruby responsável por gerenciar as requisições de usuários de um "Bolão", sistema de apostas em resultados de jogos de futebol.
A atividade inclui conceitos importantes vistos em sala de aula, como os padrões de projeto Builder e Decorator.
Desenvolvimento
Decorator Pattern
Melhoria nos Decorators de Thread Safety
A atividade começou com a implementação dos decorators de Thread Safety. Por se tratar de uma aplicação que pode ser acessada por vários usuários simultaneamente, devem ser evitadas condições de corrida, como dois usuários realizando apostas em um mesmo jogo ao mesmo tempo.
Para implementar essa nova característica à aplicação, foram criadas duas novas classes que decoram outras duas já existentes: MatchThreadSafeDecorator, que decora Match; e MatchListThreadSafeDecorator, que, por sua vez, decora MatchList. Elas foram construídas dessa forma uma vez que havia necessidade de decorar os jogos um a um.
A seguir, para exemplificar o que foi feito, o código de MatchThreadSafeDecorator:
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(final_score) synchronize do @decorated.finalize(final_score) end end end end
Melhoria nos Decorators de Persistência
A aplicação deve armazenar os dados de jogos e apostas em um pequeno banco no disco. Para a implementação dessa propriedade, foram criadas duas classes, de modo análogo à melhoria nos Decorators de Thread Safety, MatchPersistanceDecorator, que decora Match, e MatchListPersistanceDecorator, que decora MatchList.
Para exemplificar, o código de MatchPersistanceDecorator:
require 'monitor' module Decorators class MatchPersistanceDecorator < Decorator include MonitorMixin def make_bet(*args) bet = @decorated.make_bet(*args) @match_list_persistance_obj.save bet end def finalize(final_score) fin = @decorated.finalize(final_score) @match_list_persistance_obj.save fin end def initialize(obj, match_list) super(obj) @match_list_persistance_obj = match_list end end end
Builder Pattern
Para facilitar a construção de objetos dessas classes decoradas, foram criadas duas classes construtoras seguindo o padrão Builder, MatchListBuilder e UserListBuilder. Elas possuem um método chamado add_all, que decora o objeto com persistência, Thread Safety e Logger - na verdade, add_all é uma chamada de três outros métodos, cada um deles decora o objeto com alguma das características mencionadas.
As classes construtoras geradas têm métodos que retornam objetos decorados, como se vê a seguir, no código de MatchListBuilder. Observe também que o construtor de MatchListBuilder possui uma chamada para o método load_list, para carregar os dados a partir do disco.
require 'match_list' include Decorators class MatchListBuilder attr_reader :matchlist def initialize @matchlist = MatchList.new() @matchlist.load_list if @added_persistance @matchlist end def add_all add_persistance add_logging add_thread_safety end def add_persistance @matchlist = MatchListPersistanceDecorator.new(@matchlist, $match_db_name) end def add_logging @matchlist = MatchListLoggerDecorator.new(@matchlist) end def add_thread_safety @matchlist = MatchListThreadSafeDecorator.new(@matchlist) end end
Processamento de requisições
Para codificar as requisições e as saídas do programa, foi utilizada a YAML, formato para serializar dados para que possam ser lidos por humanos. Foi criado um módulo chamado Readers, que contém as classes responsáveis por tratar as requisições.
A classe RequestReader lê a partir do input em YAML o que está sendo requisitado e, com base no pedido feito, chama o construtor de uma das classes de requisição. Nessa operação, é passado um HashMap com as informações referentes ao pedido.
A seguir, o código da classe RequestReader:
require 'yaml' include Readers class RequestReader def initialize (match_list=$match_db, user_list=$user_db) @match_db = match_list @user_db = user_list end def read(io) #lê a entrada linha a linha até acharmos um "INICIO" while (!io.eof?) loop do string = io.gets break if string && string.include?("INICIO") end yaml = io.gets("\nFIM") #depois do INICIO, lê tudo até a palavra FIM yaml = yaml.chomp("FIM") #tira o FIM do final do string que foi lido do io request = YAML.load(yaml) # aqui já temos o hash com a requisicao processada #processa requisicão e escreve saída case request['request']['operation'] when "creation" req = RequestCreation.new(request) when "authentication" req = RequestAuthentication.new(request) when "match_listing" req = RequestMatchListing.new(request) when "finished_match_listing" req = RequestFinishedMatchListing.new(request) when "match_detailed_info" req = RequestMatchDetailedInfo.new(request) when "my_bet_info" req = RequestMyBetInfo.new(request) when "make_bet" req = RequestMakeBet.new(request) when "connection_end" req = RequestConnectionEnd.new(request) else $file_output.puts("Erro no arquivo de input. Operacao inexistente.") end end end end
Para ilustrar essas classes de requisição, será mostrada a classe responsável por fazer uma aposta em uma partida. Ela é chamada RequestMakeBet e tem seu código mostrado a seguir. Assim como todas as classes de requisição, no seu construtor é feita a chamada de um construtor de uma classe do módulo Printers, para gerar o output em formato YAML - no caso de RequestMakeBet, essa classe é YamlBetCreationPrinter.
#"make_bet" class RequestMakeBet def initialize(request) @username = request['request']['parameters'][0] #username @match_name = request['request']['parameters'][1] #match_name @score = request['request']['parameters'][2].to_score #score @match = $match_list.find_by_name(@match_name) @user = $user_list.find_by_name(@username) @user.make_bet(@score, @match) if (!!@match && !!@user) YamlBetCreationPrinter.new($match_list,@match,@user,@score,$file_output).print_bet_creation end end
Esta classe é aquela cujo construtor é chamado pelo construtor de RequestMakeBet, YamlBetCreationPrinter. Ela gera a saída em formato YAML no arquivo indicado por io:
require 'yaml' module Printers class YamlBetCreationPrinter def initialize(matchlist, match, user, score, io = $stdout) @matchlist, @match, @user, @score, @io = matchlist, match, user, score, io end def print_bet_creation @io.puts("INICIO") if (!@user) @io.puts( { :response => "failure" }.to_yaml ) else if (!@user.authenticated?) @io.puts( { :response => "failure" }.to_yaml ) else if (!@match) @io.puts( { :response => "failure" }.to_yaml ) else @io.puts( { :response => "success", :user_name => @user.username, :match_name => @match.name, :score => @score }.to_yaml ) end end end @io.puts("FIM") end end end
Testes (sem o servidor)
Foram realizados alguns testes "manuais" para testar o bom funcionamento da aplicação. Primeiramente, foram realizados os testes unitários.
Como pode se ver a seguir, os resultados desses testes mostram o funcionamento correto.
Figura 1 - Resultado dos testes unitários
Foram feitos também testes com arquivos de input em YAML. Para a execução deles, foram inseridas algumas linhas no método main, para que fosse feita a leitura a partir de mais arquivos. Entre a leitura, foi disparado um evento de finalizar as apostas de uma partida - tal evento não pode ser feito por usuários do Bolão, isso deve ser controlado pelo administrador.
O conteúdo dos arquivos de input são:
input1.yml
INICIO
---
request:
operation: creation
parameters:
- Joao
- 123456
FIM
INICIO
---
request:
operation: authentication
parameters:
- Jose
- 65432
FIM
INICIO
---
request:
operation: creation
parameters:
- Jose
- 654321
FIM
INICIO
---
request:
operation: authentication
parameters:
- Joao
- 123456
FIM
INICIO
---
request:
operation: authentication
parameters:
- Jose
- 654321
FIM
INICIO
---
request:
operation: make_bet
parameters:
- Jose
- Fla x Vasco
- 0 x 2
FIM
INICIO
---
request:
operation: make_bet
parameters:
- Joao
- Fla x Vasco
- 0 x 2
FIM
INICIO
---
request:
operation: make_bet
parameters:
- Joao
- Flu x Vasco
- 3 x 4
FIM
INICIO
---
request:
operation: connection_end
parameters:
- Joao
FIM
INICIO
---
request:
operation: match_detailed_info
parameters:
- Flu x Vasco
FIM
INICIO
---
request:
operation: my_bet_info
parameters:
- Joao
FIM
INICIO
---
request:
operation: make_bet
parameters:
- Joao
- Flu x Vasco
- 3 x 4
FIM
Para encerrar a partida, a seguinte linha no método main:
$match_list.find_by_name("Fla x Vasco").finalize("0 x 2")
input2.yml
INICIO
---
request:
operation: authentication
parameters:
- Joao
- 123456
FIM
INICIO
---
request:
operation: match_listing
FIM
INICIO
---
request:
operation: match_detailed_info
parameters:
- Fla x Vasco
FIM
INICIO
---
request:
operation: my_bet_info
parameters:
- Joao
FIM
INICIO
---
request:
operation: my_bet_info
parameters:
- Joao1
FIM
INICIO
---
request:
operation: make_bet
parameters:
- Joao
- Flu x Vasco
- 3 x 4
FIM
INICIO
---
request:
operation: connection_end
parameters:
- Jose
FIM
INICIO
---
request:
operation: connection_end
parameters:
- Joao
FIM
INICIO
---
request:
operation: match_detailed_info
parameters:
- Flu x Vasco
FIM
INICIO
---
request:
operation: match_detailed_info
parameters:
- Fla x Vasco
FIM
A saída do programa é o arquivo output1.yml:
INICIO
---
:response: success
:username: Joao
:password: 123456
FIM
INICIO
---
:response: failure
:output_message: Jose not logged
FIM
INICIO
---
:response: success
:username: Jose
:password: 654321
FIM
INICIO
---
:response: success
:output_message: Joao logged
FIM
INICIO
---
:response: success
:output_message: Jose logged
FIM
INICIO
---
:match_name: Fla x Vasco
:response: success
:user_name: Jose
:score: "0x2"
FIM
INICIO
---
:match_name: Fla x Vasco
:response: success
:user_name: Joao
:score: "0x2"
FIM
INICIO
---
:match_name: Flu x Vasco
:response: success
:user_name: Joao
:score: 3x4
FIM
INICIO
---
:response: success
:username: Joao
FIM
INICIO
---
:bet_count: 1 bets
:winners: no winners
:response: success
:final_score: _ x _
:name: Flu x Vasco
:finalized: not finalized
:end_time: 15/12/2008 22:00
FIM
INICIO
---
:response: failure
:output_message: user not logged
FIM
INICIO
---
:response: failure
FIM
INICIO
---
:response: success
:output_message: Joao logged
FIM
INICIO
---
:response:
- :bet_count: 1 bets
:name: Flu x Vasco
:end_time: 15/12/2008 22:00
FIM
INICIO
---
:bet_count: 2 bets
:winners: Jose, Joao
:response: success
:final_score: "0x2"
:name: Fla x Vasco
:finalized: finalized
:end_time: 10/12/2008 16:00
FIM
INICIO
current bets
---
:response:
- :match_name: Flu x Vasco
finalized bets
---
:response:
- :match_name: Fla x Vasco
winning bets
---
:response:
- :match_name: Fla x Vasco
FIM
INICIO
---
:response: failure
:output_message: user not found
FIM
INICIO
---
:match_name: Flu x Vasco
:response: success
:user_name: Joao
:score: 3x4
FIM
INICIO
---
:response: success
:username: Jose
FIM
INICIO
---
:response: success
:username: Joao
FIM
INICIO
---
:bet_count: 1 bets
:winners: no winners
:response: success
:final_score: _ x _
:name: Flu x Vasco
:finalized: not finalized
:end_time: 15/12/2008 22:00
FIM
INICIO
---
:bet_count: 2 bets
:winners: Jose, Joao
:response: success
:final_score: "0x2"
:name: Fla x Vasco
:finalized: finalized
:end_time: 10/12/2008 16:00
FIM
Servidor
A implementação do servidor multithread foi feita no arquivo server.rb, por meio do seguinte código (includes foram omitidos):
def server $server = TCPServer.new(5000) # porta utilizada = 5000 while ( io_socket = $server.accept ) # aguarda até que conexão seja recebida, # retornando um TCPSocket com a conexão. Thread.new do # dispare uma nova thread # e utilize io_socket para efetuar # a entrada e saída do método main, # de dentro desta thread 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. go_to_main(io_socket,io_socket) io_socket.puts "Encerrando conexao..." io_socket.close io_socket.puts "Desconectado" end end end def go_to_main(in_socket, out_socket) main(in_socket, out_socket) request_reader = RequestReader.new($match_list, $user_list) while (!request_reader.is_a?(RequestConnectionEnd)) request_reader.read(in_socket, out_socket) end end
Foi feito um pequeno teste, com dois usuários fazendo apostas simultaneamente no mesmo jogo. O resultado, apesar de o formato de saída não facilitar muito a leitura, comprova o bom funcionamento do servidor.
Figura 2: teste com o servidor multithread
O servidor poderia receber algumas melhorias, como o formato de saída dos dados ou uma forma de leitura melhor que a atual - inserção via teclado do formato YAML, de modo que qualquer erro de digitação não pode ser apagado, a requisição deve ser totalmente refeita.
Conclusão
Esta atividade foi bastante interessante, pois os alunos tiveram que partir de (e compreender) linhas de código em Ruby para gerar a aplicação especificada. O aprendizado propiciado foi grande com a realização deste laboratório.
Indubitavelmente foi uma tarefa muito trabalhosa, mesmo ela valendo por duas. Foram muitas horas de trabalho em um período especialmente complicado, as semanas do fim do 2o. bimestre e de exames.
Observação sobre a aplicação: os arquivos de input devem ser colocados em "c:\". Isso pode ser alterado em main.rb.