Mostrando postagens com marcador setter. Mostrar todas as postagens
Mostrando postagens com marcador setter. Mostrar todas as postagens

segunda-feira, 16 de junho de 2014

Refactoring das classes Position em Python e C#

Na postagem anterior, foram criadas as classes Position nas linguagens Python e C# (que são as linguagens que eu costumo trabalhar), como forma de testar o quanto nossa modelagem é independente de plataforma.

Pretendo manter a interface pública das classes o mais parecida possível entre todas as linguagens, a não ser que isso viole de forma muito óbvia alguma característica de cada linguagem.

Logo depois de publicar a postagem, me dei conta de alguns esquecimentos, algumas inadequações, e algums escolhas de design "suspeitas", de forma que, alguma reflexão e alguma pesquisa depois, venho apresentar as classes refatoradas.

Em primeiro lugar, movi o código para o GitHub, de forma que o leitor sempre terá onde encontrar o código atualizado, bem como o histórico de modificações. As postagens antigas não vão ser editadas para refletir as mudanças que forem ocorrendo, mas as mudanças mais significativas serão discutidas em postagens específicas, se for o caso.

Para este refactoring, as mudanças de API (que impactam a implementação de ambas as linguagens) foram as seguintes:
  • Implementação (conforme previamente modelado) do valor nulo para elevação, permitindo que haja um construtor com dois argumentos (latitude e longitude, nessa ordem), e um argumento opcional elevação cujo valor padrão é nulo;
  • Eliminação do construtor que usava uma sequência como argumento, pois isso enfraquecia a semântica, e dava margem a erros lógicos (pois não havia garantia de que os itens da sequência viriam na ordem correta), e de tempo de execução. Agora, a classe possui apenas um construtor, e esse construtor recebe dois argumentos numéricos, ou três argumentos numéricos. Uma discussão sobre essa decisão pode ser vista nesta pergunta do StackOverflow;
  • Apenas a Elevação possui setter. Latitude e a Longitude são somente leitura (e portanto possuem apenas getter);


Versão atual em C#:

using System;
using System.Collections.Generic;
using System.Linq;

namespace GpsDataModel
{
    public struct Position {

        private readonly double _lat, _lon;
        private double? _elev;

        public Position(double lat, double lon, double? elev = null) : this() {
            
            _lat = Math.Abs(((lat-90) % 360) - 180) - 90;
            _lon = ((lon-180) % 360) - 180;

            _elev = validateElevation(elev);
        }
        

        public double Latitude { get { return _lat; } }
        public double Longitude { get { return _lon; } }

        public double? Elevation { 
            get { return _elev; }
            set { _elev = validateElevation(value); }
        }


        private double? validateElevation(double? elev) {
            if (elev < -WGS84.SEMI_MAJOR_AXIS)
                throw new ArgumentOutOfRangeException(
                    String.Format("Elevation {0:0.00} " +
                           "is deeper than the center of the Earth!", elev));
            else
                return elev;
        }        


        public override string ToString() {
            return String.Format("Position({0:0.000}, {1:0.000}, {2:0.0})",
                                      _lat, _lon, _elev);
        }
    }
}

Comentários específicos sobre a implementação C#:
  • O argumento opcional é apresentado com um valor padrão, representado sintaticamente pelo operador "=";
  • Os tipos numéricos nullable são sufixados por um ponto de interrogação - no exemplo, "double?";
  • A lógica de validação do setter da Elevação, que deve ser usada também no construtor, foi movida para a função validateElevation;

Versão atual em Python:

#!/usr/bin/env python
# coding: utf-8

import WGS84

