In a very small game I am writing a player
has some cash (which is just an Int
), but I don't want to directly manipulate the cash in the player
class; I feel the player class shouldn't care about cash manipulation.
As such, I move all cash manipulation into another class.
Here's where I get all confused. I am reading up on VIPER and other patterns where "walled gardens" mean that a class will usually not know about its parent or have direct access to another object.
So, when it comes to this, I'm not sure if I'm meant to seperate the concern between a player's cash and the ability to credit/debit cash into a player's wallet, or not.
In my cash handler I need to do some error checking, throw
errors, and stuff; for now this is not important to my question; hence I've removed these to focus on the main thrust of my question.
I am using Swift Playgrounds to play around with 2 main ideas I have about how to tackle this small issue.
// : Idea #1 -- Use static functions
class Player {
var cash: Int = 0
}
let p = Player.init()
struct Wallet{
static func credit(account: Player, amount: Int) {
// (#TODO) the final one will do some checks, throw an error
var balance: Int = account.cash
balance += amount
account.cash = balance
}
static func debit(account: Player, amount: Int) {
// (#TODO) the final one will do some checks, throw an error
var balance: Int = account.cash
balance -= amount
account.cash = balance
}
}
Wallet.credit(account: p, amount: 125)
Wallet.debit(account: p, amount: 25)
print (p.cash)
Okay, in this one I use static functions; but the Wallet
struct has direct access to the player. I feel this is wrong.
My second attempt:
class Player {
var account: Account = Account()
var cash: Int {
return account.balance
}
init(cash: Int) {
self.account = Account(openingBalance: cash)
}
}
var p = Player.init(cash: 50)
class Account {
public private(set) var balance: Int = 0
init(openingBalance: Int = 0) {
self.balance = openingBalance
}
func credit(amount: Int) -> Int {
balance += amount
return self.balance
}
func deposit(amount: Int) -> Int {
balance -= amount
return self.balance
}
}
p.account.credit(amount: 100)
print (p.cash)
This one feels cleaner, but now the Player
object has direct access to the Account
?
Edit: I have a third attempt. I saw something called a proxy design pattern
, sorry if I'm not familar with the pattern; as I understand it, you could have something in between the player and the bank account acting as a proxy to decide whether or not a player can credit or debit their account.
Sadly, this experiment doesn't quite work; I have now a seemingly endless loop of do-catch statements; I'm not sure how I kick it back to the main program.
// : Third attempt -- I think this follows a Proxy pattern
public enum CashError: Error, Equatable {
case mustBePositive
case notEnoughFunds
case cannotPerformTransaction
case amountWouldBeNegative
}
class Bank {
var balance: Int = 0
enum TransactionType: Int {
case credit = 0
case debit
}
func performTransaction(transactionType: TransactionType, amount: Int) {
switch transactionType {
case .credit:
self.credit(amount: amount)
case .debit:
self.debit(amount: amount)
}
}
private func credit(amount: Int = 0) {
print ("didCredit: \(amount)")
self.balance += amount
}
private func debit(amount: Int = 0) {
print ("didDebit: \(amount)")
self.balance -= amount
}
}
class Customer {
private(set) var accountProxy: AccountProxy?
var cash: Int {
guard let proxy: AccountProxy = accountProxy else {
return 0
}
return proxy.balance
}
init(cash: Int = 0) {
print ("Create player with $\(cash)")
self.accountProxy = AccountProxy.init(customer: self)
guard let proxy = self.accountProxy else {
return
}
do {
let _ = try proxy.handle(transactionType: .credit, amount: cash)
} catch {
print (error)
}
}
}
class AccountProxy {
private var bank: Bank = Bank()
private var customer: Customer
public var balance: Int {
return self.bank.balance
}
init(customer: Customer) {
self.customer = customer
}
func handle(transactionType: Bank.TransactionType, amount: Int = 0) throws -> Bool {
print ("Attempting \(transactionType) of $\(amount)")
do {
if let _ = try canPerformTransaction(transactionType: transactionType, amount: amount) {
print ("proxy: says you can \(transactionType): $\(amount)")
self.bank.performTransaction(transactionType: transactionType, amount: amount)
return true
}
else {
print ("proxy: error - Cannot perform transction")
throw CashError.cannotPerformTransaction
}
} catch {
throw (error)
}
}
// (Private) functions
private func canPerformTransaction(transactionType: Bank.TransactionType, amount: Int ) throws -> Bool? {
switch transactionType {
case .credit:
do {
guard let result = try canCredit(amount: amount) else {
return false
}
return result
} catch {
throw error
}
case .debit:
do {
guard let result = try canDebit(amount: amount) else {
return false
}
return result
} catch {
throw error
}
}
}
private func canCredit(amount: Int) throws -> Bool? {
guard amount >= 0 else {
throw CashError.mustBePositive
}
return true
}
private func canDebit(amount: Int) throws -> Bool? {
// amount must be > 0
guard amount > 0 else {
throw CashError.mustBePositive
}
// balance must be >= amount
guard balance >= amount else {
throw CashError.notEnoughFunds
}
// the remaining sum must be >= 0
let sum = balance
guard ((sum - amount) >= 0) else {
throw CashError.amountWouldBeNegative
}
return true
}
}
let bob = Customer.init(cash: 100)
print ("Bob has $\(bob.cash)")
do {
let _ = try bob.accountProxy?.handle(transactionType: .credit, amount: 125)
} catch {
print (error)
}
print ("Bob has $\(bob.cash)")
do {
let _ = try bob.accountProxy?.handle(transactionType: .debit, amount: 25)
} catch {
print (error)
}
print ("Bob has $\(bob.cash)")
// (Logged Output):
// Create player with $100
// Attempting credit of $100
// proxy: says you can credit: $100
// didCredit: 100
// Bob has $100
// Attempting credit of $125
// proxy: says you can credit: $125
// didCredit: 125
// Bob has $225
// Attempting debit of $25
// proxy: says you can debit: $25
// didDebit: 25
// Bob has $200
Thus my query is based on the "walled gardens" concept? Should the player class have knowledge of its account?
I apologize if this seems obvious, I find it very frustrating/confusing.
I appreciate any feedback/assistance on this.
Aucun commentaire:
Enregistrer un commentaire