Team LiB
Previous Section Next Section

The Power of Encapsulation

We learned earlier that encapsulation is the mechanism that bundles together the state information (attributes) and behavior (methods) of an object. Now that we've gained some insights into public/private accessibility, encapsulation warrants a more in-depth discussion.

It's useful to think of an object as a "fortress" that "guards" data—namely, the values of all of its attributes. Rather than trying to march straight through the walls of a fortress, which typically results in death and destruction (!), we ideally would approach the guard at the gate to ask permission to enter. Generally speaking, the same is true for objects: we can't directly access the values of an object's privately declared attributes without an object's permission and knowledge, that is, without using one of an object's publicly accessible methods/properties to access the attribute's value.

Assume that you've just met someone for the first time, and wish to know his name. One way to determine his name would be to reach into his pocket, pull out his wallet, and look at his driver's license—essentially, accessing his private attribute values without his permission! The more "socially acceptable" way would be to simply ask him for his name—akin to using his GetName method or Name property—and to allow him to respond accordingly. He may respond with his formal name, or a nickname, or an alias, or may say "It's none of your business!"—but the important point is that you're giving the person (object) control over his response.

By restricting access to an object's private attributes through public accessors, we derive three benefits:

Let's discuss each of these benefits in detail.

Preventing Unauthorized Access to Encapsulated Data

Some of the information that a Student object maintains about itself—say, the student's identification number—may be highly confidential. A Student object may choose to selectively pass along this information when necessary—for example, when registering for a course—but may not wish to hand out this information to any object that happens to casually ask for it. Simply by making the attribute private, and intentionally omitting a public "getter" with which to request the attribute's value, there would be no way for another object to request the Student object's identification number.

Helping to Ensure Data Integrity

As mentioned previously, one of the arguments against declaring public attributes is that the object loses control over its data. As we saw earlier, a public attribute's value can be changed by client code without regard to any business rules that the object's class may wish to impose. On the other hand, when an accessor is used to change the value of a private attribute, value checking can be built into the set method or set accessor of a property to ensure that the attribute value won't be set to an "improper" value.

As an example, let's say that we've declared a Student attribute as follows:

private string birthDate;

Our intention is to record birth dates in the format "mm/dd/yyyy". By providing accessors with which to manipulate the birthDate attribute (instead of permitting direct public access to the attribute), we can provide logic to validate the format of any newly proposed date, and reject those that are invalid. We'll illustrate this concept by declaring a property called BirthDate for the student class as illustrated in the following code; but again, keep in mind that we could accomplish essentially the same "bulletproofing" through the creation of a SetBirthDate method:

public class Student
{
  private string birthDate;
  // Details omitted.

  // Properties.

  public string BirthDate {
    get {
      return birthDate;
    }
    set {
      // Perform appropriate validations.
      // Remember, italics represent pseudocode!
      if (date is not in the format mm/dd/yyyy) {
        do not update the birthDate attribute
      }
      else if (mm not in the range 01 to 12) {
        do not update the birthDate attribute
      }
      else if (the day number isn't valid for the selected month) {
        do not update the birthDate attribute
      }
      else if (the year is NOT a leap year, but 2/29 was specified) {
        // details omitted ...
      }
      // etc. for other validation tests.
      else {
        // All is well with what was passed in as a value to this
        // set accessor, and so we can go ahead and update the value of the
        // birthDate attribute with this value.
        birthDate = value;
      }
    }
  }

  // etc.
}

If an attempt is made to pass an invalid birth date to the property from client code, as in

s.BirthDate = "foo";

the change will be rejected and the value of the birthDate attribute will be unchanged.

On the other hand, if birthDate had been declared to be a public attribute:

public class Student
{
  public string birthDate;
  // etc.

then setting the attribute directly, thus bypassing the property (note lowercase "b") as follows:

s.birthDate = "foo";

would corrupt the attribute's value.

Limiting "Ripple Effects" When Private Features Change

Despite our best attempts, we often have a need to go back and modify code after an application has been deployed, either when an inevitable change in requirements occurs, or if we unfortunately discover a design flaw that needs attention. Unfortunately, this can often open us up to "ripple effects," wherein dozens, or hundreds, or thousands of lines of code throughout an application have to be changed, retested, etc. One of the most dramatic examples of the impact of ripple effects was the notorious "Y2K" problem: when the need to change date formats to accommodate a four-digit year arose, the burden to hunt through millions of lines of code in millions of applications worldwide to find all such cases—and to fix them without breaking anything!—was mind boggling.

Perhaps the most dramatic benefit of encapsulation combined with information hiding, therefore, is that the hidden implementation details of a class—i.e., its private data structure and/or its (effectively private) accessor code—can change without affecting how an object belonging to that class gets used in client code. To illustrate this principle, we'll craft an example using properties; but again, realize that the same power of encapsulation can be achieved through the appropriate use of "get"/"set" methods.

Let's say that an attribute is declared in the Student class as follows:

private int age;

and that we craft a corresponding Age property as follows:

public int Age {
  get {
    return age;
  }
  // will be read-only -- no set accessor provided.
}

We then proceed to use our Student class in countless applications; so, in thousands of places within the client code of these applications, we write statements such as the following, relying on the get accessor to provide us with a student's age as an int value:

if (s.Age <= 21) { ... }
// or:
int retirementAge = s.Age + 20;
// etc.

A few years later, we decide to change the data structure of a Student so that, instead of maintaining an age attribute explicitly, we instead use the student's birthDate attribute to compute a student's age whenever it's needed. We thus modify our Student class code as follows:

The "Before" Code

The "After" Code

public class Student
{
  // We have an explicit age attribute.
  private int age;

  public int Age {
    get {
      return age;
    }
  }

