mardi 12 mai 2020

How to separate concerns between a players cash and its manipulation?

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