Data Structures, Classes and Objects
Data Structures, Classes and Objects#
Data Structures vs. Classes#
When designing our software it is important to keep in mind that there is a semantic difference between data structures and classes. Data structures expose their data and have no functions, while classes hide their data and offer functions to operate on that data.
Data structures make it easy to add new functions that operate on those data structures, since the data structures themselves remain unaffected. However, code that utilizes data structures makes it hard to add new types, because all functions working with them will need to be changed to handle new ones.
For classes the opposite is true. They make it easy to add new types, since their concrete implementation can be hidden behind an abstract type. But to add a new function, all of the implementations of an abstract type need to change.
The following code snippet shows a simple
Point data structure. Since its data is publicly accessible it is easy to write functions that operate on this data.
In contrast, this
Point protocol does not reveal any details about the internal representation of its coordinates. It could be either cartesian or polar coordinates or even something else entirely.
def x(self) -> int:
def y(self) -> int:
def set_carthesian(self, x: int, y: int) -> None:
def theta(self) -> int:
def radius(self) -> int:
def set_polar(self, radius: int, theta: int) -> None:
Law of Demeter#
The Law of Demeter says that modules should not know about internal details of the objects they work with, therefore following the rule that classes do not expose their data. In the following method call an object deep inside the object we actually work with is manipulated.
Here the caller knows that the
gui object has a circle, that this circle has a center and that its position can be set with to a new point.
Instead we should hide these details and allow the caller to set the new position from the outside.
Despite looking similar to the previous example, so called fluent interfaces do not violate the Law of Demeter. Fluent interfaces return the same object after each method call in order to allow chaining calls together to a single instruction. Therefore, they do not expose the internals of the object.
The Single Responsibility Principle (SRP)#
According to the SRP a module, class or function should only have one responsibility or rather one reason to change. The following example demonstrates a class with multiple responsibilities:
"""Do complex traffic sim here"""
for vehicle in self.vehicles:
self.gui.render_rectangle(vehicle.position, vehicle.length, vehicle.width)
This class violates the SRP as it has multiple reasons to change.
We have to modify
TrafficSimulation every time
something in the simulation logic changes
we want to change the way vehicles are rendered
the public interface of the GUI class changes
Instead, we could distribute the responsibilities to multiple classes as show in the UML diagram below.