Lab5 Rodrigo Almeida

aluno: Rodirgo Simões de Almeida
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 22/12/2008

Introdução

A idéia do Lab5 era a criação de um servidor "completo", capaz de responder clientes utilizando um protocolo simples, ao mesmo tempo em que exercitar o uso sobretudo do padrão Decorator.

Desenvolvimento

Na parte de ajuste dos decorators, não houveram grandes dificuldades, e acredito que os códigos que eu tenha gerado não tenham ficado diferentes do que o resto do pessoal deve ter gerado. Os testes passaram, conforme pedido. Os códigos fontes se encontram abaixo:

Persistance

class MatchListPersistanceDecorator < Decorator
    def initialize(decorated, file)
      old_match_decorator = decorated.item_decorator
      decorated.item_decorator = lambda do |match|
        MatchPersistanceDecorator.new(self, old_match_decorator.call(match) )
      end
      super(ListPersistanceDecorator.new(decorated, file))
    end
  end
 
  class MatchPersistanceDecorator < Decorator
    def initialize(match_list, *args)
      super(*args)
      @match_list = match_list
    end
 
    def make_bet(*args)
      retval = super
      @match_list.save
      retval
    end
 
    def  finalize(*args)
      #igualzinho o de cima
    end
  end

ThreadSafety

class MatchListThreadSafeDecorator < Decorator
    def initialize(decorated)
      old_match_decorator = decorated.item_decorator
      decorated.item_decorator = lambda do |match|
        MatchThreadSafeDecorator.new( old_match_decorator.call(match) )
      end
      super
    end
  end
 
  class MatchThreadSafeDecorator < Decorator
 
    include MonitorMixin
 
    def make_bet(*args)
      synchronize do
        @decorated.make_bet(*args)
      end
    end
 
    def  finalize(*args)
      # Igualzinho o de cima
    end
  end

A repetição de código nos decorators sugere que algumas refatorações poderiam ser feitas em cima dos códigos acima, por exemplo, o thread safety poderia ser reescrito como:

class MatchThreadSafeDecorator < ThreadSafeDecorator
   synchronize_methods :make_bet, :finalize
end

Nessa abordagem, ThreadSafeDecorator implementaria o método synchronize_methods e usaria metaprogramação para gerar os overrides.

Builders

Foi solicitada a construção de dois builders, que acabaram ficando com exatamente o mesmo esqueleto (sugerindo que poderia haver um builder "abstrato" a partir do qual os dois builders seriam construídos - aplicando-se o template method). O código de um deles segue abaixo:

class MatchListBuilder
  def initialize
    @target = MatchList.new
  end
 
  def add_all
    add_persistance
    add_logging
    add_thread_safety
  end
 
  def add_persistance
    @target = Decorators::MatchListPersistanceDecorator.new(@target, "match_list.yml")
    @added_persistance = true
  end
 
  def add_logging
    @target = Decorators::MatchListLoggerDecorator.new(@target)
  end
 
  def add_thread_safety
    @target = Decorators::MatchListThreadSafeDecorator.new(@target)
  end
 
  def match_list
    @target.load_list if @added_persistance
    @target
  end
end

O servidor em si

Protocolo

Foi adotada a idéia de se usar o YAML (porque complicar?) para gerar a saída, mas, para permitir a execução de testes mais concisos, a sintaxe de entrada seguiu um protocolo diferenciado:

  • Cada comando é escrito em uma linha
  • Os parâmetros são passados entre parêntesis (como se o comando fosse uma chamada de função em C), mas como parâmetros nomeados
  • Exemplo: put_bet(name: A x C, score: 3 x 4) - chama o comando put_bet com os parâmetros name = "A x C" e score = "3 x 4", os espaços em branco adicionais são removidos automaticamente

(Processamento do input):

string = @io.gets.strip.chop
      parts = string.split("(", 2)
      command = parts[0].strip
      kvpairs = parts[1].split(",")
      parameters = {}
      kvpairs.each do |kvpair|
        kvpair_parts = kvpair.split(":")
        parameters[kvpair_parts[0].strip.to_sym] = kvpair_parts[1].strip
      end

RequestReader

Uma instância de RequestReader ficou responsável por processar toda a seqüência de comandos de um determinado cliente (assim, informações como se a autenticação foi feita e qual o usuário autenticado ficam armazenados nessa instância). O código não ficou muito elegante e um pouco grande demais, sugerindo que o método principal (next_request) poderia ser quebrado em outros métodos menores.

(transcrição completa - RequestReader)

# To change this template, choose Tools | Templates
# and open the template in the editor.
 
