A documentação no POO deve evitar especificar se um "getter" realiza ou não algum cálculo?


39

O programa de CS da minha escola evita qualquer menção à programação orientada a objetos, por isso tenho lido algumas coisas sozinho para complementá-la - especificamente, Construção de Software Orientada a Objetos , de Bertrand Meyer.

Meyer defende repetidamente que as classes devem ocultar o máximo de informações possível sobre sua implementação, o que faz sentido. Em particular, ele argumenta repetidamente que atributos (isto é, propriedades estáticas e não computadas de classes) e rotinas (propriedades de classes que correspondem a chamadas de função / procedimento) devem ser indistinguíveis entre si.

Por exemplo, se uma classe Persontem o atributo age, ele afirma que deve ser impossível dizer, a partir da notação, se Person.agecorresponde internamente a algo como return current_year - self.birth_dateou simplesmente return self.age, onde self.agefoi definido como um atributo constante. Isso faz sentido para mim. No entanto, ele continua reivindicando o seguinte:

A documentação padrão do cliente para uma classe, conhecida como forma abreviada da classe, será criada para não revelar se um determinado recurso é um atributo ou uma função (nos casos em que poderia ser).

isto é, ele afirma que mesmo a documentação da classe deve evitar especificar se um "getter" executa ou não algum cálculo.

Isso eu não sigo. A documentação não é o único local em que seria importante informar os usuários dessa distinção? Se eu fosse projetar um banco de dados cheio de Personobjetos, não seria importante saber se Person.ageé uma chamada cara ou não , para que eu pudesse decidir se implementaria algum tipo de cache para ela? Eu entendi mal o que ele está dizendo ou ele é apenas um exemplo particularmente extremo da filosofia de design do OOP?


1
Pergunta interessante. Perguntei sobre algo muito semelhante muito recentemente: como eu projetaria uma interface para que fique claro quais propriedades podem alterar seu valor e quais permanecerão constantes? . E recebi uma boa resposta apontando para a documentação, ou seja, exatamente contra o que Bertrand Meyer parece argumentar.
stakx

Eu não li o livro. Meyer dá algum exemplo do estilo de documentação que ele recomenda? Acho difícil imaginar o que você descreveu trabalhando para qualquer idioma.
user16764

1
@PatrickCollins Sugiro que você leia 'execução no reino dos substantivos' e fique por trás do conceito de verbos e substantivos aqui. Em segundo lugar OOP não é sobre getters e setters, sugiro Alan Kay (inventor do OOP): programação e escala
AndreasScheinert

@AndreasScheinert - você está se referindo a isso ? Eu ri do "tudo pela falta de um prego de ferradura", mas parece ser um discurso retórico sobre os males da programação orientada a objetos.
Patrick Collins

1
@PatrickCollins sim, este: steve-yegge.blogspot.com/2006/03/… ! Dá alguns pontos para refletir, os outros são: você deve transformar seus objetos em estruturas de dados (ab) usando setters.
AndreasScheinert

Respostas:


58

Não acho que o argumento de Meyer é que você não deva contar ao usuário quando tiver uma operação cara. Se sua função atingir o banco de dados ou fazer uma solicitação a um servidor da web e passar várias horas computando, outro código precisará saber disso.

Mas o codificador que usa sua classe não precisa saber se você implementou:

return currentAge;

ou:

return getCurrentYear() - yearBorn;

As características de desempenho entre essas duas abordagens são tão mínimas que não devem importar. O codificador que usa sua classe realmente não deve se importar com o que você tem. Esse é o ponto de Meyer.

Mas nem sempre é o caso, por exemplo, suponha que você tenha um método de tamanho em um contêiner. Isso poderia ser implementado:

return size;

ou

return end_pointer - start_pointer;

ou pode ser:

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

A diferença entre os dois primeiros realmente não deveria importar. Mas o último pode ter sérias ramificações de desempenho. É por isso que o STL, por exemplo, diz que .size()é O(1). Ele não documenta exatamente como o tamanho é calculado, mas fornece as características de desempenho.

Então : documente problemas de desempenho. Não documente detalhes da implementação. Eu não me importo com o modo como std :: sort classifica minhas coisas, desde que faça isso de maneira adequada e eficiente. Sua classe também não deve documentar como calcula as coisas, mas se algo tiver um perfil de desempenho inesperado, documente isso.


4
Além disso: documente primeiro a complexidade do tempo / espaço e, em seguida, explique por que uma função tem essas propriedades. Exemplo:// O(n) Traverses the entire user list.
Jon Purdy

