Quais são os benefícios e as desvantagens inerentes ao uso de classes para encapsular algoritmos numéricos?

13

Muitos algoritmos usados ​​na computação científica têm uma estrutura inerente diferente dos algoritmos comumente considerados em formas menos intensivas em matemática de engenharia de software. Em particular, algoritmos matemáticos individuais tendem a ser altamente complexos, geralmente envolvendo centenas ou milhares de linhas de código, e, no entanto, não envolvem estado (ou seja, não estão agindo sobre uma estrutura de dados complexa) e podem ser resumidos - em termos de programação programática. interface - para uma única função que atua em uma matriz (ou duas).

Isso sugere que uma função, e não uma classe, é a interface natural para a maioria dos algoritmos encontrados na computação científica. No entanto, esse argumento oferece poucas informações sobre como a implementação de algoritmos complexos de múltiplas partes deve ser tratada.

Embora a abordagem tradicional tenha sido simplesmente ter uma função que chama várias outras funções, passando os argumentos relevantes ao longo do caminho, o OOP oferece uma abordagem diferente, na qual os algoritmos podem ser encapsulados como classes. Para maior clareza, ao encapsular um algoritmo em uma classe, quero dizer criar uma classe em que as entradas do algoritmo são inseridas no construtor da classe e, em seguida, um método público é chamado para realmente chamar o algoritmo. Essa implementação de multigrid no psuedocode C ++ pode parecer com:

class multigrid {
    private:
        x_, b_
        [grid structure]

        restrict(...)
        interpolate(...)
        relax(...)
    public:
        multigrid(x,b) : x_(x), b_(b) { }
        run()
}

multigrid::run() {
     [call restrict, interpolate, relax, etc.]
}

Minha pergunta é a seguinte: quais são os benefícios e as desvantagens desse tipo de prática em comparação com uma abordagem mais tradicional sem classes? Existem problemas de extensibilidade ou manutenção? Para esclarecer, não pretendo solicitar opinião, mas entender melhor os efeitos posteriores (ou seja, aqueles que podem não surgir até que uma base de código se torne bastante grande) de adotar essa prática de codificação.

Ben
fonte
2
É sempre um mau sinal quando o nome da sua classe é um adjetivo e não um substantivo.
David Ketcheson
3
Uma classe pode servir como um espaço de nomes sem estado para organizar funções, a fim de gerenciar a complexidade, mas existem outras maneiras de gerenciar a complexidade em idiomas que fornecem classes. (Namespaces em C ++ e módulos em Python vêm à mente.)
Geoff Oxberry
@GeoffOxberry Não posso falar se esse é um uso bom ou ruim - e é por isso que estou perguntando em primeiro lugar - mas as classes, diferentemente dos namespaces ou módulos, também podem gerenciar o "estado temporário", por exemplo, a hierarquia da grade no multigrid, que é descartado após a conclusão do algoritmo.
Ben

Respostas:

13

Tendo desenvolvido software numérico por 15 anos, posso afirmar inequivocamente o seguinte:

  • O encapsulamento é importante. Você não deseja passar ponteiros para os dados (como sugerido), pois eles expõem o esquema de armazenamento de dados. Se você expor o esquema de armazenamento, nunca poderá alterá-lo novamente, porque você acessará os dados em todo o programa. A única maneira de evitar isso é encapsular os dados em variáveis ​​de membro privadas de uma classe e deixar que apenas as funções de membro atuem nela. Se eu li sua pergunta, você pensa em uma função que calcula os autovalores de uma matriz como sem estado, usando um ponteiro para as entradas da matriz como argumento e retornando os autovalores de alguma forma. Eu acho que essa é a maneira errada de pensar sobre isso. Na minha opinião, essa função deve ser uma função membro "const" de uma classe - não porque altera a matriz, mas porque é aquela que opera com os dados.

  • A maioria das linguagens de programação OO permite que você tenha funções membro privadas. Esta é a sua maneira de dividir um algoritmo grande em outro menor. Por exemplo, as várias funções auxiliares necessárias para o cálculo do autovalor ainda operam na matriz e, portanto, seriam naturalmente funções-membro privadas de uma classe de matriz.

  • Comparado a muitos outros sistemas de software, pode ser verdade que as hierarquias de classes geralmente são menos importantes do que, digamos, nas interfaces gráficas do usuário. Certamente, existem lugares no software numérico em que eles são proeminentes - Jed descreve um em outra resposta a esse segmento, a saber, as várias maneiras pelas quais podemos representar uma matriz (ou, mais geralmente, um operador linear em um espaço vetorial dimensional finito). O PETSc faz isso de forma muito consistente, com funções virtuais para todas as operações que atuam em matrizes (elas não chamam de "funções virtuais", mas é isso que é). Existem outras áreas nos códigos típicos de elementos finitos em que se usa esse princípio de design do software OO. Os que vêm à mente são os muitos tipos de fórmulas de quadratura e os muitos tipos de elementos finitos, todos os quais são naturalmente representados como uma interface / muitas implementações. As descrições de leis materiais também se enquadram nesse grupo. Mas pode ser verdade que é sobre isso e que o restante de um código de elemento finito não usa a herança de maneira tão difundida quanto se pode usar, por exemplo, em GUIs.

