Devo criar uma classe se minha função for complexa e tiver muitas variáveis?

40

Essa pergunta é um pouco independente da linguagem, mas não completamente, pois a Programação Orientada a Objetos (OOP) é ​​diferente em, por exemplo, Java , que não possui funções de primeira classe, do que em Python .

Em outras palavras, me sinto menos culpado por criar classes desnecessárias em uma linguagem como Java, mas sinto que pode haver uma maneira melhor nas linguagens menos padronizadas como o Python.

Meu programa precisa executar uma operação relativamente complexa várias vezes. Essa operação requer muita "contabilidade", tem que criar e excluir alguns arquivos temporários, etc.

É por isso que ele também precisa chamar muitas outras "suboperações" - colocar tudo em um método enorme não é muito bom, modular, legível etc.

Agora, essas são abordagens que vêm à minha mente:

1. Crie uma classe que tenha apenas um método público e mantenha o estado interno necessário para as suboperações em suas variáveis ​​de instância.

Seria algo como isto:

class Thing:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2
        self.var3 = []

    def the_public_method(self, param1, param2):
        self.var4 = param1
        self.var5 = param2
        self.var6 = param1 + param2 * self.var1
        self.__suboperation1()
        self.__suboperation2()
        self.__suboperation3()


    def __suboperation1(self):
        # Do something with self.var1, self.var2, self.var6
        # Do something with the result and self.var3
        # self.var7 = something
        # ...
        self.__suboperation4()
        self.__suboperation5()
        # ...

    def suboperation2(self):
        # Uses self.var1 and self.var3

#    ...
#    etc.

O problema que vejo com essa abordagem é que o estado dessa classe só faz sentido internamente e não pode fazer nada com suas instâncias, exceto chamar seu único método público.

# Make a thing object
thing = Thing(1,2)

# Call the only method you can call
thing.the_public_method(3,4)

# You don't need thing anymore

2. Crie várias funções sem uma classe e passe as várias variáveis ​​necessárias internamente entre elas (como argumentos).

O problema que vejo com isso é que tenho que passar muitas variáveis ​​entre funções. Além disso, as funções estariam intimamente relacionadas entre si, mas não seriam agrupadas.

3. Como 2., mas torne as variáveis ​​de estado globais em vez de passá-las.

Isso não seria bom, pois tenho que fazer a operação mais de uma vez, com entradas diferentes.

Existe uma quarta abordagem melhor? Caso contrário, qual dessas abordagens seria melhor e por quê? Tem algo que estou perdendo?

eu posso aprender
fonte
9
"O problema que vejo com essa abordagem é que o estado dessa classe faz sentido apenas internamente e não pode fazer nada com suas instâncias, exceto chamar seu único método público". Você articulou isso como um problema, mas não por que você pensa que é.
Patrick Maupin
@Patrick Maupin - Você está certo. E eu realmente não sei, esse é o problema. Parece que estou usando classes para algo para o qual outra coisa deve ser usada, e há muitas coisas em Python que ainda não explorei, então pensei que talvez alguém sugerisse algo mais adequado.
iCanLearn
Talvez seja sobre ser claro o que estou tentando fazer. Como, não vejo nada de particularmente errado em usar classes comuns no lugar de enumerações em Java, mas ainda existem coisas pelas quais as enumerações são mais naturais. Portanto, essa pergunta é realmente sobre se há uma abordagem mais natural para o que estou tentando fazer.
iCanLearn
3
possível duplicata de Qual é a real responsabilidade de uma classe?
gnat 12/09
11
Se você apenas dispersar seu código existente em métodos e variáveis ​​de instância e não alterar nada sobre sua estrutura, não ganhará nada além de perder a clareza. (1) é uma má ideia.
usr

Respostas:

47
  1. Crie uma classe que tenha apenas um método público e mantenha o estado interno necessário para as suboperações em suas variáveis ​​de instância.

O problema que vejo com essa abordagem é que o estado dessa classe faz sentido apenas internamente e não pode fazer nada com suas instâncias, exceto chamar seu único método público.

A opção 1 é um bom exemplo de encapsulamento usado corretamente. Você deseja que o estado interno seja oculto do código externo.

Se isso significa que sua classe possui apenas um método público, que assim seja. Vai ser muito mais fácil de manter.

No OOP, se você tem uma classe que faz exatamente uma coisa, tem uma pequena superfície pública e mantém todo o seu estado interno oculto, então você está (como diria Charlie Sheen) GANHANDO .

  1. Faça um monte de funções sem uma classe e passe as várias variáveis ​​necessárias internamente entre elas (como argumentos).

O problema que vejo com isso é que tenho que passar muitas variáveis ​​entre funções. Além disso, as funções estariam intimamente relacionadas entre si, mas não seriam agrupadas.

A opção 2 sofre de baixa coesão . Isso tornará a manutenção mais difícil.

  1. Como 2. mas torne as variáveis ​​de estado globais em vez de passá-las.