2
= (Algo tão trivial quanto o Python lennão consegue fazer isso ... (Em pelo menos algumas situações, é O(n), como aprendemos em um projeto na faculdade, quando sugeri armazenar o comprimento em vez de recalcular cada iteração de loop)
Izkata

@ Izkata, curioso. Você se lembra o que era a estrutura O(n)?
Winston Ewert

@WinstonEwert Infelizmente não. Isso foi 4+ anos atrás em um projeto de mineração de dados, e eu só tinha sugerido o meu amigo em um palpite, porque eu estava trabalhando com C em outra classe ..
Izkata

1
@ JonPurdy Eu acrescentaria que, no código comercial normal, provavelmente não faz sentido especificar a complexidade do grande O. Por exemplo, um acesso ao banco de dados O (1) provavelmente será muito mais lento que o percurso da lista na memória O (n), portanto, documente o que importa. Mas há certamente casos em que a complexidade da documentação é muito importante (coleções ou outro código pesado de algoritmo).
svick

16

Do ponto de vista acadêmico ou purista de CS, é claro que não é possível descrever na documentação nada sobre os aspectos internos da implementação de um recurso. Isso ocorre porque, idealmente, o usuário de uma classe não deve fazer suposições sobre a implementação interna da classe. Se a implementação mudar, o usuário idealmente não notará isso - o recurso cria uma abstração e os internos devem ser mantidos completamente ocultos.

No entanto, a maioria dos programas do mundo real sofre com a "Lei das abstrações com vazamentos" , de Joel Spolsky , que diz

"Todas as abstrações não triviais, até certo ponto, são vazadas".

Isso significa que é praticamente impossível criar uma abstração de caixa preta completa de recursos complexos. E um sintoma típico disso são problemas de desempenho. Portanto, para programas do mundo real, pode se tornar muito importante quais chamadas são caras e quais não são, e uma boa documentação deve incluir essas informações (ou deve dizer onde o usuário de uma classe pode fazer suposições sobre desempenho e onde não )

Portanto, meu conselho é: inclua as informações sobre possíveis chamadas caras se você escrever documentos para um programa do mundo real e exclua-as para um programa que você está escrevendo apenas para fins educacionais do seu curso de CS, considerando que todas as considerações de desempenho devem ser mantidas intencionalmente fora do escopo.


+1, mais a maior parte da documentação criada é para o próximo programador manter seu projeto, não o próximo programador para usá- lo.
jmoreno

12

Você pode escrever se uma determinada chamada é cara ou não. Melhor, use uma convenção de nomenclatura, como getAgeacesso rápido e / loadAgeou fetchAgepesquisa cara. Você definitivamente deseja informar ao usuário se o método está executando algum IO.

Cada detalhe que você fornece na documentação é como um contrato que deve ser respeitado pela classe. Deve informar sobre comportamentos importantes. Frequentemente, você verá indicações de complexidades com grande notação O. Mas você geralmente quer ser curto e direto ao ponto.


1
+1 por mencionar que a documentação faz tanto parte do contrato de uma classe quanto sua interface.
Bart van Ingen Schenau

Eu apoio isso. Além disso, em geral, tentando minimizar a necessidade de getters, fornecendo métodos com comportamento.
sevenforce

9

Se eu fosse projetar um banco de dados preenchido com objetos Person, não seria importante saber se Person.age é ou não uma ligação cara?

Sim.

É por isso que às vezes uso Find()funções para indicar que a chamada pode demorar um pouco. Isso é mais uma convenção do que qualquer outra coisa. O tempo que leva para uma função ou atributo de retorno não faz diferença para o programa (embora possa para o usuário), embora entre os programadores não é uma expectativa de que, se ele é declarado como um atributo, o custo para chamá-lo deve ser baixo.

De qualquer forma, deve haver informações suficientes no próprio código para deduzir se algo é uma função ou atributo, por isso não vejo a necessidade de dizer isso na documentação.


4
+1: essa convenção é idiomática em muitos lugares. Além disso, a documentação deve ser feita no nível da interface - nesse ponto, você não sabe como o Person.Age é implementado.
Telastyn

@Telastyn: Eu nunca pensei sobre documentação dessa maneira; isto é, isso deve ser feito no nível da interface. Parece óbvio agora. +1 para esse comentário valioso.
stakx

Eu gosto muito dessa resposta. Um exemplo perfeito do que você descreve que o desempenho não é uma preocupação para o próprio programa seria se Person fosse uma entidade recuperada de um serviço RESTful. GET é inerente, mas não é aparente se isso será barato ou caro. Claro que isso não é necessariamente POO, mas o ponto é o mesmo.
maple_shaft

+1 para usar Getmétodos sobre atributos para indicar uma operação mais pesada. Já vi código suficiente em que os desenvolvedores assumem que uma propriedade é apenas um acessador e a usam várias vezes em vez de salvar o valor em uma variável local e, assim, executar um algoritmo muito complexo mais de uma vez. Se não houver uma convenção para não implementar essas propriedades e a documentação não indicar a complexidade, desejo a quem tiver que manter boa sorte esse aplicativo.
enzi

De onde vem essa convenção? Pensando em Java, eu esperaria o contrário: o getmétodo é equivalente a um acesso de atributo e, portanto, não é caro.
sevenforce 12/09/2013

3

É importante notar que a primeira edição deste livro foi escrita em 1988, nos primeiros dias da OOP. Essas pessoas estavam trabalhando com linguagens orientadas a objetos mais puramente usadas hoje em dia. Nossas linguagens OO mais populares atualmente - C ++, C # e Java - apresentam algumas diferenças bastante significativas em relação à maneira como as linguagens iniciais, mais puramente OO, funcionavam.

Em uma linguagem como C ++ e Java, você deve distinguir entre acessar um atributo e uma chamada de método. Há um mundo de diferença entre instance.getter_methode instance.getter_method(). Um realmente obtém seu valor e o outro não.

Ao trabalhar com uma linguagem OO mais pura, da persuasão Smalltalk ou Ruby (que parece ser a linguagem Eiffel usada neste livro), torna-se um conselho perfeitamente válido. Esses idiomas implicitamente chamarão métodos para você. Não há diferença entre instance.attributee instance.getter_method.

Eu não suaria esse ponto nem o consideraria dogmaticamente. A intenção é boa - você não quer que os usuários da sua classe se preocupem com detalhes irrelevantes da implementação - mas não se traduz de maneira limpa na sintaxe de muitos idiomas modernos.


1
Ponto muito importante para considerar o ano em que a sugestão foi feita. Nit: Smalltalk e Simula remontam aos anos 60 e 70, então 88 dificilmente são "primeiros dias".
Luser droog 27/08/13

2

Como usuário, você não precisa saber como algo é implementado.

Se o desempenho é um problema, algo precisa ser feito dentro da implementação da classe, não em torno dela. Portanto, a ação correta é corrigir a implementação da classe ou registrar um bug no mantenedor.


3
É sempre o caso de um método computacionalmente caro ser um bug? Como um exemplo trivial, digamos que estou preocupado em resumir os comprimentos de uma matriz de seqüências de caracteres. Internamente, não sei se as strings no meu idioma são do tipo Pascal ou C. No primeiro caso, como as strings "conhecem" seu comprimento, posso esperar que meu loop de soma e comprimento demore um tempo linear, dependendo do número de strings. Também devo saber que as operações que alteram o comprimento das seqüências terão uma sobrecarga associada a elas, pois string.lengthserão recalculadas toda vez que forem alteradas.
22413 Patrick Collins

3
No último caso, como a string não "conhece" seu comprimento, posso esperar que meu loop de soma e comprimento demore um tempo quadrático (que depende tanto do número de strings quanto de seus comprimentos), mas das operações que alteram o comprimento de cordas será mais barato. Nenhuma dessas implementações está errada e nem mereceria um relatório de erro, mas elas exigem estilos de codificação ligeiramente diferentes para evitar soluços inesperados. Não seria mais fácil se o usuário tivesse pelo menos uma vaga idéia do que estava acontecendo?
Patrick Collins

Portanto, se você souber que a classe string implementa o estilo C, você escolherá uma maneira de codificar levando esse fato em consideração. Mas e se a próxima versão da classe string implementar a nova representação no estilo Foo? Você alterará seu código adequadamente ou aceitará a perda de desempenho causada por suposições falsas em seu código?
Mouviciel 26/08/13

Entendo. Portanto, a resposta do OO a "Como posso extrair um desempenho extra do meu código, contando com uma implementação específica?" é "Você não pode". E a resposta para "Meu código é mais lento do que eu esperava, por quê?" é "Ele precisa ser reescrito". Essa é mais ou menos a ideia?
Patrick Collins

2
@PatrickCollins A resposta do OO depende de interfaces e não de implementações. Não use uma interface que não inclua garantias de desempenho como parte da definição da interface (como o exemplo de C ++ 11 List.size sendo garantido O (1)). Não requer a inclusão de detalhes de implementação na definição da interface. Se o seu código for mais lento do que você gostaria, existe outra resposta que você precisará alterá-lo para ser mais rápido (depois de criar um perfil para determinar gargalos)?
stonemetal

2

Qualquer documentação orientada a programadores que não informe os programadores sobre o custo de complexidade de rotinas / métodos é falha.

  • Estamos procurando produzir métodos sem efeitos colaterais.

  • Se a execução de um método tiver complexidade de tempo de execução e / ou complexidade de memória que não seja O(1), em ambientes com memória ou tempo limitados, pode ser considerado como tendo efeitos colaterais .

  • O princípio da menor surpresa é violado se um método faz algo completamente inesperado - nesse caso, consumindo memória ou perdendo tempo da CPU.


1

Acho que você o entendeu corretamente, mas também acho que você tem um bom argumento. se Person.agefor implementado com um cálculo caro, acho que também gostaria de ver isso na documentação. Pode fazer a diferença entre chamá-lo repetidamente (se for uma operação barata) ou chamá-lo uma vez e armazenar em cache o valor (se for caro). Não tenho certeza, mas acho que nesse caso Meyer pode concordar que um aviso na documentação deva ser incluído.

Outra maneira de lidar com isso pode ser a introdução de um novo atributo cujo nome implica que um cálculo demorado pode ocorrer (como Person.ageCalculatedFromDB) e, em seguida, Person.ageretornar um valor armazenado em cache na classe, mas isso nem sempre é apropriado e parece ser complicado demais coisas, na minha opinião.


3
Também se pode argumentar que, se você precisar conhecer o agede a Person, deve chamar o método para obtê-lo independentemente. Se os chamadores começarem a fazer coisas muito inteligentes pela metade para evitar ter que fazer o cálculo, correm o risco de fazer com que suas implementações não funcionem corretamente porque ultrapassaram um limite de aniversário. As implementações caras na classe se manifestam como problemas de desempenho que podem ser erradicados pela criação de perfil e melhorias como o cache podem ser feitas na classe, onde todos os chamadores verão os benefícios (e os resultados corretos).
Blrfl

1
@ Blrfl: bem, sim, o cache deve ser feito na Personclasse, mas acho que a pergunta foi planejada como mais geral e isso Person.agefoi apenas um exemplo. Provavelmente, há alguns casos em que faria mais sentido para o chamador escolher - talvez o receptor tenha dois algoritmos diferentes para calcular o mesmo valor: um rápido, mas impreciso, um muito mais lento, mas mais preciso (a renderização 3D vem à mente como um único local). onde isso pode acontecer), e a documentação deve mencionar isso.
FrustratedWithFormsDesigner