class Position(object):

    __slots__ = ('_lat', '_lon', '_elev')

    def __init__ (self, lat, lon, elev=None):

        self._lat = abs(((float(lat)-90) % 360) - 180) - 90
        self._lon = ((float(lon)-180) % 360) - 180

        self.elevation = elev

           
    
    @property
    def latitude(self):
        return self._lat
        
    @property
    def longitude(self):
        return self._lon
        
    @property
    def elevation(self):
        return self._elev

    @elevation.setter
    def elevation(self, elev):
        if elev > (-WGS84.SEMI_MAJOR_AXIS):
            self._elev = float(elev)
        else:
            raise ValueError ("Elevation %.2f " + "
                is deeper than the center of the Earth!" % elev)
 
 

    def __str__ (self):
        return ("Position(%.3f, %.3f, %.1f)"
                          % (self._lat, self._lon, self._elev))

quarta-feira, 11 de junho de 2014

Posição geográfica com a classe Position: Implementações em Python e C#

Anteriormente, foi discutido como representar simbolicamente a posição geográfica, dentro do sistema de coordenadas WGS84, em um contexto independente de plataforma, constituindo-se em uma classe com três propriedades numéricas (uma para cada coordenada tridimensional), todas somente leitura, respeitando certos limites:
  • Latitude, em graus, no intervalo { -90 <= Latitude <= 90 };
  • Longitude, em graus, no intervalo { -180 <= Longitude < 180 };
  • Elevação, em metros, no intervalo { -6378137 < Elevação };
 Daí decorre o seguinte modelo de design:
  • Cada instância de Position deve ser os três valores inicializados, contando para isso com um construtor com três parâmetros numéricos, e também um construtor com um parâmetro que seja uma sequência de três valores numéricos, na ordem "Latitude, Longitude, Elevação";
  • Considerando que Latitude e Longitude, ao longo da superfície da Terra, são circulares, entende-se que valores além dos limites devem ser "continuados", das seguintes formas:
    • Longitudes crescentes passam de 180 para -180, e decrescentes passam de -180 para 180, lembrando que uma posição exatamente sobre o meridiano 180, de acordo com a ISO TC 211, deve ser representada com valor negativo (ou seja, -180);
    • Latitudes crescentes, chegando a 90, começam a decrescer, conforme a sequência de exemplo [87, 88, 89, 90, 89, 88, 87...], enquanto decrescentes se comportam ao contrário: [-87, -88, -89, -90, -89, -88 ...]. Isso representaria alguém atravessando o pólo de um lado a outro.
    • Elevações podem ser negativas, desde que não ultrapassem o centro da Terra. Não se considera a continuação dessa linha em direção ao outro lado porque isso mudaria a latitude e a longitude, violando o pressuposto de independência entre as coordenadas.
A seguir, implementações comentadas em C# e Python. O código contém comentários numerados que são discutidos abaixo.

Classe Position em C#

using System;
using System.Collections.Generic;
using System.Linq;

namespace GpsDataModel
{

    public static class WGS84 {
        public static double SEMI_MAJOR_AXIS { get { return 6378137; } }
    }

    public struct Position {    // (1)

        private readonly double _lat, _lon, _elev;    // (2)


        public Position(double lat, double lon, double elev) {
            
            // (3) //
            _lat = Math.Abs(((lat-90) % 360) - 180) - 90;
            _lon = ((lon-180) % 360) - 180;
            
            
            if (elev > -WGS84.SEMI_MAJOR_AXIS)
                _elev = elev;
            else
                // (4) //
                throw new ArgumentOutOfRangeException(
                    String.Format("Elevation {0:0.00} is deeper than the center of the Earth!", elev));    
        }

        public Position(IEnumerable coords) : this(    // (5)
            coords.ElementAt(0),
            coords.ElementAt(1),
            coords.ElementAt(2)
        ) {}    // (6)
        
        
        // (7) //
        public double Latitude { get { return _lat; } }
        public double Longitude { get { return _lat; } }
        public double Elevation { get { return _lat; } }



        // (8) //
        public override string ToString() {
            return String.Format("Position({0:0.000}, {1:0.000}, {2:0.0})", _lat, _lon, _elev);
        }

    }


    // (9) //
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Position(91, 180, 100));
            Console.WriteLine(new Position(-91, -180, 100));
            Console.WriteLine(new Position(0, -190, 100));
            Console.WriteLine(new Position(new double[]{91, 180, 100}));
            
            // (10) //
            //try {
                //new Position(0,0,0).Elevation = 20;
            //} catch (ArgumentOutOfRangeException e) {
                //Console.WriteLine(e.Message);
            //}
        }
    }

}


  1. Uso de struct ao invés de class: de acordo com a documentação do C#/.NET, structs são recomendadas para objetos pequenos que representam valor e que não serão modificados. A própria documentação sugere "pontos em um sistema de coordenadas" são bons exemplos de struct;
  2. Uso de readonly: variáveis readonly não podem ser modificadas após sua inicialização. A inicialização ocorre no construtor, obrigatoriamente;
  3. Uso de "fórmulas de validação": como foi dito, desejamos que os valores de latitude e longitude sejam "contínuos", portanto foi atribuída a fórmula da onda triangular para a latitude, e a fórmula da onda dente-de-serra para a longitude. Assim, pode-se fornecer valores de entrada fora do intervalo, e a fórmula vai convertê-los para valores válidos dentro do intervalo de cada coordenada;
  4. Tratamento de erro na validação da Elevação: Como foi dito, valores de elevação que estão abaixo do centro da Terra não são válidos. Em C#, a forma recomendável de tratar esse erro é gerar manualmente uma exceção (no caso, do tipo ArgumentOutOfRangeException), com a sentença throw;
  5. Construtor alternativo: desejamos que, além de usar no construtor os três argumentos numéricos separados, seja possível usar como argumento uma sequência com os três valores. Assim, qualquer argumento do tipo "IEnumerable" pode ser usado, o qual vai ser repassado ao construtor padrão por encadeamento, usando a sintaxe ": this";
  6. Bloco vazio após o construtor: a sintaxe do C# exige que haja um bloco após um construtor, mesmo que esse bloco esteja vazio;
  7. Propriedades públicas somente com getter: recomenda-se expor os campos privados através de propriedades públicas que tenham somente getter. No caso da classe Position, isso é (mais) uma forma de garantir a imutabilidade dos valores após criados;
  8. Representação amigável (human-readable) com o método ToString: de acordo com o livro Effective C#, recomenda-se dar um override no método ToString (herdado de Object), para que qualquer tentativa de conversão do objeto em string retorne uma string legível, que traga alguma informação sobre os valores daquela instância da classe;
  9. Programa simples de teste: para o propósito desta postagem, incluí um programa que demonstra o uso dos construtores e do método ToString. O recomendável seria ter um teste unitário para a classe, mas isso fica para uma próxima oportunidade.

