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 Person
tem o atributo age
, ele afirma que deve ser impossível dizer, a partir da notação, se Person.age
corresponde internamente a algo como return current_year - self.birth_date
ou simplesmente return self.age
, onde self.age
foi 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 Person
objetos, 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?
fonte
Respostas:
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:
ou:
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:
ou
ou pode ser:
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.
fonte
// O(n) Traverses the entire user list.
len
nã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)O(n)
?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
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.
fonte
Você pode escrever se uma determinada chamada é cara ou não. Melhor, use uma convenção de nomenclatura, como
getAge
acesso rápido e /loadAge
oufetchAge
pesquisa 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.
fonte
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.
fonte
Get
mé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.get
método é equivalente a um acesso de atributo e, portanto, não é caro.É 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_method
einstance.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.attribute
einstance.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.
fonte
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.
fonte
string.length
serão recalculadas toda vez que forem alteradas.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.
fonte
Acho que você o entendeu corretamente, mas também acho que você tem um bom argumento. se
Person.age
for 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.age
retornar um valor armazenado em cache na classe, mas isso nem sempre é apropriado e parece ser complicado demais coisas, na minha opinião.fonte
age
de aPerson
, 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).Person
classe, mas acho que a pergunta foi planejada como mais geral e issoPerson.age
foi 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.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
, eWidth
propriedades 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 comofinal
campos 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
float
oudouble
nãoint
), pode ser necessário documentar quais propriedades "definem" o valor de uma classe. Por exemplo, mesmo que oLeft
plusWidth
deva ser igualRight
, a matemática de ponto flutuante geralmente é inexata. Por exemplo, suponhaRectangle
que um que usa tipoFloat
aceitaLeft
eWidth
como parâmetros construtores seja construído comLeft
dados como1234567f
eWidth
como1.1f
. A melhorfloat
representação da soma é 1234568.125 [que pode ser exibida como 1234568.13]; o próximo menorfloat
seria 1234568.0. Se a turma realmente armazenarLeft
eWidth
, ele pode relatar o valor da largura conforme especificado. Se, no entanto, o construtor computasseRight
com base no repasseLeft
eWidth
, e posteriormente computado comWidth
base noLeft
eRight
, reportaria a largura1.25f
mais do que o repasse1.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,
SetLeftAndWidth
ouSetLeftAndRight
, ou então deixar claro que propriedades estão sendo especificado e que estão mudando (por exemploMoveRightEdgeToSetWidth
,ChangeWidthToSetLeftEdge
ouMoveShapeToSetRightEdge
) .À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.
fonte
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:
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.
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.
fonte
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)
fonte
Como a resposta aceita chega à conclusão:
e o código auto-documentado é considerado melhor que a documentação , segue-se que o nome do método deve indicar quaisquer resultados de desempenho incomuns.
Então ainda
Person.age
parareturn current_year - self.birth_date
mas se o método usa um loop para calcular a idade (sim):Person.calculateAge()
fonte