For every problem, there could be more than one way to solve it. To understand Object-Oriented Programming (OOP) and use it to its best utility, we need to understand some basic principles. The best way to demonstrate that is by example.
Problem Statement
Suppose in my system, I have to keep track of credit card details. In a very simplistic design, here are the attributes that I need to create for my CreditCard
class:
cardNumber
: Card numbercardType
: Visa or Mastercard or Amex etc.ccv
: The code at the back of the cardcurrency
: Default is SGD, unless otherwise specifiedcreditLimit
: The limit of spending on the cardissuingBank
: Bank that issued the cardcardOwner
: Name of owner on the credit cardcardExpiry
: Expiry date of cardamountSpent
: Total amount spent for the cardamountDue
: Amount due for payment in that monthdueDate
: Due date for payment foramountDue
You may include more attributes depending on your design, but I shall use these for now. Note that all dates in this class are datetime
instances.
Encapsulation
Oooh what a big word! This is one of the features of OOP, but it definitely isn’t solely unique to OOP. The role of encapsulation essentially hides vital information from outside the class. This prevents unauthorised access to the values of object attributes, or the internal structure of a class. We should be selective on which data can be viewed or changed from outside the class to preserve the integrity of our program. In this case, we may first set all our attributes in the CreditCard
class to private.
To ensure that the values of certain attributes are protected from unnecessary change outside the class, we need to decide which attributes should have mutators and which should not. In our problem statement, all the attributes, except the creditLimit
, amountDue
, dueDate
and cardExpiry
, should not be changed with a mutator.
We may have accessors for all the attributes of this class, so that we can display them appropriately.
Class Attributes
The attributes we have defined above are known as data attributes; they differ from object to object. On the other hand, class attributes are tied to the class itself, and may be accessed using
You do not need to understand thoroughly every single way to use class attributes though, because in most scenarios, you won’t be using class attributes for those purposes. Remember, there are many ways to write a program.
For our problem statement, we could create a class attribute called supportedCardTypes
, that is a tuple and contains the types (like Visa, Mastercard etc.) that are supported by our system. I picked an immutable type as using a mutable type like a list to represent the data may have undesired side effects. Using this class attribute, my class will decide whether to initialise the cardType upon object creation, or produce an error due to an unsupported card type.
class CreditCard: supportedCardTypes = ('Visa', 'Mastercard', 'Amex', 'UnionPay') def __init__(self, cardNumber, cardType, ccv, creditLimit, \ issuingBank, cardOwner, cardExpiry, currency='SGD'): # The prefix double underscores make the attributes private self.__cardNumber = cardNumber if cardType in CreditCard.supportedCardTypes: self.__cardType = cardType else: print("Invalid card type") self.__cardType = '' self.__ccv = ccv self.__currency = currency self.__creditLimit = creditLimit self.__issuingBank = issuingBank self.__cardOwner = cardOwner self.__cardExpiry = cardExpiry self.__amountSpent = 0 self.__amountDue = 0 self.__dueDate = None
Class Methods
Class methods are also known as behaviours. Objects from the same class will have similar behaviours, so we define them using methods.
We shall define our accessors so that we may access our data attributes outside the CreditCard
class.
The following are our accessors, one for each data attribute.
# Accessors/getters def get_cardNumber(self): return self.__cardNumber def get_cardType(self): return self.__cardType def get_ccv(self): return self.__ccv def get_currency(self): return self.__currency def get_creditLimit(self): return self.__creditLimit def get_issuingBank(self): return self.__issuingBank def get_cardOwner(self): return self.__cardOwner def get_cardExpiry(self): return self.__cardExpiry def get_amountSpent(self): return self.__amountSpent def get_amountDue(self): return self.__amountDue def get_dueDate(self): return self.__dueDate
We shall also define our mutators for creditLimit
, amountDue
, dueDate
and cardExpiry
.
# Mutators/setters def set_creditLimit(self, limit): self.__creditLimit = limit def set_amountDue(self, amount): self.__amountDue = amount def set_dueDate(self, date): self.__dueDate = date def set_cardExpiry(self, date): self.__cardExpiry = date
We should have methods that will withdraw or deposit to the credit card account. Upon withdrawal, the amountSpent
will increase. Depositing into the card will reduce the amountSpent
. Note that amountSpent
can be a negative value if the user deposits more than the amount they spent.
# Actions/behaviours def withdraw(self, amount): if self.__amountSpent + amount > self.__creditLimit: print('Amount spent exceeds credit limit. Withdrawal failed.') else: self.__amountSpent += amount print('Withdrawal success.') def deposit(self, amount): self.__amountSpent -= amount
We may have some display methods to show certain information of the card, like the card monthly statement, amount spent so far, and card information. For the last one, we could use the __str__ method to create a string representation of the CreditCard instance.
# Display/retrieve information def displayCurrentStatement(self): if self.__dueDate != None and self.__amountDue != 0: s = 'Amount due is {}{}, due date is {}'.format(self.__currency, \ self.__amountDue, self.__dueDate.strftime("%Y-%m-%d")) print(s) else: print('Credit card not due yet.') def displayAmountSpent(self): s = 'Amount Spent: {}{}\nAmount Left to Spend: {}{}'.format(self.__currency, \ self.__amountSpent, self.__currency, \ self.__creditLimit - self.__amountSpent) print(s) def __str__(self): s = 'Card Information: {} {}\nCredit Limit: {}{}\nIssuing Bank: {}\nName on Card: {}\nExpiry Date: {}'.format(self.__cardType, \ self.__cardNumber, self.__currency, self.__creditLimit, \ self.__issuingBank, self.__cardOwner, \ self.__cardExpiry.strftime("%Y-%m-%d")) return s
For the purpose of this demonstration, we shall create the instances of the CreditCard
class within the same file. Most of the time, the codes will be contained in separate files. For the following code, you may add on your own codes by creating more than one instance of CreditCard
, and calling the other methods. Try testing the code with credit card types that are not included in the supportedCardTypes
attribute.
from datetime import datetime card = CreditCard('123456789000', 'Visa', 456, 9000, 'OCBC', 'Luvelle', datetime(2019, 6, 30)) print(card) card.displayCurrentStatement() card.displayAmountSpent()
Putting it Together
Check out the demo code in this link!
Happy coding! 🙂