  // etc.
}
public class Student
{
  // We replace age with birthDate.
    private DateTime birthDate;

  public int Age {
    get {
      // Compute the age on demand
      // (pseudocode).
      return (system date-birthDate);
    }
  }

  // etc.
}

In the "after" version of Student, we're computing the student's age by subtracting his or her birth date (an attribute) from today's date. This is an example of what we informally refer to as a "pseudoattribute"—to client code, the presence of an Age property implies that there is an attribute by the name of age, when in fact there may not be!

Note?/td>

Note that the same effect can be achieved with "get" and "set" methods: i.e., the availability of a getXxx() method signature for a class doesn't guarantee that there is actually an explicit attribute by the name of xxx in that class.

The beauty is that we don't care that the private details of the Student class design have changed! In all of the thousands of places within the client code of countless applications where we've used an expression such as

// This client code is unaffected by the private details of how age is
// computed internally to the Student class. Such details can in fact
// change after this client code is written, and the client code
// won't "break"!
if (s.Age <= 21) { ... }
// or:
int retirementAge = s.Age + 20;

to retrieve a student's age as an int value, this code will continue to work as-is, without any changes being necessary. Hence, we've avoided the "dreaded" ripple effect and have dramatically reduced the amount of effort necessary to accommodate a design change.

Such changes are said to be encapsulated, or limited to the internal code of the Student class only.

Of course, all bets are off if the developer of a class changes one of its public method or property headers, because then all of the client code that passes messages to objects of this type using this method or property header will potentially have to change. For example, if we were to change the Student class design as follows:

public class Student
{
  // We've changed the type of the age attribute from int to float ...
  private float age;

  // ... and the type of the Age attribute accordingly.
  public float Age {
    get {
      // details omitted.
    }
  }

  // etc.
}

then much our client code would indeed "break," as in the following example:

// This will no longer compile!
int currentAge = s.Age;

This client code will "break" because we now have a type mismatch: we're getting back a float value, but are trying to assign it to an int variable, which as we learned in Chapter 1 will generate a compiler error as follows:

    error CS0029: Cannot implicitly convert type 'float' to 'int'

We'd have to hunt for all of the thousands of places in our countless applications where we are using the Age get accessor, and modify our code, such as the following:


// We're now using a cast.
int currentAge = (int) s.Age;

which is indeed a major ripple effect. But again, this ripple effect is due to the fact that we changed a public feature of our class—a public property header, to be precise. As long as we restrict our changes to private features of a class—private attributes and accessor code bodies, but not public method/property header(s)— ripple effects aren't an issue. Any client code that was previously written to utilize Student accessors will continue to work as intended; the client code will be blissfully ignorant that the internal details of the Student class have changed.

Using Accessors from Within a Class's Own Methods

As we saw earlier in the chapter, there are no restrictions on directly accessing a class's features from within that class's own methods. Both public and private features may be manipulated at will, using simple names, as the following code example illustrates:

using System;

public class Student
{
  private string name;
  // etc.

  // Properties.

  public string Name {
    get {
      return name;
    }
    set {
      name = value;
    }
  }

  // etc.

  public void PrintAllAttributes() {
    // We're directly accessing the values of the name attribute, simply
    // because we CAN! We're not bothering to use the Name property
    // declared above.
    Console.WriteLine(name);
    // etc.
  }

  // etc.
}

However, it's considered a best practice to get into the habit of invoking a class's accessors, when available, even from within that class's own methods, vs. directly accessing attributes "just because we can," as a convenience. The reason for this is as follows: just because an attribute may be simple today, there's no guarantee that it won't become complicated down the road.

As an example, let's say that after we've programmed the Student class—including the PrintAllAttributes method shown previously—we decide that we want a student's name to always appear with the first name abbreviated as a single letter followed by a period; for example, "John Smith" should always appear as "J. Smith". So, we change the internal logic of the Name property's get accessor to make it a bit more sophisticated, as follows:

public class Student
{
  private string name;
  // etc.

  public string Name {
    // This version of the get accessor does more work: it reformats
    // the Student's name as internally stored, e.g.,
    // "Chris Williams" would be returned as "C. Williams".
    get {
      // Declare a few temporary variables.
      string firstInitial;
      string lastName;

      // Extract the first letter of the first name from the "name"
      // attribute, and store the result in variable "firstInitial".
      // (Details omitted.)

      // Extract the last name from the "name" attribute, and store
      // the result in variable "lastName".
      // (Details omitted.)
      return firstInitial + ". " + lastName;
    }
    set {
      name = value;
    }
  }

  // etc.
}

No longer does the Name get accessor simply return the value of the name attribute unaltered, as it used to. But, if from within our PrintAllAttributes method we're still directly accessing the values of the attributes via dot notation:

  public void PrintAllAttributes() {
    // We're still directly accessing the values of the attributes, and
    // so we're losing out on the logic of the newly crafted Name
    // property's get accessor.
    Console.WriteLine(name);
    // etc.
  }

  // etc.
}

we wind up circumventing the Name property, which means that all of our hard work to restructure the student's name in the Name get accessor is being ignored. Hence, client code such as the following:

Student s = new Student();
s.Name = "Cynthia Coleman";
// details omitted.
s.PrintAllAttributes();

produces incorrect results:

Cynthia Coleman

On the other hand, if we had recrafted the PrintAllAttributes method to utilize the Name get accessor:

public void PrintAllAttributes() {
  // We're now using our own get accessors.
  Console.WriteLine(Name);
  // etc.

then we'd have automatically benefited from the change that we made to Name, and our client code would now produce correct results:

Student s = new Student();
s.Name = "Cynthia Coleman";
// details omitted.
s.PrintAllAttributes();

This results in the following output:

C. Coleman

again without having to have modified the client code.


Team LiB
Previous Section Next Section