Projeto de sistema de caixa eletrônico simples OOP
Para praticar o projeto de sistemas, criei um sistema simples de caixa eletrônico de banco.
Eu não queria complicar, então deixei de fora coisas como caixa de dinheiro por caixa eletrônico e abertura de bandejas de dinheiro, etc. Eu também queria negligenciar APIs para validar pinos, etc. (é apenas uma função que sempre retornará True) . Eu queria me concentrar em classes e métodos.
Então, no meu exemplo eu tenho
- uma
Accountclasse que tem um número de conta, um saldo e funções de retirada e depósito. - uma
Cardclasse que possui contas e uma função de seleção de conta que irá procurar o número da conta no cartão e depois devolvê-lo. - uma
ATMclasse que tem as seguintes variáveis:card_inserted,card_validated,current_account,current_carde a função principal sãoperform_request(que quer dar o equilíbrio, depositar ou levantar dinheiro),validate_pin(que fará a chamada para a API, escarnecido no meu código),select_account(que vai selecione uma conta do cartão)
Eu escrevi um teste para ele e funcionou. Gostaria de saber se poderia obter algum feedback sobre isso (não o teste, eu sei que posso fazer muitas coisas para tornar os testes melhores, mas essa foi apenas uma versão rápida e suja dele).
atm.py:
def validate_api(card_nr, pin):
# validate account nr and pin
# return true or fale
return True
class Card():
def __init__(self, accounts = []):
self.accounts = accounts
def select_account(self, account_nr):
# check if there is a built in filter func in python
for account in self.accounts:
if account.account_nr == account_nr:
return account
raise Exception("Account number invalid")
class Account():
def __init__(self, account_nr = None):
self.account_nr = account_nr
self.balance = 0
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
class ATM():
def __init__(self):
self.card_inserted = False
self.card_validated = False
self.current_account = None
self.current_card = None
def validate_card_and_pin(self):
if not self.card_inserted:
raise Exception('Card must be inserted first')
if self.card_validated is False:
raise Exception
# raise Exception("Card not validated")
def insert_card(self, bank_card):
# code to accept card
self.card_inserted = True
self.current_card = bank_card
def eject_card(self):
self.end_session()
# code to push card out
def validate_pin(self, pin):
if not self.card_inserted:
raise Exception('Card must be inserted first')
# card is inserted, pin has to be validated
# post pin and card number to api
# response will be saved in validated variable
validated = validate_api(card_nr=0,pin=0)
if validated == False:
self.card_validated = False
return self.card_validated
self.card_validated = True
return self.card_validated
def select_account(self, account_nr):
self.validate_card_and_pin()
if self.current_card is None:
raise Exception("no card in ATM")
if self.card_validated is False:
raise Exception("card not validated")
current_account = self.current_card.select_account(account_nr)
self.current_account = current_account
'''
1 = check balance
2 = deposit money
3 = withdraw money
'''
def perform_request(self, request, amount = 0):
self.validate_card_and_pin()
if request == 1:
return self.check_balance()
elif request == 2:
return self.accept_cash(amount)
elif request == 3:
return self.give_out_cash(amount)
else:
raise Exception("invalid request")
def accept_cash(self, amount):
# open cash tray
# count cash
self.current_account.deposit(amount)
return amount
def give_out_cash(self, amount):
# count cash
# open tray
self.current_account.withdraw(amount)
return amount
def check_balance(self):
return self.current_account.balance
def end_session(self):
self.card_inserted = False
self.card_validated = False
self.current_account = None
self.current_card = None
return True
E aqui o teste:
def test_depositing_50_and_withdrawing_20():
checking = Account(1)
saving = Account(2)
bank_card = Card([checking, saving])
atm = ATM()
atm.insert_card(bank_card)
atm.validate_pin("123")
atm.select_account(1)
atm.perform_request(2, 50)
if atm.perform_request(1) is not 50:
raise Exception("depositing function doesn't work")
atm.end_session()
atm.insert_card(bank_card)
atm.validate_pin("123")
atm.select_account(1)
atm.perform_request(3, 20)
if atm.perform_request(1) is not 30:
raise Exception("withdrawing function doesn't work")
atm.select_account(2)
atm.validate_pin("123")
atm.perform_request(2, 10)
if atm.perform_request(1) is not 10:
raise Exception("depositing function doesn't work")
print("Test successful")
Respostas
class Card():
Você não precisa de parênteses nas declarações de classe. Além disso, embora a maior parte da formatação seja muito boa, há algumas coisas minuciosas. Ao fornecer um argumento padrão, não deve haver espaços ao redor do= e deve haver duas linhas em branco entre as definições de nível superior .
Você realmente nunca deve ter um parâmetro padrão mutável como faz em:
def __init__(self, accounts = []):
Nesse caso, como você nunca modifica self.accounts, você está seguro. Se você alguma vez adicionou um add_accountmétodo que associa uma conta a um cartão por appending self.accounts, verá que tudo Cardem seu programa que foi criado usando o argumento padrão mudaria quando esse método for executado.
Eu mudaria para:
def __init__(self, accounts=None): # Default to None
self.accounts = accounts or [] # then create the list inside the method instead
# check if there is a built in filter func in python
Existem várias maneiras de usar alguma função de atalho sofisticada para encontrar o primeiro elemento que satisfaça um predicado, mas, honestamente, eu apenas me limitaria ao que você tem. Um forloop com um retorno antecipado é muito fácil de entender. Já que você deseja gerar uma exceção se uma conta não for encontrada, as outras maneiras ficam um pouco desleixadas. Se estiver tudo bem com o retorno Nonede um erro, você pode usar:
def select_account(self, account_nr):
return next((account for account in self.accounts if account.account_nr == account_nr), None)
nexttenta pegar o primeiro elemento de um iterador fornecido por você. Estou dando a ele uma expressão geradora, que produzirá um elemento somente se account.account_nr == account_nrfor verdadeiro. O Nonecomo o segundo argumento é um valor padrão se nada for encontrado. Ainda prefiro o forestilo iterativo .
Em Account, você está permitindo o Noneuso como um número de conta. Isso me parece o tipo de campo que não deve ser "anulável" ou omitido ao criar um objeto. Acho que um número de conta (único, existente) é fundamental para a ideia de uma conta bancária. Eu me livraria do padrão e forçaria o usuário a fornecer essas informações ao criar uma conta. Pode ser útil, entretanto, ter um segundo starting_balanceparâmetro que permite ao usuário definir o saldo inicial da conta e permitir que seja o padrão 0.
validate_card_and_piné um nome enganoso. Não parece fazer as validações. Ele espera que as validações já tenham sido feitas e que o self.card_validatedestado interno já esteja definido. assert_is_validatedpode ser um nome melhor para o que está fazendo.
Acho que validate_pinpoderia ser mais claro. Observe como grande parte do código na parte inferior está duplicado. Você está definindo o card_validatedque quer que seja validatede, em seguida, retornando esse valor. A função pode ser simplesmente:
def validate_pin(self, pin):
if not self.card_inserted:
raise Exception('Card must be inserted first')
self.card_validated = validate_api(card_nr=0,pin=0)
return self.card_validated
Eu talvez repensasse lançando exceções dos métodos. A meu ver, um PIN digitado errado, por exemplo, não é realmente excepcional . Sou fã de retornar Nonecomo um indicador de erro nos casos em que uma função só pode falhar de uma maneira; gosto validate_pin. Você apenas precisa ter o hábito de identificar quando uma função retorna Nonee lidar com esse caso de maneira adequada.
Se você quiser usar exceções, porém, lançar uma planície Exceptioné uma má ideia. Isso torna mais difícil para o chamador capturar e manipular exceções específicas de seu código. Acho que este é um caso apropriado para criar exceções personalizadas . Algo como:
class PINValidationFailed(RuntimeError):
pass
Então, o chamador pode capturar especificamente essa exceção para lidar com as falhas de PIN.
'''
1 = check balance
2 = deposit money
3 = withdraw money
'''
def perform_request(self, request, amount = 0):
Se for uma docstring, ela precisa estar dentro da função e recuada:
def perform_request(self, request, amount=0):
'''
1 = check balance
2 = deposit money
3 = withdraw money
'''
Eu acho que if self.card_validated is False:é mais claro simplesmente if not self.card_validated:.
Depois de retocar isso, fico com:
def validate_api(card_nr, pin):
# validate account nr and pin
# return true or false
return True
class Card:
def __init__(self, accounts=None):
self.accounts = accounts or []
def select_account(self, account_nr):
return next((account for account in self.accounts if account.account_nr == account_nr), None)
class Account:
def __init__(self, account_nr, starting_balance=0):
self.account_nr = account_nr
self.balance = starting_balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
class ATM:
def __init__(self):
self.card_inserted = False
self.card_validated = False
self.current_account = None
self.current_card = None
def validate_card_and_pin(self):
if not self.card_inserted:
raise RuntimeError('Card must be inserted first')
if not self.card_validated:
raise RuntimeError("Card not validated")
def insert_card(self, bank_card):
# code to accept card
self.card_inserted = True
self.current_card = bank_card
def eject_card(self):
self.end_session()
# code to push card out
def validate_pin(self, pin):
if not self.card_inserted:
raise RuntimeError('Card must be inserted first')
self.card_validated = validate_api(card_nr=0, pin=0)
return self.card_validated
def select_account(self, account_nr):
self.validate_card_and_pin()
if self.current_card is None:
raise RuntimeError("no card in ATM")
if self.card_validated is False:
raise RuntimeError("card not validated")
current_account = self.current_card.select_account(account_nr)
self.current_account = current_account
def perform_request(self, request, amount=0):
'''
1 = check balance
2 = deposit money
3 = withdraw money
'''
self.validate_card_and_pin()
if request == 1:
return self.check_balance()
elif request == 2:
return self.accept_cash(amount)
elif request == 3:
return self.give_out_cash(amount)
else:
raise RuntimeError("invalid request")
def accept_cash(self, amount):
# open cash tray
# count cash
self.current_account.deposit(amount)
return amount
def give_out_cash(self, amount):
# count cash
# open tray
self.current_account.withdraw(amount)
return amount
def check_balance(self):
return self.current_account.balance
def end_session(self):
self.card_inserted = False
self.card_validated = False
self.current_account = None
self.current_card = None
return True