Somente a partir desses três pontos, deve ficar claro que a programação orientada a objetos também é definitivamente aplicável aos códigos numéricos e que seria tolo ignorar os muitos benefícios desse estilo. Pode ser verdade que o BLAS / LAPACK não use esse paradigma (e que a interface usual exposta pelo MATLAB também não), mas arriscaria adivinhar que todo software numérico de sucesso escrito nos últimos 10 anos é, de fato, Orientado a Objeto.

Wolfgang Bangerth
fonte
16

Encapsulamento e ocultação de dados são extremamente importantes para bibliotecas extensíveis em computação científica. Considere matrizes e solucionadores lineares como dois exemplos. Um usuário só precisa saber que um operador é linear, mas pode ter uma estrutura interna, como esparsidade, um kernel, uma representação hierárquica, um produto tensorial ou um complemento de Schur. Em todos os casos, os métodos de Krylov não dependem dos detalhes do operador, eles dependem apenas da ação da MatMultfunção (e talvez da sua adjacente). Da mesma forma, o usuário de uma interface do solucionador linear (por exemplo, um solucionador não-linear) se preocupa apenas com a solução do problema linear e não deve precisar ou querer especificar o algoritmo usado. De fato, especificar essas coisas impediria a capacidade do solucionador não linear (ou outra interface externa).

As interfaces são boas. Dependendo de uma implementação é ruim. Se você conseguir isso usando classes C ++, objetos C, classes de tipo Haskell ou algum outro recurso de linguagem é irrelevante. A capacidade, robustez e extensibilidade de uma interface é o que importa nas bibliotecas científicas.

Jed Brown
fonte
8

As classes devem ser usadas apenas se a estrutura do código for hierárquica. Como você está mencionando algoritmos, a estrutura natural deles é um fluxograma, não uma hierarquia de objetos.

No caso do OpenFOAM, a parte algorítmica é implementada em termos de operadores genéricos (div, grad, curl, etc) que são basicamente funções abstratas que operam em diferentes tipos de tensores, usando diferentes tipos de esquemas numéricos. Essa parte do código é basicamente construída a partir de muitos algoritmos genéricos que operam em classes. Isso permite que o cliente escreva algo como:

solve(ddt(U) + div(phi, U)  == rho*g + ...);

Hierarquias como modelos de transporte, modelos de turbulência, esquemas de diferenciação, esquemas de gradiente, condições de contorno etc. são implementados em termos de classes C ++ (novamente, genéricas nas quantidades de tensores).

Notei uma estrutura semelhante na biblioteca CGAL, na qual os vários algoritmos são agrupados como grupos de objetos de função agrupados com informações geométricas para formar Kernels geométricos (classes), mas isso é feito novamente para separar operações da geometria (remoção de ponto de uma face, de um tipo de dados pontual).

Estrutura hierárquica ==> classes

Procedimental, fluxograma ==> algoritmos

tmaric
fonte
5

Mesmo que essa seja uma pergunta antiga, acho que vale a pena mencionar a solução particular de Julia . O que essa linguagem faz é "POO sem classe": as principais construções são tipos, ou seja, objetos de dados compostos semelhantes a structs em C, nos quais uma relação de herança é definida. Os tipos não têm "funções-membro", mas cada função tem uma assinatura de tipo e aceita subtipos. Por exemplo, você poderia ter um resumo Matrixtipo e subtipos DenseMatrix, SparseMatrixe tem um método genérico do_something(a::Matrix, b::Matrix)com especialização do_something(a::SparseMatrix, b::SparseMatrix). O despacho múltiplo é usado para selecionar a versão mais apropriada a ser chamada.

Essa abordagem é mais poderosa que o OOP baseado em classe, que equivale a despacho baseado em herança apenas no primeiro argumento, se você adotar a convenção de que "um método é uma função com thiso primeiro parâmetro" (comum, por exemplo, em Python). Alguma forma de despacho múltiplo pode ser emulada, digamos, em C ++, mas com contorções consideráveis .

A principal distinção é que os métodos não pertencem a classes, mas existem como entidades separadas e a herança pode ocorrer em todos os parâmetros.

Algumas referências:

http://docs.julialang.org/en/release-0.4/manual/methods/

http://assoc.tumblr.com/post/71454527084/cool-things-you-can-do-in-julia

https://thenewphalls.wordpress.com/2014/03/06/understanding-object-oriented-programming-in-julia-inheritance-part-2/

Federico Poloni
fonte
1

Duas vantagens da abordagem OO podem ser:

  • um cálculo longo a partir do qual você pode ou não desejar resultados diferentes. Por exemplo, seβ é a saída final, mas depende de um resultado intermediário α, você pode ter um calculate_alpha()método que armazena em cache oαresultado dentro da instância. Então, quando você liga calculate_beta(), também liga calculate_alpha()se não houverα O resultado já foi armazenado em cache.

  • um cálculo com várias entradas, onde, se uma entrada for alterada, todo o cálculo não precisará necessariamente ser feito novamente. Por exemplo, o calculate_f()método retornaf(x,y,z). Se você decidir refazer o cálculo para outro valor dez, você pode ligar set_z()e ozO parâmetro é marcado internamente como 'sujo'; portanto, quando você ligar calculate_f()novamente, apenas a parte do cálculo que dependez é refeito.

ptomato
fonte