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

40

Esta questão é um pouco linguagem-agnóstico, mas não completamente, já Programação Orientada a Objetos (OOP) é diferente, por exemplo, Java, que não tem funções de primeira classe, do que é na Python .

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

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

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

Agora, essas são abordagens que me vêm à 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 parecido com 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. Faça um monte de funções sem uma classe e passe as várias variáveis internamente necessárias 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, já que tenho que fazer a operação mais de uma vez, com entradas diferentes.

Existe uma quarta abordagem melhor? Se não, qual dessas abordagens seria melhor e por quê? Há algo que estou perdendo?

    
por iCanLearn 12.09.2015 / 16:28
fonte

6 respostas

47
  1. Make a class that has one public method only and keeps the internal state needed for the suboperations in its instance variables.

The problem I see with this approach is that the state of this class makes sense only internally, and can't do anything with its instances except call their only public method.

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

Se isso significa que sua classe só tem um método público, então que seja. Vai ser muito mais fácil de manter.

Em OOP, se você tem uma classe que faz exatamente 1 coisa, tem uma pequena superfície pública e mantém todo o seu estado interno escondido, então você é (como Charlie Sheen diria) WINNING . / p>

  1. Make a bunch of functions without a class and pass the various internally needed variables between them (as arguments).

The problem I see with this is that I have to pass a lot of variables between functions. Also, the functions would be closely related to each other, but wouldn't be grouped together.

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

  1. Like 2. but make the state variables global instead of passing them.

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 é compensada pelo custo de manutenção brutal que ela traz. É por isso que você ouve velhos peidos como eu falando sobre encapsulamento o tempo todo.

A opção vencedora é # 1 .

    
por 12.09.2015 / 17:39
fonte
23

Acho que o # 1 é, na verdade, uma opção ruim.

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 partes de dados suboperation1 usa? Coleta dados usados por suboperação2? Quando você passa dados por meio de armazenamento em si mesmo, não sei dizer como as partes da funcionalidade estão relacionadas. Quando olho para mim, alguns dos atributos são do construtor, alguns da chamada para o_public_method e alguns adicionados aleatoriamente em outros lugares. Na minha opinião, é uma bagunça.

E o número 2? Bem, em primeiro lugar, vamos analisar o segundo problema:

Also, the functions would be closely related to each other, but wouldn't be grouped together.

Eles estariam em um módulo juntos, então eles seriam totalmente agrupados juntos.

The problem I see with this is that I have to pass a lot of variables between functions.

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

Normalmente, quando esta situação surge, significa que você não encontrou o caminho certo para decompor o seu problema. Você está achando estranho dividir-se em várias funções porque está tentando dividir o caminho errado.

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

My program needs to do a relatively complex operation a number of times. That operation requires a lot of "bookkeeping", has to create and delete some temporary files etc.

Deixe-me escolher um exemplo de algo que se encaixa na sua descrição, um instalador. Um instalador tem que copiar um monte de arquivos, mas se você cancelá-lo no meio, você precisa retroceder todo o processo, incluindo colocar de volta os arquivos que você substituiu. O algoritmo para isso parece algo como:

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 grande bagunça.

Acho que sua solução nº 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 # 2 é parecida 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 as 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 o faz através do FileTransactionLog, que manipula a cópia temporária e rastreia quais arquivos foram copiados. No caso de exceção, ele copia os arquivos originais de volta e, no caso de sucesso, apaga 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 manipula todos os detalhes sobre arquivos temporários e registros, e a função pode se concentrar no algoritmo básico.

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

    
por 13.09.2015 / 07:23
fonte
9

Como a única desvantagem aparente do método 1 é seu padrão de uso abaixo do ideal, acho que a melhor solução é direcionar o encapsulamento um passo adiante: use uma classe, mas também forneça uma função independente, que apenas construa o objeto, 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 realmente se torna apenas um detalhe de implementação de sua função pública. O código de chamada nunca precisa saber sobre sua classe interna.

    
por 13.09.2015 / 08:51
fonte
4

Por um lado, a questão é de alguma forma agnóstico em termos de linguagem; mas, por outro lado, a implementação depende da linguagem e seus paradigmas. Neste caso, é o Python, que suporta vários paradigmas.

Além das suas soluções, existe também a possibilidade de executar as operações completas sem estado de uma forma mais funcional, por ex.

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
  • manutenção

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

    
por 12.09.2015 / 23:42
fonte
4

Por que projetar quando posso ser codificado?

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

Should I create a class if my function is complex and has a lot of variables?

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

Design Orientado a Objetos é sobre Complexidade

O objetivo do OO é construir e manter com sucesso 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 OO naturalmente gerencia a complexidade principalmente através de classes focadas que aderem ao princípio de responsabilidade única. Essas classes dão estrutura e funcionalidade em toda a respiração e profundidade do sistema, interagem e integram essas dimensões.

Dado que, muitas vezes é dito que as funções penduradas sobre o sistema - a classe de utilidade geral muito onipresente - é um cheiro de código sugerindo um design insuficiente. Eu tendo a concordar.

    
por 14.09.2015 / 00:24
fonte
3

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

Note que, se você não precisa de um objeto, ainda pode definir funções dentro de funções. Com Python 3 , há a nova declaração nonlocal para permitir que você altere variáveis em sua função pai.

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

    
por 12.09.2015 / 17:01
fonte