Learning to code includes memorizing syntax, comprehending logical flow, and an intuition of design.
This includes learning the basic units of code (e.g. if, and, for, def
), the behaviour of code components (e.g. loops, generators, functions), and knowing how to combine these into a synergistic whole.
Object-oriented program is an advantage which heralds its own recommendations by the opportunities of least resistance.
It is an advantage because it eases and potentiates a capacity, namely for the use of objects (including their instantiation, inheritance, initialization, methods), and thus a capacity for seamless integration of a powerful tool. It has recommendations because it is better to do something easily, clearly, and in a modular manner using objects, than it is without objects, once they are available. It has opportunities of least resistence because the advantages of using objects are implicit, and don’t insist on their use except by an intention to do something by the best available path (which are the recommendations).
There is a lot to be said about thinking OOP. And others have undoubtedly done so with greater experience, scope, and comprehension. Here are some of the lessons learned by a beginner, learning to program, learning python.
An object is the container for its own information
An object should know what it contains, be able to modify what it contains, and be able to report back on what it contains and what it has done.
Reading and modifying attributes
Attributes can be fine to read from the outside. And they can also be fine to modify from the outside. Lots of libraries do this. For example, the Pygame library Rect
object has a directly accessible left
attribute (or the socket library Socket
object has_ipv6
attribute etc).
Sometimes it is better to use an object method to read and modify attributes. This is not just due to the role of read-only attributes (e.g. Socket
object Family
attribute). (1) It makes it easier to add complexity later (e.g. make a report every time an attribute is changed, or check whether the change should have downstream implications). (2) It makes it easier to unit test (e.g. can patch the method with a default value, or a generator or otherwise mock method). Neither of these should be underestimated, although the first reason will prove itself most obviously.
Example using a card game with a Hand
object that is the Player
object’s hand: Instead of telling the player to remove a random element from the Hand.cards
attribute (i.e. a list), the player should call Hand.discard_random(number = 1)
. Or even Hand.discard(random = True, number = 1)
. This later function could allow a specific card to discard (instead of being random), and look like this
def discard(self, which_card = None, random = False, number = 0)
Not only is this code more versatile. It will make it easier to later add a text-message function that tells the player which card they discarded. It will also make it easier to test the code, since a random discard can be patched to do any number of things (e.g. if want to discard a specific card under the guise of “random”).
Limiting an object’s scope
The programmer knows all the objects and variables, and thus it is temping to create a field of objects that all know (without being told) of each other’s existence, and are free to modify each other and call upon each other’s methods. This can be done by creating a “Controller” object that has all the other programmed objects instantiated as its attributes, and then gives each of those objects itself as a reference. This allows any object to call any other object by simply self.controller.other_object.method().
In many cases this may be useful, but it can create a messy code flow, wherein it is difficult to keep track and respond to changes that have been made. It can make a vulnerable code, wherein singular changes can disenfranchise a host of unexpected object methods.
But: it is often important to have objects respond beyond their own internal domain.
Making use of return dialogue
This can be solved by (1) using a method’s return, and employing the returned value to enable the appropriate changes that need to be made. If the direct receiver of the return is also not capable of inducing the necessary changes, then it too may need to employ return. Eventually the value could be returned to the Controller, who will act appropriately. This will encourage a certain design of the code,
For example, continuing with the Controller/Player/Card concept: Lets add an Enemy object. Lets say the Player can take a card from their deck, and use the card’s .activate(), and that the Enemy should be informed.
- Controller says response = player.action()
- player.action() uses player.deck.choose_card(), which returns a card, which is then used for card.activate()
- card.activate() could have access to the Controller and then call upon controller.tell_enemy(message). Or…
- card.activate() returns a message which the player returns to the controller, and the controller tells the enemy the message
Making use of composition to create internally consistent objects
This can also be solved by (2) designing the objects with a strong intention of their composition. This can mean designing a Player object, which is composed of itself, with attributes of a Deck objects, which in turn contains Card objects. Now the Player is a complex whole, within which certain sub-objects can talk. This is different from allowing objects access to a master (e.g. Controller) object, because they are still delimited. There are compromises to this approach, namely, Card objects are not sensical outside a Player paradigm.
Once again, it is a good idea to not let the Cards directly control their host Player, but instead to be allowed to call upon Player methods. In some cases it may be useful to give a component object (e.g. Cards) their host object (e.g. Player) upon initialization, but it may be better to only give them Player when needed (e.g. when card.activate() is called by player) – this will encourage modular design, full awareness of where information and control is moving, and will at least allow limited use of the object outside the host domain (e.g. allow Cards to be placed into a master deck without belonging to a Player).