desempenho versus reutilização

8

Como posso escrever funções reutilizáveis ​​sem sacrificar o desempenho? Estou enfrentando repetidamente a situação em que quero escrever uma função de uma maneira que a reutilize (por exemplo, não faz suposições sobre o ambiente de dados), mas sabendo o fluxo geral do programa, sei que não é a mais eficaz método. Por exemplo, se eu quiser escrever uma função que valide um código de ações, mas seja reutilizável, não posso simplesmente assumir que o conjunto de registros está aberto. No entanto, se eu abrir e fechar o conjunto de registros sempre que a função for chamada, o desempenho atingido ao percorrer milhares de linhas poderá ser enorme.

Portanto, para o desempenho, posso ter:

Function IsValidStockRef(strStockRef, rstStockRecords)
    rstStockRecords.Find ("stockref='" & strStockRef & "'")
    IsValidStockRef = Not rstStockRecords.EOF
End Function

Mas, para reutilização, eu precisaria de algo como o seguinte:

Function IsValidStockRef(strStockRef)
    Dim rstStockRecords As ADODB.Recordset

    Set rstStockRecords = New ADODB.Recordset
    rstStockRecords.Open strTable, gconnADO

    rstStockRecords.Find ("stockref='" & strStockRef & "'")
    IsValidStockRef = Not rstStockRecords.EOF

    rstStockRecords.Close
    Set rstStockRecords = Nothing
End Function

Estou preocupado que o impacto no desempenho de abrir e fechar esse conjunto de registros quando chamado de dentro de um loop em milhares de linhas / registros seja severo, mas o uso do primeiro método torne a função menos reutilizável.

O que devo fazer?

Caltor
fonte

Respostas:

13

Você deve fazer o que produzir o maior valor comercial nessa situação.

Escrever software é sempre uma troca. Quase nunca todas as metas válidas (manutenção, desempenho, clareza, concisão, segurança etc. etc.) estão completamente alinhadas. Não caia na armadilha das pessoas míopes que consideram uma dessas dimensões primordiais e lhe dizem para sacrificar tudo por ela.

Em vez disso, entenda quais riscos e quais benefícios cada alternativa oferece, quantize-os e siga o que maximiza o resultado. (Você não precisa realmente fazer estimativas numéricas, é claro. É suficiente ponderar fatores como "usar essa classe significa nos prender nesse algoritmo de hash, mas como não estamos usando para se proteger contra ataques maliciosos , apenas para conveniência, este é bom o suficiente para que possamos ignorar a chance de 1: 1.000.000.000 de uma colisão acidental ".)

O mais importante é lembrar que são compensações; nenhum princípio isolado justifica tudo para satisfazer e nenhuma decisão, uma vez tomada, precisa permanecer eternamente . Você pode sempre ter que revisar retrospectivamente quando as circunstâncias mudarem de uma maneira que você não previa. Isso é uma dor, mas não tão doloroso quanto tomar a mesma decisão sem retrospectiva.

Kilian Foth
fonte
2
Embora o que você diz seja verdade em geral, algo deve ser dito para escrever código para proteger contra qualquer caso possível versus pré-requisitos. Na minha humilde opinião, não há nada errado em simplesmente esperar que o conjunto de registros já esteja aberto quando chamado, com documentação suficiente. Se houver algo, se este for um método em uma biblioteca, faça uma verificação rápida se estiver aberto e, se não estiver, gere uma exceção. Não há necessidade de "fazê-lo funcionar" em qualquer cenário possível.
21415 Neil
6

Nenhum deles parece mais reutilizável que o outro. Eles apenas parecem estar em diferentes níveis de abstração . A primeira é para chamar código que entenda o sistema de estoque intimamente o suficiente para saber que validar uma referência de estoque significa procurar através de uma Recordsetconsulta com algum tipo. O segundo é para chamar o código que apenas quer saber se um código de ações é válido ou não e não tem interesse em se preocupar com a forma como você verifica isso.

Mas, como na maioria das abstrações , essa é "vazada". Nesse caso, a abstração vaza através de seu desempenho - o código de chamada não pode ignorar completamente como a validação é implementada, porque, se o fizesse, poderia chamar essa função milhares de vezes conforme você descrevia e degradar seriamente o desempenho geral.

