Graphics Class Design : Shape

Chapter 14: Graphics Class Design

“Functional, durable, beautiful.”
Vitruvius

Shape 14.2

Class Shape represents the general notion of something that can appear in a Window on a screen:

  • It is the notion that ties our graphical objects to our Window abstraction, which in turn provides the connection to the operating system and the physical screen.
  • It is the class that deals with color and the style used to draw lines. To do that it holds a Line_style and a Color (for lines and for fill).
  • It can hold a sequence of Points and has a basic notion of how to draw them.

Experienced designers will recognize that a class doing three things probably has problems with generality. However, here, we need something far simpler than the most general solution.

We’ll first present the complete class and then discuss its details:

class Shape {  // deals with color and style and holds sequence of lines
public:
      void draw() const;                 // deal with color and draw lines
      virtual void move(int dx, int dy); // move the shape +=dx and +=dy

      void set_color(Color col);
      Color color() const;

      void set_style(Line_style sty);
      Line_style style() const;

      void set_fill_color(Color col);
      Color fill_color() const;

      Point point(int i) const;          // read-only access to points
      int number_of_points() const;

      virtual ~Shape() { }
protected:
      Shape();
      virtual void draw_lines() const;   // draw the appropriate lines
      void add(Point p);                 // add p to points
      void set_point(int i, Point p);    // points[i]=p;
private:
      vector<Point> points;               // not used by all shapes
      Color lcolor;                      // color for lines and characters
      Line_style ls;
      Color fcolor;                      // fill color

      Shape(const Shape&);           // prevent copying
      Shape& operator=(const Shape&);
};

This is a relatively complex class designed to support a wide variety of graphics classes and to represent the general concept of a shape on the screen. However, it still has only four data members and 15 functions. Furthermore, those functions are all close to trivial so that we can concentrate on design issues. For the rest of this section we will go through the members one by one and explain their role in the design.

An abstract class 14.2.1

Consider first Shape’s constructor:

protected:
      Shape();

The constructor is protected. That means that it can only be used directly from classes derived from Shape (using the :Shape notation). In other words, Shape can only be used as a base for classes, such as Line and Open_polyline. The purpose of that “protected:” is to ensure that we don’t make Shape objects directly. For example:

Shape ss;      // error: cannot construct Shape

Shape is designed to be a base class only. In this case, nothing particularly nasty would happen if we allowed people to create Shape objects directly, but by limiting use, we keep open the possibility of modifications to Shape that would render it unsuitable for direct use. Also, by prohibiting the direct creation of Shape objects, we directly model the idea that we cannot have/see a general shape, only particular shapes, such as Circle and Closed_polyline. Think about it! What does a shape look like? The only reasonable response is the counter question “What shape?” The notion of a shape that we represent by Shape is an abstract concept. That’s an important and frequently useful design notion, so we don’t want to compromise it in our program. Allowing users to directly create Shape objects would do violence to our ideal of classes as direct representations of concepts.

The constructor is defined like this:

Shape::Shape()
      : lcolor(fl_color()),            // default color for lines and characters
      ls(0),                           // default style
      fcolor(Color::invisible)      // no fill
{
}

It is a default constructor, so it sets the members to their default. Here again, the underlying library used for implementation, FLTK, “shines through.” However, FLTK’s notions of color and style are not mentioned directly by the uses. They are only part of the implementation of our Shape, Color, and Line_style classes. The vector defaults to an empty vector.

A class is abstract if it can be used only as a base class. The other – more common – way of achieving that is called a pure virtual function; see B’14.3.5. A class that can be used to create objects – that is, the opposite of an abstract class – is called a concrete class. Note that abstract and concrete are simply technical words for an everyday distinction. We might go to the store to buy a camera. However, we can’t just ask for a camera and take it home. What brand of camera? Which particular model camera? The word camera is a generalization; it refers to an abstract notion. An Olympus E-3 refers to a specific kind of camera, which we (in exchange for a large amount of cash) might acquire a particular instance of: a particular camera with a unique serial number. So, “camera” is much like an abstract (base) class; “Olympus E-3” is much like a concrete (derived) class, and the actual camera in my hand (if I bought it) would be much like an object.

The declaration

virtual ~Shape() { }

defines a virtual destructor. We won’t use that for now, so we leave the explanation to B’17.5.2, where we show a use.

Access control 14.2.2

Class Shape declares all data members private:

private:
      Vector<Point> points;
      Color lcolor;
      Line_style ls;
      Color fcolor;

