Faria sentido usar objetos (em vez de tipos primitivos) para tudo em C ++?

17

Durante um projeto recente em que estou trabalhando, tive que usar várias funções que se parecem com isso:

static bool getGPS(double plane_latitude, double plane_longitude, double plane_altitude,
                       double plane_roll, double plane_pitch, double plane_heading,
                       double gimbal_roll, double gimbal_pitch, double gimbal_yaw, 
                       int target_x, int target_y, double zoom,
                       int image_width_pixels, int image_height_pixels,
                       double & Target_Latitude, double & Target_Longitude, double & Target_Height);

Então, eu quero refatorá-lo para algo parecido com isto:

static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
                            PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)

Isso me parece significativamente mais legível e seguro do que o primeiro método. Mas faz sentido criar PixelCoordinatee PixelSizeclasses? Ou seria melhor usar apenas std::pair<int,int>para cada um. E faz sentido ter uma ZoomLevelaula ou devo apenas usar uma double?

Minha intuição por trás do uso de classes para tudo é baseada nessas suposições:

  1. Se houver classes para tudo, seria impossível passar um ZoomLeveldentro de onde um Weightobjeto era esperado; portanto, seria mais difícil fornecer argumentos errados para uma função
  2. Da mesma forma, algumas operações ilegais causariam erros de compilação, como adicionar um GPSCoordinatea um ZoomLevelou outroGPSCoordinate
  3. As operações legais serão fáceis de representar e tipificar com segurança. ou seja, subtrair dois GPSCoordinates produziria umaGPSDisplacement

No entanto, a maioria dos códigos C ++ que eu vi usa muitos tipos primitivos, e imagino que deve haver uma boa razão para isso. É uma boa ideia usar objetos para qualquer coisa, ou tem desvantagens das quais não estou ciente?

zackg
fonte
8
"a maioria dos códigos C ++ que eu já vi usa muitos tipos primitivos" - dois pensamentos me vêm à mente: muitos códigos C ++ podem ter sido escritos por programadores familiarizados com C, que apresentam uma abstração mais fraca; também de Sturgeon Lei
congusbongus
3
E acho que é porque os tipos primitivos são simples, diretos, rápidos, elegantes e eficientes. Agora, no exemplo do OP, é definitivamente uma boa ideia introduzir algumas novas classes. Mas a resposta para a pergunta do título (onde diz "tudo") é um retumbante não!
Lister
1
O @Max digitando os duplos não faz absolutamente nada para resolver o problema do OP.
Lister
1
@CongXu: Eu tinha quase o mesmo pensamento, mas mais no sentido "muitos códigos em qualquer idioma podem ter sido escritos por programadores com problemas em criar abstrações".
Doc Brown
1
Como diz o ditado "se você tem uma função com 17 parâmetros, provavelmente perdeu alguns".
Joris Timmermans 28/03

Respostas:

24

Sim definitivamente. Funções / métodos que levam muitos argumentos é um cheiro de código e indica pelo menos um dos seguintes:

  1. A função / método está fazendo muitas coisas ao mesmo tempo
  2. A função / método requer acesso a muitas coisas, porque é perguntar, não contar ou violar alguma lei de design de OO
  3. Os argumentos estão realmente intimamente relacionados

Se o último for o caso (e o seu exemplo certamente sugere isso), é hora de fazer uma parte dessa "abstração" de fantasia que as crianças legais estavam falando oh, apenas algumas décadas atrás. Na verdade, eu iria além do seu exemplo e faria algo como:

static GPSCoordinate getGPS(Plane plane, Angle3D gimbalAngle, GPSView view)

(Não estou familiarizado com o GPS, portanto essa provavelmente não é uma abstração correta; por exemplo, não entendo o que o zoom tem a ver com uma função chamada getGPS.)