A opção 3, como a opção 2, sofre de baixa coesão, mas muito mais severamente!

A história mostrou que a conveniência das variáveis ​​globais é superada pelo brutal custo de manutenção que ela traz. É por isso que você ouve peidos velhos como eu reclamando sobre encapsulamento o tempo todo.


A opção vencedora é a nº 1 .

MetaFight
fonte
6
# 1 é uma API bastante feia, no entanto. Se eu decidisse fazer uma aula para isso, provavelmente faria uma única função pública que delega à classe e tornaria a classe inteira privada. ThingDoer(var1, var2).do_it()vs. do_thing(var1, var2).
User2357112 suporta Monica
7
Embora a opção 1 seja um vencedor claro, eu daria um passo adiante. Em vez de usar o estado interno do objeto, use variáveis ​​locais e parâmetros de método. Isso torna a função pública reentrada, o que é mais seguro.
4
Uma coisa extra que você pode fazer na extensão da opção 1: proteger a classe resultante (adicionando um sublinhado na frente do nome) e definir uma função no nível do módulo def do_thing(var1, var2): return _ThingDoer(var1, var2).run(), para tornar a API externa um pouco mais bonita.
Sjoerd Job Postmus
4
Não sigo o seu raciocínio para 1. O estado interno já está oculto. Adicionar uma classe não muda isso. Portanto, não vejo por que você recomenda (1). De fato, não é necessário expor a existência da classe.
usr
3
Qualquer classe que você instancia e, em seguida, chama imediatamente um único método e nunca mais usa novamente é um grande cheiro de código para mim. É isomórfica para uma função simples, apenas com fluxo de dados obscurecido (conforme as partes fragmentadas da implementação se comunicam atribuindo variáveis ​​na instância descartável em vez de passar parâmetros). Se suas funções internas usam tantos parâmetros que as chamadas são complexas e pesadas, o código não fica menos complicado quando você oculta esse fluxo de dados!
Ben
23

Eu acho que # 1 é realmente uma má opção.

Vamos considerar sua função:

def the_public_method(self, param1, param2):
    self.var4 = param1
    self.var5 = param2 
    self.var6 = param1 + param2 * self.var1
    self.__suboperation1()
    self.__suboperation2()
    self.__suboperation3()

Quais dados a suboperação1 usa? Coleta dados usados ​​pela suboperação2? Quando você repassa dados armazenando-os, não sei como as partes da funcionalidade estão relacionadas. Quando olho para mim mesmo, alguns dos atributos são do construtor, alguns da chamada para o método public_public e alguns adicionados aleatoriamente em outros lugares. Na minha opinião, é uma bagunça.

E o número 2? Bem, primeiro, vejamos o segundo problema com ele:

Além disso, as funções estariam intimamente relacionadas, mas não seriam agrupadas.

Eles estariam em um módulo juntos, para que fossem totalmente agrupados.

O problema que vejo com isso é que tenho que passar muitas variáveis ​​entre funções.

Na minha opinião, isso é bom. Isso torna explícitas as dependências de dados no seu algoritmo. Ao armazená-las, seja em variáveis ​​globais ou em um eu, você oculta as dependências e as faz parecer menos ruins, mas elas ainda estão lá.

Geralmente, quando essa situação surge, significa que você não encontrou o caminho certo para decompor seu problema. Você está achando estranho dividir em várias funções porque está tentando dividir da maneira errada.

Obviamente, sem ver sua função real, é difícil adivinhar o que seria uma boa sugestão. Mas você dá uma dica do que está lidando aqui:

Meu programa precisa executar uma operação relativamente complexa várias vezes. Essa operação requer muita "contabilidade", tem que criar e excluir alguns arquivos temporários etc.

Deixe-me escolher um exemplo de algo que se encaixa na sua descrição, um instalador. Um instalador precisa copiar um monte de arquivos, mas se você o cancelar parcialmente, será necessário retroceder todo o processo, incluindo a devolução de todos os arquivos que você substituiu. O algoritmo para isso se parece com:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

Agora multiplique essa lógica por ter que fazer configurações de registro, ícones de programas etc., e você terá uma bagunça.

Penso que a sua solução # 1 se parece com:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

Isso torna o algoritmo geral mais claro, mas oculta a maneira como as diferentes partes se comunicam.

A abordagem nº 2 se parece com:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

Agora é explícito como partes dos dados se movem entre funções, mas é muito estranho.

Então, como essa função deve ser escrita?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

O objeto FileTransactionLog é um gerenciador de contexto. Quando copy_new_files copia um arquivo, ele é feito através do FileTransactionLog, que lida com a cópia temporária e controla quais arquivos foram copiados. No caso de exceção, ele copia os arquivos originais de volta e, no caso de êxito, exclui as cópias temporárias.