Dois métodos que fornecem resultados diferentes é um caso de uso diferente do que quando você espera a mesma resposta a cada vez.
Blrfl

0

A documentação para classes orientadas a objetos geralmente envolve uma troca entre dar aos mantenedores da classe flexibilidade para mudar seu design, ao invés de permitir que os consumidores da classe façam pleno uso de seu potencial. Se uma classe imutável irá ter um número de propriedades que terá um determinado exacta relação uns com os outros (por exemplo, a Left, Right, eWidthpropriedades de um retângulo alinhado à grade com coordenadas inteiras), pode-se projetar a classe para armazenar qualquer combinação de duas propriedades e calcular a terceira, ou pode-se projetar para armazenar as três. Se nada sobre a interface esclarecer quais propriedades estão armazenadas, o programador da classe poderá alterar o design caso isso seja útil por algum motivo. Por outro lado, se, por exemplo, duas das propriedades são expostas como finalcampos e a terceira não, as versões futuras da classe sempre terão que usar as mesmas duas propriedades como sendo a "base".

Se as propriedades não tiverem um relacionamento exato (por exemplo, porque são floatou doublenão int), pode ser necessário documentar quais propriedades "definem" o valor de uma classe. Por exemplo, mesmo que o Leftplus Widthdeva ser igual Right, a matemática de ponto flutuante geralmente é inexata. Por exemplo, suponha Rectangleque um que usa tipo Floataceita Lefte Widthcomo parâmetros construtores seja construído com Leftdados como 1234567fe Widthcomo 1.1f. A melhor floatrepresentação da soma é 1234568.125 [que pode ser exibida como 1234568.13]; o próximo menor floatseria 1234568.0. Se a turma realmente armazenar LefteWidth, ele pode relatar o valor da largura conforme especificado. Se, no entanto, o construtor computasse Rightcom base no repasse Lefte Width, e posteriormente computado com Widthbase no Lefte Right, reportaria a largura 1.25fmais do que o repasse 1.1f.

