Object Oriented Programming
Object-orientation (OO) is an approved paradigm to organize the complexity of software. The methods of object-oriented analysis (OOA) and design (OOD) provide an engineering approach, which leads from the specific requirements to its software implementation. The general procedure is based on principles of modularization (decomposition), encapsulation, abstraction and hierarchy.
In the 1980s Alan Kay introduced the term „Object Oriented Programming“.
An OO Language should at least support the following three key principles:
- Encapsulation
- Inheritance
- Polymorphism
Encapsulation
Encapsulation essentially has two use cases:
- keep state valid
An object is responsible to guarantee it’s state is valid!
- information hiding
Imagine a class point that can be moved with Cartesian coordinates or with polar coordinates. How the data is stored internally is not relevant for the caller.
class Point:
def translate(x, y) -> None:
# intern implementation
def translate(radius, phi) -> None:
# intern implementation
Implementation Details Many programming languages provides explicit visibility keywords. e.g. in C++:
class Point {
private:
int x = 0;
int y = 0;
}
However in Python all functions and attributes are publicly accessible. Therefore, as a convention, an underscore is placed in front of “private” functions and attributes.
class Point:
def __init__:
self._x = 0
self._y = 0
Inheritance
Classes can be specializations of other classes. That means classes can be in hierarchical order by inheriting properties and behavior of higher ranked classes. Lower ranked classes can specialize (override) or extend higher classes. On code level Inheritance may be used to avoid redundancy by allowing code reuse.
Inheritance can be implemented very easily in python with the class declaration class <class_name>(base_class_name)
class Shape:
def __init__(self) -> None:
self._center_x = 0.
self._center_y = 0.
def translate(self, delta_x: float, delta_y: float) -> None:
self._center_x += delta_x
self._center_y += delta_y
def draw(self) -> None:
print(self)
class Circle(Shape): # Circle inherits from Shape
def __init__(self, radius: float) -> None:
super().__init__() # calling the base class constructor in the sub class
self._radius = radius
def __str__(self) -> str:
return "I am a circle!"
However, the inheritance is a very strong dependence of the inheriting class to the base class. This means that inherited behavior cannot be replaced. it is different with composition. When it comes to redundancy avoidance, composition should be prioritized.
Favor composition over inheritance
However, inheritance enables another very important principle: polymorphism.
Polymorphism
In general, polymorphism is the ability to have different individuals of a species. In object-oriented programming, polymorphism refers to a programming language’s ability of objects to react differently to one and the same message depending on their class. Technically, this is achieved by redefining methods in derived classes - Inheritance. One may also speak of the autonomy or independence of objects.
In this example, an object of the class Lamp
depends only on the interface LightBulb
and the method lightUp()
, which means that it can call its methods.
class Lamp:
def __init__(bulb: LightBulb) -> None: # dependency (constructor) injection
self._bulb: LightBulb = bulb
def on(self) -> None:
self._bulb.lightUp() # sending a message to the dependency
Lamp
does not now about the specific LightBulb
implementations, like ClassicalBulb
or EnergySavingBulb
.
The construction of Lamp
and the specific LightBulb
needs to take place somewhere else, e.g. in the main script. Depending on how bulb
is instantiated, it behaves in Lamp
differently. Here you can see the autonomy of the object.
bulb: LightBulb
if save_energy
bulb = EnergySavingBulb()
else
bulb = ClassicalBulb()
lamp = Lamp(bulb)