Problem overview
A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total: the amount plus a processing fee. The total is worked out the same way for every payment, and only the fee depends on the kind:
Card:amount * 2 / 100.PayPal:amount * 3 / 100 + 30.Cash: 0.
A card payment of 1999 cents has a fee of 39 (the division drops the fraction) and a total of 2038.
Approach
The shared part goes into the parent Payment: it stores the amount and writes total() once, as amount + fee(). The fee is the part that differs, so Payment declares fee() but can't give it a value. The subclasses override it. When total() calls fee(), Swift runs the version of the object's own class, a card's fee for a card. Each call takes O(1) time.
Swift has no abstract classes. The usual stand-in is a fee() in the parent whose body is fatalError("must override"), which stops the program if it is ever reached. A subclass that overrides fee() never reaches it.
Step by step
- In
Payment, declarevar amount: Int. - In
init, store the parameter inself.amount. - In
fee(), callfatalError("must override"). It never returns, so the method needs noreturn. - In
total(), returnamount + fee(). - Declare
Card,PayPalandCashwith: Payment. They inheritinit,amountandtotal()and override onlyfee(). - Write each formula. Multiply first and divide second.
The solution in Swift
class Payment {
var amount: Int
init(_ amount: Int) {
self.amount = amount
}
func fee() -> Int {
fatalError("must override")
}
func total() -> Int {
return amount + fee()
}
}
class Card: Payment {
override func fee() -> Int {
return amount * 2 / 100
}
}
class PayPal: Payment {
override func fee() -> Int {
return amount * 3 / 100 + 30
}
}
class Cash: Payment {
override func fee() -> Int {
return 0
}
}
The totals and the sum of the fees for the lesson's three examples:
func run(_ ps: [Payment]) {
print(ps.map { String($0.total()) }.joined(separator: " "))
print(ps.reduce(0) { $0 + $1.fee() })
}
run([Card(1999), PayPal(500), Cash(700)])
run([Card(49), PayPal(33), Card(50)])
run([Card(0), PayPal(0), Cash(0)])
2038 545 700
84
49 63 51
31
0 30 0
30
The array passed to run has type [Payment], so the calls go through the parent type, and each payment still uses its own fee.
Swift details worth knowing
- Integer division drops the fraction. Amounts are
Int, so 49 * 2 / 100 is 0 and 50 * 2 / 100 is 1. - The order of the operations matters.
Card(150).fee()is 3, but dividing first gives150 / 100 * 2, which is 2. - The subclasses write no
init. They add no stored properties, so they inherit the parent'sinit(_:), andCard(1999)works.
Edge cases and common mistakes
- Amount 0. The card and cash fees are 0, but PayPal still adds its 30 cents.
- Forgetting
Cash's override. Its fee would then runPayment's version. A class that adds nofee(), such asclass Check: Payment {}, crashes onCheck(10).total()withFatal error: must override. - Writing the total in each subclass. It works, but the point is to write it once in the parent.
Testing
These amounts are worth trying in a file of your own. On the solution above, Card(0), PayPal(0) and Cash(0) have totals 0, 30 and 0. Card(1999), PayPal(500) and Cash(700) have totals 2038, 545 and 700, with fees adding up to 84. A card with 49 cents pays no fee at all.
Tracing the rounding example
The second example is Card(49), PayPal(33) and Card(50). The card fee of 49 is 49 * 2 / 100, which is 98 / 100: the whole part is 0, so the total stays 49. PayPal's 33 * 3 / 100 is 99 / 100, also 0, and then 30 is added: the fee is 30 and the total 63. The second card's 50 * 2 / 100 is exactly 1, so its total is 51. The totals line reads 49 63 51 and the fees add up to 0 + 30 + 1 = 31.
The fee is rounded down at every payment before the fees are added, not once at the end. That matches the run above, which sums the individual fees.
What happens inside one call
Take Card(1999) held in a variable of type Payment. A call to total() finds the version in Payment, because Card doesn't override it. That version evaluates amount + fee(), and the call to fee() goes to Card's override. Here is the amount, the fee and the total of each payment in the first example:
let list: [Payment] = [Card(1999), PayPal(500), Cash(700)]
for p in list {{ print(p.amount, p.fee(), p.total()) }}
1999 39 2038
500 45 545
700 0 700
The total is always the amount plus the fee. Only the middle number comes from different code for each class.
Why a stand-in for abstract
In languages that have abstract classes, the compiler refuses to create a Payment that has no fee. Swift has no such rule for classes, so the check happens at run time: a plain Payment(100).total() would stop with the same Fatal error: must override message. The lesson's tests only create the three concrete kinds, so the message never appears. A protocol with a fee() requirement is the other Swift way to say "every payment has a fee", but it can't also hold the stored amount and the shared total(), which is why a class is used here.
Key takeaways
- Put the shared rule in the parent and the changing step in an overridden method.
- In Swift, an abstract method is a method that calls
fatalError.
