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.

@dataclass
class Point:
    x: int
    y: int

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.

class Point(Protocol):
    def x(self) -> int:
        pass

    def y(self) -> int:
        pass

    def set_carthesian(self, x: int, y: int) -> None:
        pass

    def theta(self) -> int:
        pass

    def radius(self) -> int:
        pass

    def set_polar(self, radius: int, theta: int) -> None:
        pass

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.

Do Not!
gui.get_circle().get_center().set_position(point)

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.

Do
gui.move_circle_to(point)

Fluent interfaces

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.

house_builder.floor().walls().windows().floor().walls().roof().build()

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:

class TrafficSimulation:
    vehicles: list[Vehicle]
    gui: GUI

    def simulate_traffic(self):
        """Do complex traffic sim here"""

    def render(self):
        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.

UML diagram of the distributed responsibilities