Por fim, se você precisar escolher entre código mal abstraído e desempenho inaceitável, precisará escolher o código mal abstraído. Mas primeiro, você deve procurar uma solução melhor - um compromisso que mantenha um desempenho aceitável e apresente uma abstração decente (se não ideal). Infelizmente, não conheço muito bem o VBA, mas em uma linguagem OO, meu primeiro pensamento seria atribuir uma classe ao código de chamada com métodos como:

BeginValidation()
IsValidStockRef(strStockRef)
EndValidation()

Aqui, seus métodos Begin...e End...fazem o gerenciamento único do ciclo de vida do conjunto de registros, que IsValidStockRefcorresponde à sua primeira versão, mas usa esse conjunto de registros pelo qual a própria classe assumiu a responsabilidade, em vez de a transmitir. O código de chamada chamaria o Begin...e End...métodos fora do loop e o método de validação dentro.

Nota: Este é apenas um exemplo ilustrativo muito aproximado e pode ser considerado uma primeira passagem na refatoração. Os nomes provavelmente poderiam usar ajustes e, dependendo do idioma, deveria haver uma maneira mais limpa ou idiomática de fazê-lo (C #, por exemplo, poderia usar o construtor para começar e Dispose()terminar). Idealmente, o código que deseja apenas verificar se uma referência de estoque é válida não deveria, por si só, fazer nenhum gerenciamento do ciclo de vida.

Isso representa uma ligeira degradação da abstração que estamos apresentando: agora o código de chamada precisa saber o suficiente sobre validação para entender que é algo que requer algum tipo de configuração e desmontagem. Mas, em troca desse compromisso relativamente modesto, agora temos métodos que podem ser usados ​​facilmente chamando código, sem prejudicar nosso desempenho.

Ben Aaronson
fonte
Downvoter: Algum motivo em particular, por interesse?
precisa
Eu não era o menos favorável, mas vou adivinhar. BeginValidation,, EndValidatione IsValidStockReftenham um relacionamento especial entre si. O conhecimento desse relacionamento é mais complexo do que o conhecimento necessário para lidar diretamente com a RecordSet. E o conhecimento necessário para lidar com a RecordSeté mais amplamente aplicável.
Keen
@Cory Eu concordo até certo ponto, e minha mão foi forçada um pouco pela falta de conhecimento sobre o vba. Tentei apontar isso com a próxima frase, mas talvez minhas palavras não fossem claras ou fortes o suficiente. Eu fiz uma edição para tentar fazer isso um pouco mais clara
Ben Aaronson
Uma observação interessante, em C #, você deve usar a usinginstrução para fazer este trabalho. Em outros idiomas (aqueles que usam exceções de qualquer maneira), para fazer o mesmo trabalho que usingvocê precisaria try {} finally {}para garantir o descarte adequado e, mesmo assim, às vezes é impossível agrupar corretamente todo o código que possa throw. Esse é um problema em potencial com todas as soluções mencionadas aqui e também não tenho certeza de como isso deve ser resolvido no VBA.
Keen
@Cory: E em C ++, você simplesmente usaria RAII.
Deduplicator
3

Durante muito tempo, eu costumava implementar um sistema complicado de verificações para poder usar transações de banco de dados. A lógica da transação é a seguinte: abra uma transação, execute as operações do banco de dados, recupere o erro ou confirme com êxito. A complicação vem do que acontece quando você deseja que uma operação adicional seja executada na mesma transação. Você precisaria escrever um segundo método inteiramente que execute as duas operações ou poderia chamar o método original a partir de um segundo, abrindo uma transação apenas se uma ainda não tiver sido aberta e realizando / retrocedendo alterações apenas se você fosse o um para abrir a transação.

Por exemplo:

public void method1() {
    boolean selfOpened = false;
    if(!transaction.isOpen()) {
        selfOpened = true;
        transaction.open();
    }

    try {
        performDbOperations();
        method2();

        if(selfOpened) 
            transaction.commit();
    } catch (SQLException e) {
        if(selfOpened) 
            transaction.rollback();
        throw e;
    }
}

public void method2() {
    boolean selfOpened = false;
    if(!transaction.isOpen()) { 
        selfOpened = true;
        transaction.open();
    }

    try {
        performMoreDbOperations();

        if(selfOpened) 
            transaction.commit();
    } catch (SQLException e) {
        if(selfOpened) 
            transaction.rollback();
        throw e;
    }
}

Observe que não estou advogando o código acima por nenhum meio. Isso deve servir como um exemplo do que não fazer!

Pareceu bobagem criar um segundo método para executar a mesma lógica do primeiro, além de algo extra, mas eu queria poder chamar a seção API do banco de dados do programa e resolver os problemas lá. No entanto, enquanto isso resolveu parcialmente o meu problema, todo método que escrevi envolveu adicionar essa lógica detalhada de verificar se uma transação já está aberta e confirmar / retroceder alterações se meu método a abrisse.

O problema era conceitual. Eu não deveria ter tentado abraçar todos os cenários possíveis. A abordagem adequada foi abrigar a lógica de transação em um único método, usando um segundo método como parâmetro que executaria a lógica real do banco de dados. Essa lógica assume que a transação está aberta e nem realiza uma verificação. Esses métodos podem ser chamados em combinação para que esses métodos não sejam confusos com a lógica de transação desnecessária.

A razão pela qual mencionei isso é porque meu erro foi assumir que eu precisava fazer meu método funcionar em qualquer situação. Ao fazer isso, não apenas meu método chamado verificou se uma transação estava aberta, mas também aqueles que chamou. Nesse caso, não é um grande problema de desempenho, mas se, por exemplo, eu precisava verificar a existência de um registro no banco de dados antes de prosseguir, estaria verificando todos os métodos que o exigem, quando deveria ter assumido o tempo todo. o chamador deve estar ciente de que o registro deve existir. Se o método for chamado de qualquer maneira, esse é um comportamento indefinido e você não precisa se preocupar com o que acontece.

Em vez disso, você deve fornecer muita documentação e escrever o que você espera que seja verdadeiro antes que uma chamada seja feita ao seu método. Se for importante o suficiente, adicione-o como um comentário antes do método, para que não haja erros (o javadoc fornece um bom suporte para esse tipo de coisa em java).

Espero que ajude!

Neil
fonte
2

Você pode ter duas funções sobrecarregadas. Dessa forma, você pode usar os dois de acordo com a situação.

Você nunca pode (nunca vi isso acontecer) otimizar para tudo, então você precisa se contentar com alguma coisa. Escolha o que você acha que é mais importante.

cauchy
fonte
Infelizmente estou fazendo muito no VBA e sobrecarregar não é uma opção. Eu poderia usar um Optionalparâmetro para obter um efeito semelhante.
Caltor
2

2 funções: uma abre o conjunto de registros e o passa para uma função de análise de dados.

O primeiro pode ser ignorado se você já tiver um conjunto de registros aberto. O segundo pode assumir que será passado um conjunto de registros aberto, ignorando a origem e processando os dados.

Você tem desempenho e reutilização, então!

gbjbaanb
fonte
Não acho que seja necessário abrir o conjunto de registros para o chamador, mas, caso contrário, concordo.
21415 Neil
0

A otimização (além da micro-otimização) está diretamente em desacordo com a modularidade.

A modularidade funciona isolando o código do contexto global, enquanto a otimização do desempenho explora o contexto global para minimizar o que o código precisa fazer. A modularidade é o benefício do baixo acoplamento, enquanto (o potencial para) desempenho muito alto é o benefício do alto acoplamento.

A resposta é arquitetônica. Considere as partes do código que você deseja reutilizar. Talvez seja o componente de cálculo de preço ou a lógica de validação de configuração.

Em seguida, você deve escrever o código que interage com esse componente para reutilização. Dentro de um componente em que você nunca pode usar apenas parte do código, você pode otimizar o desempenho, pois sabe que ninguém mais o usará.

O truque é determinar quais são seus componentes.

tl; dr: entre componentes escreva com modularidade em mente, dentro componentes escreva com desempenho em mente.

Trenó
fonte
Modularidade e otimização não estão necessariamente em desacordo. Os compiladores modernos podem alinhar praticamente qualquer coisa em qualquer lugar, portanto, não importa o quão modular você escreva, desde que o compilador possa agrupá-lo em um "executável não modular", não há razão para que não seja tão rápido quanto o código que foi escrito não modular em primeiro lugar. Claro, nem todos os compiladores podem fazer isso muito bem, mas ...
leftaroundabout
@leftaroundabout Bem, eu quis dizer no nível do código fonte, mas você está muito certo. Não há razão para que um compilador suficientemente inteligente não possa substituir sua classificação de bolhas por uma classificação rápida!
Sled