7.6. Abstraction

Most programming constructs are abstractions. For example, even the idea of number on a computer is an abstraction.

Programmers need to define, manipulate, and store numbers. Each language and compiler implements specific properties and operations. C++ defines Integral types and Floating point types. However, these numbers are not the numbers you learned in your math classes. In math, you learned that:

None of these things are guaranteed true on a computer.

It depends on the abstractions used in your language. In C++, no numbers are infinite. Some numbers can only store positive values. The floating point numbers have two different representations for the value \(0\). Decimal numbers that have exact values in mathematics can only be approximated in C++.

Even with all these limitations, numeric types in C++ are quite useful. They achieve the goal of allowing programmers to work with numbers easily without having to worry much about how they are actually represented in hardware.

Abstraction is all about hiding implementation details.

Consider a car as an example of abstraction in design. Here’s an outline for starting a Model-T in 1915 [1] :

  1. On front of car, pull chock near right fender and engage crank lever under radiator.

    • Turn slowly to prime carburetor.

  2. Get into car, insert key in ignition.

    • Turn start setting to either magneto or battery.

    • Adjust timing stalk and throttle stalk.

    • Pull back on handbrake to place car in neutral.

  3. Return to front of car.

    • Use left hand to crank lever. (If the engine back-fires, using your left hand results in fewer broken arms)

  4. If car starts, jump in!

The Model-T was one of the most popular cars ever made. It had no frills and few abstractions. You had to understand how it was made even to get it started.

This is not true with most cars today. Compare the previous steps with starting a car today:

  1. Step on brake pedal and push button.

Over the last 100 years manufacturers have hidden most implementation details even though cars today are far more complex than a Model T.

7.6.1. Class abstractions

The design principles that apply to everyday objects also apply to software objects.

  • Hide as many details as possible

  • Don’t hide too many

You, as the designer of a class are responsible for getting the abstractions correct in the classes you design. Keep in mind that every class defines a new type. In general we want to create classes that are easy to use and easy to modify.

Let’s examine a poorly abstracted car class:

class Car {
  private:
    double speed_;
    double heading_;
    int    x_;
    int    y_;
  public:
    double speed ();
    double heading ();
    int    x ();
    int    y ();
    void speed (double speed);
    void heading (double direction);
    void x (int new_x);
    void y (int new_y);
};

What is wrong with this design?

  • We have created a class and dutifully made all of the class variables private.

  • We then (also dutifully) created functions to set and get each of the class member variables.

Isn’t this is what object-oriented programming is all about?

No, it is not.

What is wrong with this design? This list is long, but here are some of the major problems:

  1. By default, no class members are initialized. A default constructed car is invalid.

  2. It is still functionally a struct, even though this class has invariants.

    • The x and y values can be modified independently of speed and heading.

    • The speed could be set \(< 0\).

    • The heading could be anything. It’s not even clear what values are valid (\(0 - 360\)? \(0 - 2\pi\)?)

  3. The car position is maintained in two separate int members, not a location object.

  4. If users try to use this Car, then they are responsible for calculating correct x and y from the current heading and speed.

  5. Fundamentally, this is not how cars are operated.

    • A car has a steering wheel and at least 1 pedal to accelerate

    • Cars only change position when moving

    • Cars only change direction when moving AND when the steering wheel is turned

    • The steering wheel has limits to keep the tires aligned with the forward momentum.

How can we improve the abstractions in our car class?

First, group the x and y coordinates into a single type. We could do just this:

// a location on a Cartesian grid
struct Point {
  double x = 0.0;
  double y = 0.0;
};

This works, but it’s not much fun to use. When using Point objects, we will often want to initialize both the x and y values at once:

Point r {3.0,3.0};

The default constructors won’t do this automatically. We need a 2 argument constructor:

Point (double x_value, double y_value)
    : x{x_value}, y{y_value}
{}

Once a non-default constructor is added to a class, the compiler will not automatically generate the default constructor for us, so we need to be explicit:

Point () = default;

And putting them together we have very minimal class:

// a location on a Cartesian grid
struct Point {
  double x = 0.0;
  double y = 0.0;
  Point (double x_value, double y_value)
      : x{x_value}, y{y_value}
  {}
  Point () = default;
};

We can’t consider this class complete, but we will stop with Point for now and move onto the Car class. When we use Point, we should prevent Car users from changing it directly:

class Car {
  private:
    Point  location_;
    double speed_    = 0.0;
    double heading_  = 0.0;

  public:
    Point  location() const;
    double speed()    const;
    double heading()  const;
};

It is OK to keep the access functions for speed and heading. Car users are likely to need this information, but they should never set them directly. We should also promise that these functions will never change the state of a car by making all of the access functions const.

In order to make our car more ‘real’, we should add two more private member variables, one for the current steer angle and one for the current change in speed. Users are never given access to these, but internally a Car can use them to compute a new location:

class Car {
  private:
    Point  location_;
    double speed_    = 0;
    double heading_  = 0;
    double angle_    = 0;  // current steering angle
    double rate_     = 0;  // current change in speed

  public:
    Point  location() const;
    double speed()    const;
    double heading()  const;
};

Since users can’t change steer angle directly, there needs to be come way to influence that value. One way is to define an enumerated type to set the angle on the steering wheel:

enum class Direction { CENTER, LEFT, RIGHT };

With these parts added, we can add public functions that use them:

//
// Users steer the car by choosing a steer direction.
// As long as a direction is applied, the steer angle will increase (or decrease)
// towards the indicated direction until the max steering angle
// for the car is reached.
//
// The max steer angle might be Car make/model dependent.
//
double steer (Direction dir);

//
// Change the car speed by (de)accelerating.
// Positive values will increase car speed.
// Negative values will reduce car speed.
// Zero values leave the car speed unchanged.
double accelerate (double rate);

And finally, an update function a car simulator might call to modify the state of the car every time step. Putting it all together:

// a location on a Cartesian grid
struct Point {
  double x = 0.0;
  double y = 0.0;
  Point (double x_value, double y_value)
      : x{x_value}, y{y_value}
  {}
  Point () = default;
};

enum class Direction { CENTER, LEFT, RIGHT };

class Car {
  private:
    Point  location_;
    double speed_    = 0;
    double heading_  = 0;
    double angle_    = 0;
    double rate_     = 0;

  public:
    Point  location() const;
    double speed()    const;
    double heading()  const;

    double steer (Direction dir);
    double accelerate (double rate);
    void   update();
};

Actually implementing these functions is a lab assignment.

While this class is far from complete, at least now we have a design that we can extend without needing to completely change it later once we realize it did not control any of its invariants.


You have attempted of activities on this page