Com classes mutáveis, as coisas podem ser ainda mais interessantes, pois uma alteração em um dos valores inter-relacionados implicará uma alteração em pelo menos um outro, mas nem sempre é claro qual. Em alguns casos, pode ser melhor para evitar ter métodos que "set" uma única propriedade como tal, mas em vez disso quer ter métodos para por exemplo, SetLeftAndWidthou SetLeftAndRight, ou então deixar claro que propriedades estão sendo especificado e que estão mudando (por exemplo MoveRightEdgeToSetWidth, ChangeWidthToSetLeftEdgeou MoveShapeToSetRightEdge) .

Às vezes, pode ser útil ter uma classe que controla quais valores de propriedades foram especificados e quais foram calculados a partir de outros. Por exemplo, uma classe "momento no tempo" pode incluir um horário absoluto, um horário local e um deslocamento de fuso horário. Como em muitos desses tipos, dadas duas informações, uma pode calcular a terceira. Saber quaisuma parte da informação foi calculada, no entanto, às vezes pode ser importante. Por exemplo, suponha que um evento seja registrado como tendo ocorrido às "17:00 UTC, fuso horário -5, horário local 12:00" e depois descobre que o fuso horário deveria ter sido -6. Se alguém souber que o UTC foi gravado em um servidor, o registro deve ser corrigido para "18:00 UTC, fuso horário -6, horário local 12:00"; se alguém digitar a hora local com um relógio, deve ser "17:00 UTC, fuso horário -6, hora local 11:00". Sem saber se a hora global ou local deve ser considerada "mais crível", no entanto, não é possível saber qual correção deve ser aplicada. Se, no entanto, o registro controlasse o horário especificado, as alterações no fuso horário poderiam deixá-lo em paz enquanto o outro era alterado.