class RequestReader
  def initialize(io, match_list, user_list)
    @io = io
    @logged = false
    @user = nil
    @default_printer = Printers::DefaultPrinter.new(io)
    @match_printer = Printers::DefaultMatchPrinter.new(@default_printer, match_list)
    @user_printer = Printers::DefaultUserPrinter.new(@default_printer, user_list, match_list)
    @listening = true
    @user_list = user_list
    @match_list = match_list
  end
 
  def start
    while @listening
      puts "Waiting for socket input"
      process_next
    end
  end
 
  def stop_listening
    @listening = false
    @io.close
  end
 
  def just_send_a_okay_back
    @default_printer.send_back(:status => 'ok')
  end
 
  def send_error error
    @default_printer.send_back(:status => 'failed', :error => error)
  end
 
  def process_next
    begin
      string = @io.gets.strip.chop
      parts = string.split("(", 2)
      command = parts[0].strip
      kvpairs = parts[1].split(",")
      parameters = {}
      kvpairs.each do |kvpair|
        kvpair_parts = kvpair.split(":")
        parameters[kvpair_parts[0].strip.to_sym] = kvpair_parts[1].strip
      end
    rescue Exception => e
      @default_printer.send_back(:status => 'bad request', :input => string + ')')
    end
 
    puts "Got request #{string})"
    puts " - Mapped to #{command} with args #{parameters.inspect}"
 
    begin
      if command == 'exit' # :DEBUG:
        just_send_a_okay_back
        stop_listening
        exit
      elsif command == 'create_user'
         @user_list.add(parameters[:username], parameters[:password])
         return just_send_a_okay_back
      elsif command == 'authenticate'
        (@user = @user_list.authenticate(parameters[:username], parameters[:password])) || raise("authentication failed")
        @logged = true
        return just_send_a_okay_back
      elsif not @logged
        raise "authentication required"
      end
 
      case command
      when 'create_match' # :DEBUG:
        @match_list.add(parameters[:name], Time.mktime(parameters[:end_time]))
        just_send_a_okay_back
      when 'finalize_match' # :DEBUG:
        @match_list.find_by_name(parameters[:name]).finalize(parameters[:score])
        just_send_a_okay_back
      when 'current_matches':
        return @match_printer.print_current_matches
      when 'finalized_matches':
        return @match_printer.print_finalized_matches
      when 'get_match':
        return @match_printer.print_match_details(parameters[:name])
      when 'get_bets':
        return @user_printer.print_user_bets(@user)
      when 'put_bet':
        (match = @match_list.find_by_name(parameters[:name])) || raise("No such match exists")
        match.make_bet(parameters[:score], @user)
        just_send_a_okay_back
      when 'logout':
        @logged = false
        just_send_a_okay_back
        stop_listening
      else
        raise "Unknown command"
      end
    rescue Exception => e
      raise e if SystemExit === e
      send_error(e.message)
    end
  end
end

Requisições adicionais

A fim de permitir popular as MatchLists diretamente a partir dos testes, novos comandos foram liberados: create_match e finalize_match. Além disso, foi criado o comando exit, que força o servidor a desligar (útil quando dá problemas, infelizmente o Ruby não se dá muito bem com as chamadas de interrupção enviadas via teclado pelo prompt do DOS).

Printers

Aqui, usamos uma classe base DefaultPrinter, que basicamente é responsável por serializar um Hash no padrão especificado no protocolo (no caso YAML), isto é, ela faz a conversão alto nível —> baixo nível, e printers específicos para MatchList e User (que preparam os objetos a serem enviados para DefaultPrinter). Respostas simples (como "deu tudo certo") e exceções são enviadas diretamente pela classe RequestReader para a impressora padrão.

Um ponto interessante da estrutura usada é que, caso o protocolo fosse alterado, bastaria trocar DefaultPrinter e todos os seus usuários não precisariam alterar (a princípio) seus códigos.

(Código de DefaultPrinter)

module Printers
  class DefaultPrinter
    def initialize io
      @io = io
    end
 
    def send_back string_or_object
      string_or_object = string_or_object.to_yaml unless String === string_or_object
      @io.puts "BEGIN\r"; puts "Writing Response..."
      @io.puts string_or_object.gsub(/\n/,"\r\n"); puts string_or_object
      @io.puts "END\r"; puts "Response Finished..."
      self
    end
  end
end

Um exemplo de printer específica

class DefaultMatchPrinter
 
      def initialize printer, match_list
        @printer = printer
        @match_list = match_list
      end
 
      def print_match_details name
        m = @match_list.find_by_name || raise("No such match exists")
        @printer.send_back(
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :finalized => m.finalized?,
              :final_score => m.final_score,
              :bet_count => m.bet_count,
              :winners => m.winners.collect{|u|u.username}
         )
      end
 
      def print_current_matches
        @printer.send_back(
          @match_list.list_current.collect do |m|
            { :name => m.name,
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :bet_count => m.bet_count }
          end
        )
      end
 
      # Continua
  end

CR LF

O windows usa (sabidamente) esse padrão de quebra de linha (\r\n). Foi necessário adaptar o código manualmente para que os resultados exibidos não ficassem "estranhos" (pulando linha mas com o cursor começando logo em baixo de onde a linha anterior acabou)

Testes

Para os testes rodou-se o servidor via linha de comando e como clientes telnet usou-se o Putty (o telnet do Windows vista não mostrava os caracteres digitados o que atrapalhava os testes). A única ressalva é que o Putty enviava um lixo na primeira requisição.

Segue abaixo um screenshot:

ss1

Nota: os testes mostraram que o servidor apresentava sim alguns bugs, mas estes não atrapalharam o bom funcionamento geral da solução

Conclusão

O Lab foi relativamente rápido de ser implementado (a parte dos decorators foi um pouco repetitiva, e me pareceu que uma desvantagem desse padrão é que fica mais complexa a criação dos objetos, não é a toa que foi sugerido usar-se um builder para remover essa limitação. Nesse projeto simples, inclusive tive um pouco a impressão de que o código ficaria mais fácil de entender e manter se fosse logo socado todo numa mesma classe não decorada - mas obviamente esse não é o caso de usos mais complexos e elaborados. Em aplicações que exigem alto desempenho, outra desvantagem do decorator é que ele aumenta a pilha de chamadas (call stack), o que pode degradar memória e reduzir a performance), e a parte do servidor eu havia feito algo equivalente em Java faz pouco tempo então não tive grandes dificuldades - mas por ser leigo em entender como o ruby gerencia threads, interrupções I/O e coisas do tipo não conseguia fechar o servidor com Ctrl+C como consigo com os demais problemas, motivo pelo qual decidi colocar (conforme explicado acima) um comando exit() para forçar o servidor a se desligar.

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