Since the data members of Shape are declared private, we need to provide access functions. There are several possible styles for doing this. We chose one that we consider simple, convenient, and readable. If we have a member representing a property X, we provide a pair of functions X() and set_X() for reading and writing, respectively. For example:

void Shape::set_color(Color col)
{
      lcolor = col;

}

Color Shape::color() const
{
      return lcolor;
}

The main inconvenience of this style is that you can’t give the member variable the same name as its readout function. As ever, we chose the most convenient names for the functions because they are part of the public interface. It matters far less what we call our private variables. Note the way we use const to indicate that the readout functions do not modify their Shape (B’9.7.4).

Shape keeps a vector of Points, called points, that a Shape maintains in support of its derived classes. We provide the function add() for adding Points to points:

void Shape::add(Point p)         // protected
{
      points.push_back(p);
}

Naturally, points start out empty. We decided to provide Shape with a complete functional interface rather than giving users – even member functions of classes derived from Shape – direct access to data members. To some, providing a functional interface is a no-brainer, because they feel that making any member of a class public is bad design. To others, our design seems overly restrictive because we don’t allow direct write access to the members to all members of derived classes.

A shape derived from Shape, such as Circle and Polygon, knows what its points mean. The base class Shape does not “understand” the points; it only stores them. Therefore, the derived classes need control over how points are added. For example:

  • Circle and Rectangle do not allow a user to add
    points; that just wouldn’t make sense. What would be a rectangle with an extra
    point? (B’12.7.6)

  • Lines allows only pairs of points to be added (and not
    an individual point; B’13.3).

  • Open_polyline and Marks allow any number of points to
    be added.

  • Polygon allows a point to be added only by an add() that checks for intersections (B’13.8).

We made add() protected (that is, accessible from a derived class only) to ensure that derived classes take control over how points are added. Had add() been public (everybody can add points) or private (only Shape can add points), this close match of functionality to our idea of shapes would not have been possible.

Similarly, we made set_point() protected. In general, only a derived class can know what a point means and whether it can be changed without violating an invariant. For example, if we have a Regular_hexagon class defined as a set of six points, changing just a single point would make the resulting figure “not a regular hexagon.” On the other hand, if we changed one of the points of a rectangle, the result would still be a rectangle. In fact, we didn’t find a need for set_points() in our example classes and code, so set_point() is there, just to ensure that the rule that we can read and set every attribute of a Shape holds. For example, if we wanted a Mutable_rectangle, we could derive it from Rectangle and provide operations to change the points.

We made the vector of Points, points, private to protect it against undesired modification. To make it useful, we also need to provide access to it:

void Shape::set_point(int i, Point p)      // not used; not necessary so far
{
      points[i] = p;
}

Point Shape::point(int i) const
{
      return points[i];
}

int Shape::number_of_points() const
{
      return points.size();
}

In derived class member functions, these functions are used like this:

void Lines::draw_lines() const
      // draw lines connecting pairs of points
{
      for (int i=1; i<number_of_points(); i+=2)
            fl_line(point(i-1).x,point(i-1).y,point(i).x,point(i).y);
}

You might worry about all those trivial access functions. Are they not inefficient? Do they slow down the program? Do they increase the size of the program? No, they will all be compiled away (“inlined”) by the compiler. Calling number_of_points() will take up exactly as many bytes of memory and execute exactly as many instructions as calling points.size() directly.

These access control considerations and decisions are important. We could have provided this close-to-minimal version of Shape:

struct Shape {      // close-to-minimal definition - too simple - not used
      Shape();
      void draw() const;         // deal with color and call draw_lines
      virtual void draw_lines() const;   // draw the appropriate lines
      virtual void move(int dx, int dy);   // move the shape +=dx and +=dy

      vector<Point> points;               // not used by all shapes
      Color lcolor;
      Line_style ls;
      Color fcolor;
};

What value did we add by those extra 12 member functions and two lines of access specifications (private: and protected:)? The basic answer is that protecting the representation ensures that it doesn’t change in ways unanticipated by a class designer so that we can write better classes with less effort. This is the argument about “invariants” (B’9.4.3). Here, we’ll point out such advantages as we define classes derived from Shape. One simple example is that earlier versions of Shape used

Fl_Color lcolor;
int line_style;

This turned out to be too limiting (an int line style doesn’t elegantly support line width, and Fl_Color doesn’t accommodate invisible) and led to some messy code. Had these two variables been public and used in a user’s code, we could have improved our interface library only at the cost of breaking that code (because it mentioned the names line_color and line_style).

In addition, the access functions often provide notational convenience. For example, s.add(p) is easier to read and write than s.points.push_back(p).

More by Author

Must Read