Problem overview
A shop takes payments by card, by PayPal and in cash. Each payment has an amount in cents and a total, which is the amount plus a fee. The total is calculated the same way for every payment. Only the fee depends on the kind:
Card:amount * 2 / 100(2%);PayPal:amount * 3 / 100 + 30(3% plus 30 cents);Cash: 0.
Every division rounds down. A card payment of 1999 cents has a fee of 39, not 39.98, so its total is 2038. A PayPal payment of 500 has a fee of 45 and a total of 545; cash of 700 has a total of 700.
Approach: share the total, leave the fee open
The shared part, the amount and the rule "total = amount + fee", belongs in one place. The part that differs, the fee, belongs in each kind. An abstract class is made for this:
- it can hold fields, constructors and ordinary methods that all children inherit;
- it can declare an abstract method, with no body, that says "every payment has a fee" and forces each child to write it;
- it cannot be created itself.
new Payment(100)is a compile error (CS0144, cannot create an instance of the abstract type), because a plain payment would have no fee.
The ordinary method Total() calls Fee(), and when it runs on a card, the card's Fee() is the one that answers.
Step by step
- Declare
public int amount;inPayment, once, for all kinds. - In the constructor, store the parameter with
this.amount = amount. An abstract class may have a constructor: it runs when a child object is created. - Declare
public abstract int Fee();with a semicolon instead of a body. - Write
Total()asamount + Fee(). It is not abstract and not overridden, so it is written once for all children. - In each child, the constructor is already given.
: base(amount)hands the amount toPayment's constructor. Each child then overridesFee()with its own formula.
The solution in C#
abstract class Payment {
public int amount;
public Payment(int amount) {
this.amount = amount;
}
public abstract int Fee();
public int Total() {
return amount + Fee();
}
}
class Card : Payment {
public Card(int amount) : base(amount) { }
public override int Fee() {
return amount * 2 / 100;
}
}
class PayPal : Payment {
public PayPal(int amount) : base(amount) { }
public override int Fee() {
return amount * 3 / 100 + 30;
}
}
class Cash : Payment {
public Cash(int amount) : base(amount) { }
public override int Fee() {
return 0;
}
}
Details worth knowing
Multiply first, then divide. With integers, 150 * 2 / 100 is 3, but 150 / 100 * 2 is 2, because 150 / 100 already rounds down to 1. Both were run: the first prints 3 and the second 2.
Small amounts. A card payment of 49 cents has 49 * 2 / 100 = 0, so its total is 49. A PayPal payment of 0 still costs 30. Neither is a special case in the code; integer division and the added 30 handle them.
Every child must override Fee. Even Cash, whose fee is 0, has to write the method, because it is abstract in Payment. A child that leaves it out does not compile.
Using the classes
Payment[] payments = { new Card(1999), new PayPal(500), new Cash(700) };
int fees = 0;
foreach (Payment p in payments) {
Console.Write(p.Total() + " ");
fees += p.Fee();
}
Console.WriteLine();
Console.WriteLine(fees);
2038 545 700
84
The array has the abstract type, and each element is a real card, PayPal or cash payment. A second run with Card(49), PayPal(33) and Card(50) prints the totals 49 63 51 and the fee sum 31. Amounts go up to 1,000,000 cents, and then a card fee is 20000 and the total 1020000, far from the limits of int, so no overflow can happen here.
Complexity
Each fee and each total takes constant time: one multiplication, one division and one addition.
A worked table
For each payment, the fee comes from the child class and the total from the shared method:
| Payment | Amount | Fee | Total |
|---|---|---|---|
| Card | 1999 | 39 | 2038 |
| PayPal | 500 | 45 | 545 |
| Cash | 700 | 0 | 700 |
| Card | 49 | 0 | 49 |
| PayPal | 33 | 30 | 63 |
| Card | 50 | 1 | 51 |
Abstract class or interface?
A payment has data (the amount) and a method with real code (Total) that every kind shares, so an abstract class fits. An interface, such as the INotifier of other exercises, lists only what a class must offer; it holds no fields and, here, would make each kind repeat the amount and the total. Use an abstract class when the kinds share code or state, and an interface when they only share a promise.
Common mistakes
- Trying to create a
Paymentdirectly. - Writing a body for the abstract method, or forgetting the semicolon.
- Forgetting
: base(amount), so the parent's constructor never receives the amount. - Dividing before multiplying, which rounds too early.
Calling through the abstract type
Code that uses payments declares its variables as Payment, never as Card or PayPal. When Total() runs on an object created with new PayPal(500), the call Fee() inside it runs PayPal's version, so the shared method and the specific rule cooperate without either knowing the other's details. Adding a fourth kind later means writing a new child with its own Fee(); Payment and the code that sums totals stay as they are.
Key takeaways
- An abstract class can hold fields, a constructor and ordinary methods, and it declares abstract methods that each child must write.
- A child hands values to the parent's constructor with
: base(...). - The ordinary method
Total()callsFee(), and at run time the object's own override answers. - In integer formulas, multiply first and divide last, since each division rounds down.