0

Todas essas regras sobre como ocultar informações nas classes fazem todo o sentido, na suposição de que é preciso proteger contra alguém entre os usuários da classe que cometerá o erro de criar uma dependência na implementação interna.

Não há problema em criar essa proteção, se a classe tiver um público assim. Mas quando o usuário escreve uma chamada para uma função da sua classe, ele confia em você com a conta bancária do tempo de execução.

Aqui está o tipo de coisa que vejo muito:

  1. Os objetos têm um bit "modificado" dizendo se estão, em certo sentido, desatualizados. Simples o suficiente, mas eles têm objetos subordinados; portanto, é fácil deixar "modificado" ser uma função que resume todos os objetos subordinados. Então, se houver várias camadas de objetos subordinados (às vezes compartilhando o mesmo objeto mais de uma vez), simples "Get" s da propriedade "modificada" podem levar uma fração saudável do tempo de execução.

  2. Quando um objeto é de alguma forma modificado, supõe-se que outros objetos espalhados pelo software precisem ser "notificados". Isso pode ocorrer em várias camadas da estrutura de dados, janelas, etc., escritas por diferentes programadores e, às vezes, repetindo-se em infinitas recursões que precisam ser protegidas. Mesmo que todos os escritores desses manipuladores de notificação tenham um cuidado razoável para não perder tempo, toda a interação composta pode acabar usando uma fração imprevisível e dolorosamente grande do tempo de execução, e a suposição de que isso é simplesmente "necessário" é feita com alegria.

Então, eu gosto de ver aulas que apresentam uma interface abstrata limpa e agradável para o mundo exterior, mas eu gosto de ter uma noção de como elas funcionam, apenas para entender qual trabalho elas estão me salvando. Mas além disso, costumo sentir que "menos é mais". As pessoas estão tão apaixonadas pela estrutura de dados que acham que mais é melhor, e quando eu ajusto o desempenho, a razão massiva universal dos problemas de desempenho é a adesão servil às estruturas de dados inchadas, criadas da maneira como as pessoas são ensinadas.

Então vá entender.


0

Adicionar detalhes de implementação como "calcular ou não" ou "informações de desempenho" torna mais difícil manter o código e o documento sincronizados .

Exemplo:

Se você possui um método "caro de desempenho", deseja documentar "caro" também para todas as classes que usam o método? e se você alterar a implementação para não ser mais cara. Deseja atualizar essas informações também para todos os consumidores?

É claro que é bom para um mantenedor de código obter todas as informações importantes da documentação do código, mas eu não gosto de documentação que afirma algo que não é mais válido (fora de sincronia com o código)


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.