Prefira a repetição à abstração incorreta
A repetição de código é geralmente vista como um problema. Ela pode levar a uma manutenção mais difícil e aumentar a probabilidade de erros. No entanto, às vezes, a repetição de código pode ser uma boa escolha.
Por exemplo, imagine que você está escrevendo um jogo simples e precisa desenhar muitos objetos diferentes na tela, como jogadores, inimigos e projéteis. Cada um desses objetos tem algumas coisas em comum, como posição e tamanho, mas também tem algumas coisas diferentes, como a imagem que deve ser desenhada. Se você tentar usar uma classe abstrata para representar todos esses objetos e, em seguida, criar classes filhas para representar cada tipo de objeto, o código pode ficar muito complexo e difícil de entender.
Em vez disso, você pode escolher repetir o código para desenhar cada objeto. Isso pode parecer redundante, mas na verdade é mais fácil de entender e manter. Cada “pedaço” do código é curto e simples e faz apenas uma coisa, e não há necessidade de navegar por uma hierarquia de classes para entender o que está acontecendo.
class Player:
def __init__(self, x, y, image):
self.x = x
self.y = y
self.image = image
def draw(self):
# desenha a imagem do jogador na posição x, y
class Enemy:
def __init__(self, x, y, image):
self.x = x
self.y = y
self.image = image
def draw(self):
# desenha a imagem do inimigo na posição x, y
class Projectile:
def __init__(self, x, y, image):
self.x = x
self.y = y
self.image = image
def draw(self):
# desenha a imagem do projétil na posição x, y
Outro exemplo é quando você está escrevendo uma biblioteca para outros desenvolvedores usarem. Se você fornecer uma abstração complexa para fazer algo simples, os desenvolvedores terão que gastar mais tempo para entender como usá-la e serão mais propensos a cometer erros. Em vez disso, forneça uma abstração simples, que pode ser facilmente compreendida e usada corretamente.
def validate_int(input_string):
try:
int(input_string)
return True
except ValueError:
return False
def validate_float(input_string):
try:
float(input_string)
return True
except ValueError:
return False
Em ambos os exemplos, o código é simples e fácil de entender, e não há necessidade de uma abstração mais complexa. A repetição de código é aceitável, pois é mais fácil de entender e manter.
Agora, veja um exemplo de abstração de um sistema de gerenciamento de arquivos:
class FileSystem:
def __init__(self, root_path):
self.root = Directory(root_path)
def ls(self, path):
current_dir = self._traverse_to_dir(path)
return current_dir.ls()
def mkdir(self, path):
current_dir = self._traverse_to_dir(path)
current_dir.mkdir()
def touch(self, path):
current_dir = self._traverse_to_dir(path)
current_dir.touch()
def rm(self, path):
current_dir = self._traverse_to_dir(path)
current_dir.rm()
def _traverse_to_dir(self, path):
parts = path.split("/")
current_dir = self.root
for part in parts:
current_dir = current_dir.cd(part)
return current_dir
class Directory:
def __init__(self, name):
self.name = name
self.files = []
self.directories = []
def ls(self):
return self.files + self.directories
def mkdir(self):
new_dir = Directory(name)
self.directories.append(new_dir)
return new_dir
def touch(self):
new_file = File(name)
self.files.append(new_file)
return new_file
def rm(self):
# logica para remover arquivos e diretorios
def cd(self, name):
for directory in self.directories:
if directory.name == name:
return directory
raise ValueError("No such directory")
class File:
def __init__(self, name):
self.name = name
Neste exemplo, temos uma classe abstrata FileSystem
que gerencia arquivos e diretórios, e as classes Directory
e File
que representam diretórios e arquivos, respectivamente.
As classes usam uma hierarquia de herança, e as operações para acessar, criar ou remover arquivos ou diretorios são realizadas através de métodos das classes, onde a classe FileSystem
é a classe principal e as outras classes são usadas para representar os arquivos e pastas.
Isso pode parecer um código mais organizado, mas as coisas ficam mais complexas como por exemplo, para entender como o método rm
funciona, é necessário entender como as classes Directory
e File
funcionam e se relacionam entre si. Além disso, se você quiser adicionar uma nova funcionalidade, como por exemplo, a capacidade de copiar arquivos, você precisaria modificar várias classes.
Outra coisa que devs gostam muito de fazer é adicionar bibliotecas externas em seus projetos para realizar tarefas simples, quando poderia simplesmente implementar a solução. Evintando assim adicionar mais uma dependência no projeto.
import nltk
def count_words(file_path):
with open(file_path, "r") as file:
text = file.read()
tokenizer = nltk.tokenize.WhitespaceTokenizer()
tokens = tokenizer.tokenize(text)
word_count = len(tokens)
return word_count
Neste exemplo, estamos utilizando a biblioteca NLTK
(Natural Language Toolkit) para contar as palavras em um arquivo de texto. A biblioteca NLTK
é uma biblioteca muito poderosa para processamento de linguagem natural, mas neste caso, estamos apenas usando a classe WhitespaceTokenizer
para dividir o texto em tokens(palavras), e contando quantas palavras existem. Isso é algo simples que poderia ser feito com poucas linhas de código sem a biblioteca, mas ao usar a biblioteca NLTK
, estamos adicionando uma camada adicional de complexidade e dependência externa ao nosso projeto.
Em resumo, a repetição de código pode ser uma boa escolha quando o código é simples e fácil de entender, e quando uma abstração mais complexa não é necessária. Por outro lado, as abstrações podem deixar o código mais complexo e difícil de entender. É importante avaliar as necessidades do seu projeto e escolher a abordagem que melhor se adapta a ele.