Problem overview
A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total, which is the amount plus a processing fee. The total is worked out the same way for every payment. Only the fee depends on the kind:
Card: 2% of the amount,amount * 2 / 100.PayPal: 3% plus 30 cents,amount * 3 / 100 + 30.Cash: no fee.
Every division rounds down. A card payment of 1999 cents has a fee of 39 (the exact 39.98 is cut), so its total is 2038. PayPal on 500 costs 45 in fees, total 545, and cash on 700 stays 700. The fees add up to 84.
Approach
Other languages solve "shared part plus a part each kind fills in" with an abstract class. Go has no classes, no inheritance and no abstract classes, so it splits the job in three:
- An interface,
Payment, says what every payment must have:getAmount()andfee(). - A small struct,
BasePayment, holds the shared data (the amount) and the method that returns it. - A plain function,
total(p Payment), holds the shared rule. Interfaces have no method bodies, so a rule that is written once becomes a function that accepts anyPayment.
Each kind of payment embeds BasePayment, which gives it the amount field and getAmount() for free, and adds its own fee(). With both methods, it is a Payment, with no implements needed.
Step by step
- In the
Paymentinterface, listgetAmount() intandfee() int. Through an interface, code can only call methods, so even the amount needs one. - Give
BasePaymentone field,amount, and agetAmountmethod that returns it. - Write
totalasp.getAmount() + p.fee(). The call tofee()runs the fee of whatever kind of paymentpholds. - In
Card, writeBasePaymentalone on a line to embed it.NewCardbuilds aCardwhoseBasePaymentis itself built from the amount: an embedded field is named after its type, so the outer braces setBasePaymentand the inner ones set itsamount. - Write
Card'sfee:this.amount * 2 / 100. The namethis.amountreaches into the embedded struct directly. - Repeat the embedding and the constructor for
PayPal, with the feethis.amount * 3 / 100 + 30. - Repeat them for
Cash, whosefeereturns 0. Cash still needs the method: without it, aCashwould not be aPayment.
The solution in Go
type Payment interface {
getAmount() int
fee() int
}
type BasePayment struct {
amount int
}
func (this BasePayment) getAmount() int {
return this.amount
}
func total(p Payment) int {
return p.getAmount() + p.fee()
}
type Card struct {
BasePayment
}
func NewCard(amount int) Card {
return Card{BasePayment: BasePayment{amount: amount}}
}
func (this Card) fee() int {
return this.amount * 2 / 100
}
type PayPal struct {
BasePayment
}
func NewPayPal(amount int) PayPal {
return PayPal{BasePayment: BasePayment{amount: amount}}
}
func (this PayPal) fee() int {
return this.amount * 3 / 100 + 30
}
type Cash struct {
BasePayment
}
func NewCash(amount int) Cash {
return Cash{BasePayment: BasePayment{amount: amount}}
}
func (this Cash) fee() int {
return 0
}
Things worth knowing:
getAmountis defined once, onBasePayment, and every struct that embeds it has it. ACardis aPaymentbecause it hasgetAmountfrom the base and its ownfee.feeonBasePaymentdoes not exist, so a bareBasePaymentis not aPayment. That plays the part of an abstract method that every subclass has to write.- The constructors return the concrete type (
Card), and callers can store it in aPayment.
Using the payments
payments := []Payment{NewCard(1999), NewPayPal(500), NewCash(700)}
sum := 0
for _, p := range payments {
fmt.Print(total(p), " ")
sum += p.fee()
}
fmt.Println()
fmt.Println(sum)
2038 545 700
84
The loop never asks which kind of payment it holds. total(p) and p.fee() pick the right fee on their own.
Edge cases and common mistakes
- Rounding down. A card payment of 49 cents has a fee of 0 (the exact 0.98 is cut), and PayPal on 33 has 0.99 cut to 0 plus the 30, so for
NewCard(49),NewPayPal(33)andNewCard(50)the totals are 49, 63 and 51, and the fees add up to 31. - Amount 0.
NewCard(0)andNewCash(0)total 0, butNewPayPal(0)totals 30, because the 30 cents come on top of the percentage. - Multiply first, then divide. Integer division drops the fraction at once.
150 / 100 * 2gives 2, while150 * 2 / 100gives 3, so write the amount times the percentage first. - Overflow. The largest amount is 1,000,000 cents. Times 3 that is 3,000,000, far below what a 64-bit
intholds, so no overflow here. - A missing
fee. A struct withoutfeecannot be passed tototal: the compiler says it does not implementPayment.
This design keeps two things apart. What payments share (the amount and the total rule) is written once. What differs (the fee) is written by each kind. Adding a bank transfer later means one new struct with an embedded BasePayment, a constructor and a fee, and nothing else changes.
Complexity
Each fee and each total takes constant time, O(1): a few arithmetic operations and two method calls.
Testing
Run the three examples. The first gives totals 2038, 545 and 700, with fees adding to 84. The second gives 49, 63 and 51, with fees adding to 31. The third, with all amounts 0, gives 0, 30 and 0, and a fee sum of 30. A card at the maximum amount, 1,000,000 cents, has a fee of 20,000, and PayPal at the same amount has 30,030.