Por favor, prefira aulas como PixelCoordinateacima std::pair<T, T>, mesmo que as duas tenham os mesmos dados. Ele agrega valor semântico, além de um pouco de segurança extra de tipo (o compilador impedirá que você passe de a ScreenCoordinatepara, PixelCoordinatemesmo que ambos sejam pairs.

congusbongus
fonte
1
para não mencionar que você pode passar as estruturas maiores, de referência para esse pouco de micro-otimização todos os miúdos legal está tentando fazer, mas realmente não deveria
aberração catraca
6

Isenção de responsabilidade: eu prefiro C a C ++

Em vez de classes, eu usaria estruturas. Este ponto é pedante, na melhor das hipóteses, uma vez que estruturas e classes são quase idênticas (veja aqui um gráfico simples ou essa pergunta do SO ), mas acho que há mérito na diferenciação.

Os programadores C estão familiarizados com estruturas como blocos de dados que têm um significado maior quando agrupados, enquanto que quando vejo um clasas, presumo que exista algum estado interno e métodos para manipular esse estado. Uma coordenada GPS não tem estado, apenas dados.

Da sua pergunta, parece que é exatamente isso que você está procurando, um contêiner geral, não uma estrutura estável. Como os outros mencionaram, eu não me incomodaria em colocar o Zoom em uma classe própria; Pessoalmente, odeio aulas criadas para conter um tipo de dados (meus professores adoram criar Heighte dar Idaulas).

Se houver classes para tudo, seria impossível passar um ZoomLevel no local em que um objeto Weight era esperado; portanto, seria mais difícil fornecer argumentos errados para uma função

Eu não acho que isso seja um problema. Se você reduzir o número de parâmetros agrupando as coisas óbvias como você fez, os cabeçalhos devem ser suficientes para evitar esses problemas. Se você estiver realmente preocupado, separe as primitivas com uma estrutura, que deve fornecer erros de compilação para erros comuns. É fácil trocar dois parâmetros um pelo outro, mas mais difícil se eles estiverem mais afastados.

Da mesma forma, algumas operações ilegais causariam erros de compilação, como adicionar um GPSCoordinate a um ZoomLevel ou outro GPSCoordinate

Bom uso de sobrecargas do operador.

As operações legais serão fáceis de representar e tipificar com segurança. subtrair duas coordenadas GPS resultaria em um deslocamento GPS

Também é bom o uso de sobrecargas do operador.

Eu acho que você está no caminho certo. Outra coisa a considerar é como isso se encaixa no restante do programa.

beatgammit
fonte
3

O uso de tipos dedicados tem a vantagem de ser muito mais difícil estragar a ordem dos argumentos. A primeira versão da sua função de exemplo, por exemplo, começa com uma cadeia de 9 duplos. Quando alguém usa essa função, é muito fácil estragar a ordem e passar os ângulos do cardan no lugar das coordenadas do GPS e vice-versa. Isso pode levar a erros muito estranhos e difíceis de rastrear. Quando você passa apenas três argumentos com tipos diferentes, esse erro pode ser detectado pelo compilador.

Na verdade, consideraria ir um passo adiante e introduzir tipos tecnicamente idênticos, mas com um significado semântico diferente. Por exemplo, tendo uma classe PlaneAngle3de uma classe separada GimbalAngle3d, embora ambos sejam uma coleção de três duplos. Isso tornaria impossível usar um como o outro, a menos que seja intencional.

Eu recomendaria o uso de typedefs, que são apenas aliases para tipos primitivos ( typedef double ZoomLevel), não apenas por esse motivo, mas também por outro: permite facilmente alterar o tipo posteriormente. Quando um dia você teve a ideia de que o uso floatpoderia ser tão bom quanto eficiente, doublemas com mais memória e CPU, basta mudar isso em um só lugar, e não em dozends.

Philipp
fonte
+1 para a sugestão typedef. Oferece o melhor dos dois mundos: tipos e objetos primitivos.
Karl Bielefeldt
Não é mais difícil alterar o tipo de um membro da classe do que um typedef; ou você quis dizer em comparação com os primitivos?
MaHuJa
2

Ótimas respostas aqui já, mas há uma coisa que gostaria de acrescentar:

Usar PixelCoordinatee PixelSizeclasse torna as coisas muito específicas. Um general Coordinateou Point2Dclasse provavelmente pode atender melhor às suas necessidades. A Point2Ddeve ser uma classe implementando as operações de ponto / vetor mais comuns. Você poderia implementar essa classe um mesmo genericamente ( Point2D<T>onde T pode ser intou doubleou qualquer outra coisa).

As operações de ponto / vetor 2D são tão comuns em muitas tarefas de programação em geometria 2D que, para minha experiência, essa decisão aumentará muito a chance de reutilizar as operações 2D existentes. Você vai evitar a necessidade de re-implementá-las para cada PixelCoordinate, GPSCoordinate"tomamos" class coordenada x novamente e novamente.

Doc Brown
fonte
1
você também pode fazer struct PixelCoordinate:Point2D<int> se você quer segurança tipo adequado
catraca aberração
0

Então, eu quero refatorá-lo para algo parecido com isto:

static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
                        PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)

Isso me parece significativamente mais legível e seguro do que o primeiro método.

É [mais legível]. Também é uma boa ideia.

Mas faz sentido criar as classes PixelCoordinate e PixelSize?

Se eles representam conceitos diferentes, faz sentido (ou seja, "sim").

Ou seria melhor usar std :: pair para cada um.

Você poderia começar fazendo typedef std::pair<int,int> PixelCoordinate, PixelSize;. Dessa forma, você cobre a semântica (você está trabalhando com coordenadas e tamanhos de pixel, não com std :: pairs no código do cliente) e, se precisar estender, basta substituir o typedef por uma classe e o código do cliente deve requer alterações mínimas.

E faz sentido ter uma classe ZoomLevel ou devo usar apenas uma dupla?

Você também pode digitá-lo, mas eu usaria um double até precisar de uma funcionalidade associada a ele que não exista por um duplo (por exemplo, ter um ZoomLevel::defaultvalor exigiria que você definisse uma classe, mas até ter algo como mantenha um duplo).

Minha intuição por trás do uso de classes para tudo é baseada nessas suposições [omitidas por brevidade]

São argumentos fortes (você deve sempre se esforçar para tornar suas interfaces fáceis de usar corretamente e difíceis de usar incorretamente).

No entanto, a maioria dos códigos C ++ que eu vi usa muitos tipos primitivos, e imagino que deve haver uma boa razão para isso.

Há razões; em muitos casos, esses motivos não são bons. Um motivo válido para fazer isso pode ser o desempenho, mas isso significa que você deve ver esse tipo de código como uma exceção, não como uma regra.

É uma boa ideia usar objetos para qualquer coisa, ou tem desvantagens das quais não estou ciente?

Geralmente, é uma boa idéia garantir que, se seus valores sempre apareçam juntos (e não façam sentido à parte), você os agrupe (pelos mesmos motivos mencionados em sua postagem).

Ou seja, se você possui coordenadas que sempre possuem x, ye z, é muito melhor ter uma coordinateclasse do que transmitir três parâmetros em todos os lugares.

utnapistim
fonte