Search code examples
c++inheritanceencapsulation

A question about encapsulation and inheritence practices


I've heard people saying that having protected members kind of breaks the point of encapsulation and is not the best practice, one should design the program such that derived classes will not need to have access to private base class members.


An example situation

Now, imagine the following scenario, a simple 8bit game, we have bunch of different objects, such as, regular boxes act as obstacles, spikes, coins, moving platforms etc. List can go on.

All of them have x and y coordinates, a rectangle that specifies size of the object, and collision box, and a texture. Also they can share functions like setting position, rendering, loading the texture, checking for collision etc.

But some of them also need to modify base members, e.g. boxes can be pushed around so they might need a move function, some objects may be moving by themselves or maybe some blocks change texture in-game.

Therefore a base class like object can really come in handy, but that would either require ton of getters - setters or having private members to be protected instead. Either way, compromises encapsulation.


Given the anecdotal context, which would be a better practice:

1. Have a common base class with shared functions and members, declared as protected. Be able to use common functions, pass the reference of base class to non-member functions which only needs to access shared properties. But compromise encapsulation.

2. Have a separate class for each, declare the member variables as private and don't compromise encapsulation.

3. A better way that I couldn't have thought.


I don't think encapsulation is highly vital and probably way to go for that anecdote would be just having protected members, but my goal with this question is writing a well practiced, standard code, rather than solving that specific problem.

Thanks in advance.


Solution

  • First off, I'm going to start by saying there is not a one-size fits all answer to design. Different problems require different solutions; however there are design patterns that often may be more maintainable over time than others.

    Indeed, a lot of suggestions for design make them better in a team environment -- but good practices are also useful for solo projects as well so that it can be easier to understand and change in the future.

    Sometimes the person who needs to understand your code will be you, a year from now -- so keep that in mind😊

    I've heard people saying that having protected members kind of breaks the point of encapsulation

    Like any tool, it can be misused; but there is nothing about protected access that inherently breaks encapsulation.

    What defines the encapsulation of your object is the intended projected API surface area. Sometimes, that protected member is logically part of the surface-area -- and this is perfectly valid.

    If misused, protected members can give clients access to mutable members that may break a class's intended invariants -- which would be bad. An example of this would be if you were able to derive a class exposing a rectangle, and were able to set the width/height to a negative value. Functions in the base class, such as compute_area could suddenly yield wrong values -- and cause cascading failures that should otherwise have been guarded against by better encapsulated.

    As for the design of your example in question:

    Base classes are not necessarily a bad thing, but can easily be overused and can lead to "god" classes that unintentionally expose too much functionality in an effort to share logic. Over time this can become a maintenance burden and just an overall confusing mess.

    Your example sounds better suited to composition, with some smaller interfaces:

    • Things like a point and a vector type would be base-types to produce higher-order compositions like rectangle.
    • This could then be composed together to create a model which handles general (logical) objects in 2D space that have collision.
    • intersection/collision logic can be handled from an outside utility class
    • Rendering can be handled from a renderable interface, where any class that needs to render extends from this interface.
    • intersection handling logic can be handled by an intersectable interface, which determines behaviors of an object on intersection (this effectively abstracts each of the game objects into raw behaviors)
    • etc