Classe Position em Python

#!/usr/bin/env python
# coding: utf-8

class WGS84(object):
    SEMI_MAJOR_AXIS = 6378137

class Position(object):    # (1)

    __slots__ = ('_lat', '_lon', '_elev')    # (2)

    def __init__ (self, *args):    # (3)

        if len(args) == 1:
            lat, lon, elev = args[0]
        elif len(args) == 3:
            lat, lon, elev = args
        else:    # (4)
            raise ValueError ("Position constructor takes 3 arguments or sequence with length = 3")

        self.latitude = lat
        self.longitude = lon
        self.elevation = elev



    ## (5) ##
    @property
    def latitude(self):
        return self._lat

    @latitude.setter
    def latitude(self, value):
        self._lat = abs(((float(value)-90) % 360) - 180) - 90



    ## (6) ##
    def get_longitude(self):
        return self._lon

    def set_longitude(self, value):
        self._lon = ((float(value)-180) % 360) - 180

    longitude = property(get_longitude, set_longitude)



    @property
    def elevation(self):
        return self._elev

    @elevation.setter
    def elevation(self, value):
        if value > (-WGS84.SEMI_MAJOR_AXIS):
            self._elev = float(value)
        else:
            raise ValueError ("Elevation %.2f is deeper than the center of the Earth!" % value)


    ## (7) ##
    def __str__ (self):
        return "Position(%.3f, %.3f, %.1f)" % (self._lat, self._lon, self._elev)


## (8) ##
if __name__ == "__main__":

    print Position(0,0,0)
    print Position(90, 180, 10)
    print Position(-90, -180, 20)
    print Position(91, 181, 30)
    print Position(-91, -181, 40)
    print Position ((-30, -51, 50)) 
     
  1. Herdando de object: em Python "tudo são objects", e uma recomendação é que todas as classes herdem do tipo object;
  2. Usando __slots__ para diminuir uso de memória e evitar o uso de setattr: como nossa classe é para ser leve - o que justificou o uso de struct em C# - estamos usando a propriedade __slots__ para definir que a classe Position vai ter somente três atributos. Caso isso não fosse feito, vale a situação padrão em Python, que é a existência da propridade __dict__, que é um dicionário dinâmico existente em cada instância. Isso permite adicionar e remover atributos, e é um comportamento que também não desejamos (ao menos não nesta classe);
  3. Uso de *args como parâmetro: em Python, o asterisco antes de um parâmetro significa que os argumentos passados serão transformados em uma sequência, disponível no corpo da função como a variável args. Assim, podemos passar os três argumentos numéricos, ou uma sequência de comprimento três, tratando condicionalmente cada caso no corpo do construtor;
  4. Tratamento de erro na validação da Elevação: assim como em C#, geramos manualmente uma exceção, neste caso com a sentença raise;
  5. Getters e setters com a função property: a função property é usada para que atributos sejam acessados através de funções, que podem incluir validação, cálculo, etc.
    1. Neste caso, a classe apresentará a propriedade "latitude", por exemplo, mas não será possível atribuir um valor a ela, pois ela não possui setter, apenas getter;
    2. Os nomes das funções devem ser os mesmos dos decoradores;
    3. No caso da latitude, é usada a sintaxe "@", e a funçao property é aplicada como um decorador;
    4. Nada impede o usuário de setar diretamente a propriedade "_lat", mas a convenção de nomenclatura usa o underline no início do nome para indicar uma variável interna que não deve ser diretamente setada;
  6. Função property declarada diretamente: Neste caso, a função property é usada de forma explícita, ao invés de usar decoradores, e podemos escolher nomes diferentes para as funções se quisermos;
  7. Representação amigável com o método __str__: em Python, a conversão para string chama automaticamente o método __str__, que deve pode ser substituído conforme a conveniência;
  8. Auto-teste: por convenção, cada módulo (ou seja, cada arquivo) contém um bloco condicional que roda quando o script é chamado diretamente, mas não roda quando o módulo é importado por outro script. Isso é detectado porque a variável __name__ assume o valor "__main__" rodando diretamente. Isso costuma ser aproveitado para inserir alguns testes simples, ou algum código que permita executar uma função específica, mas também permita o reuso das funções declaradas no arquivo, através de importação.

Muito provavelmente, ambas as implementações são passíveis de melhorias. Quando e se isso for feito, haverá postagens com discussão apropriada.

Nas postagens que virão, pretendo subir na escala de abstração, entrando no mundo das geometrias definidas por posições, como Pontos, Linhas e Superfícies.