Isso funciona porque encontramos uma decomposição mais natural da tarefa. Anteriormente, estávamos misturando a lógica sobre como instalar o aplicativo com a lógica sobre como recuperar uma instalação cancelada. Agora, o log de transações lida com todos os detalhes sobre arquivos temporários e contabilidade, e a função pode se concentrar no algoritmo básico.

Suspeito que o seu caso esteja no mesmo barco. Você precisa extrair os elementos da contabilidade em algum tipo de objeto para que sua tarefa complexa possa ser expressa de maneira mais simples e elegante.

Winston Ewert
fonte
9

Como a única desvantagem aparente do método 1 é seu padrão de uso subótimo, acho que a melhor solução é levar o encapsulamento um passo adiante: use uma classe, mas também forneça uma função autônoma, que apenas constrói o objeto, chama o método e retorna :

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

Com isso, você tem a menor interface possível para o seu código, e a classe que você usa internamente se torna realmente apenas um detalhe de implementação da sua função pública. O código de chamada nunca precisa saber sobre sua classe interna.

cmaster
fonte
4

Por um lado, a questão é, de alguma forma, independente da linguagem; mas, por outro lado, a implementação depende da linguagem e de seus paradigmas. Nesse caso, é o Python, que suporta múltiplos paradigmas.

Além de suas soluções, há também a possibilidade de executar as operações sem apátrida de uma maneira mais funcional, por exemplo

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

Tudo se resume a

  • legibilidade
  • consistência
  • manutenibilidade

Mas -Se a sua base de código for OOP, ela violará a consistência se de repente algumas partes forem escritas em um estilo (mais) funcional.

Thomas Junk
fonte
4

Por que projetar quando posso codificar?

Ofereço uma visão contrária às respostas que li. Nessa perspectiva, todas as respostas e, francamente, até a própria pergunta, se concentram na mecânica da codificação. Mas esse é um problema de design.

Devo criar uma classe se minha função for complexa e tiver muitas variáveis?

Sim, pois faz sentido para o seu design. Pode ser uma classe em si ou parte de outra classe ou seu comportamento pode ser distribuído entre as classes.


Design Orientado a Objetos é sobre Complexidade

O objetivo do OO é criar e manter com êxito sistemas grandes e complexos, encapsulando o código em termos do próprio sistema. O design "adequado" diz que tudo está em alguma classe.

O design de OO gerencia naturalmente a complexidade principalmente por meio de classes focadas que aderem ao princípio de responsabilidade única. Essas classes fornecem estrutura e funcionalidade ao longo da respiração e profundidade do sistema, interagem e integram essas dimensões.

Dado isso, é comum dizer que funções pendentes no sistema - a classe de utilidade geral onipresente demais - é um cheiro de código que sugere um design insuficiente. Eu tendem a concordar.

radarbob
fonte
Design OO gerencia naturalmente a complexidade OOP introduz naturalmente uma complexidade desnecessária.
Miles Rout
"Complexidade desnecessária" me lembra comentários inestimáveis ​​de colegas de trabalho como "ah, são muitas aulas". A experiência me diz que o suco vale a pena. Na melhor das hipóteses, vejo praticamente classes inteiras com métodos de 1 a 3 linhas e com todas as classes fazendo parte, a complexidade de qualquer método é minimizada: Um único LOC curto comparando duas coleções e retornando as duplicatas - é claro que há um código por trás disso. Não confunda "muito código" com código complexo. De qualquer forma, nada é gratuito.
Radarbob 15/09/2015
11
Muito código "simples" que interage de maneira complicada é MUITO pior do que uma pequena quantidade de código complexo.
Miles Rout
11
A pequena quantidade de código complexo continha complexidade . Há complexidade, mas apenas lá. Não vaza. Quando você tem várias peças individualmente simples que trabalham juntas de uma maneira realmente complexa e difícil de entender, isso é muito mais confuso, porque não há bordas ou paredes na complexidade.
Miles Rout
3

Por que não comparar suas necessidades com algo que existe na biblioteca padrão do Python e depois ver como isso é implementado?

Observe que, se você não precisar de um objeto, ainda poderá definir funções nas funções. Com o Python 3, existe a nova nonlocaldeclaração para permitir que você altere variáveis ​​em sua função pai.

Você ainda pode achar útil ter algumas classes privadas simples dentro de sua função para implementar abstrações e organizar operações.

meuh
fonte
Obrigado. E você sabe alguma coisa na biblioteca padrão que se parece com o que tenho na pergunta?
iCanLearn
Eu acharia difícil citar algo usando funções aninhadas; na verdade, não encontro nonlocalnenhum lugar nas minhas bibliotecas python atualmente instaladas. Talvez você possa entender a partir de textwrap.pyqual TextWrapperclasse, mas também uma def wrap(text)função que simplesmente cria uma TextWrapper instância, chama o .wrap()método nela e retorna. Portanto, use uma classe, mas adicione algumas funções de conveniência.
meuh 12/09/15