Earlier in this tutorial, you learned about two of the important principles of object-oriented
programming, Inheritance and Polymorphism.
Now that you've seen much of the syntax of C#, I'll show you how C# supports the
another of the object-oriented principles - Encapsulation. This lesson will discuss
Encapsulation with the following objectives:
- Understand the object-oriented principle of Encapsulation.
- Learn the available modifiers for type members.
- Protect object state through properties.
- Control access to methods.
- Learn how to modify types for assembly encapsulation
What is Encapsulation and How Does It Benefit Me?
In object-oriented programming, you create objects that have state and behavior.
An object's state is the data or information it contains. For example, if you have
a BankAccount object, its state could be Amount and CustomerName.
Behavior in an object is often represented by methods. For example, the BankAccount
object's behavior could be Credit, Debit, and GetAmount.
This sounds like a nice definition of an object, and it is, but you must also consider
how this object will be used.
When designing an object, you must think about how others could use it. In a best-case
scenario any program using the object would be well designed and the code would
never change. However, the reality is that programs do change often and in a team
environment many people touch the same code at one time or another. Therefore, it
is beneficial to consider what could go wrong as well as the pristine image of how
the object *should* be used.
In the case of the BankAccount object, examine the situation where code
outside of your object could access a decimal Amount field or
a string CustomerName field. At the point of time that the code
is written, everything would work well. However, later in the development cycle,
you realize that the BankAccount object should keep track of an int
CustomerID rather than string CustomerName because you don't want
to duplicate relationships between information (or some other valid reason to alter
the definition of internal state). Such changes cause a rippling effect in your
code because it was built to use the BankAccount class, as originally designed
(with CustomerName being a string), and you must now change code that accesses
that state throughout your entire application.
The object-oriented principle of Encapsulation helps avoid such problems, allowing
you to hide internal state and abstract access to it though type members such as
methods, properties, and indexers. Encapsulation helps you reduce coupling between
objects and increases the maintainability of your code.
Type Member Access Modifiers
An access modifier allows you to specify the visibility of code outside a type or
assembly. Access modifiers can be applied to either types or type members. A later
section on Type Access Modifiers discusses modifiers that can be applied to types.
This section discusses those modifiers that apply to type members and how they affect
visibility.
Generally, you should hide the internal state of your object from direct access
from outside code. Then implement other members, such as methods and properties, that
wrap that state. This allows the internal implementation of the state to change
at will, while the members wrapping the state can still return a representation
of the state that doesn't change. This means that outside code will access your
object via members that wrap state and be guaranteed that the type of information
extracted is consistent. Additionally, because external code doesn't have access
to the internal state of your object, they can't alter that state in an inconsistent
manner that could break the way your object works.
The first step in encapsulating object state is to determine what type of access
that outside code should have to the members of your type. This is performed with
access modifiers. The type of access granted varies from no external access at all
to full public access and a few variations in between the extremes. Table 19-1 lists
all of the type member access modifiers and explains their meaning.
Table 19-1. Type member access modifiers control what code has access to a specified type member.
Access Modifier | Description (who can access) |
---|---|
private | Only members within the same type. (default for type members) |
protected | Only derived types or members of the same type. |
internal | Only code within the same assembly. Can also be code external to object as long as it is in the same assembly. (default for types) |
protected internal | Either code from derived type or code in the same assembly. Combination of protected OR internal. |
public | Any code. No inheritance, external type, or external assembly restrictions. |
As you've learned from previous lessons of the C# Tutorial, types contain several types of members, including constructors, properties, indexers, methods, and others. Rather than show you an exhaustive list of all of the permutations of access modifiers you can use with these members, I'll take a more practical approach and describe a sub-set of access modifiers used on properties and methods.
Opening Type Members to public Access
You've seen the public access modifier used in earlier parts of the C#
Tutorial. Any time the public access modifier is used on a type member,
calling code will be able to access the type member. If you make your type member
public, you are giving everyone permission to use it. Listing 19-1 shows an example
of using the public access modifier on a method.
Listing 19-1. Declaring a Method with a public Access Modifier: BankAccountPublic.cs
using System;
class BankAccountPublic
{
public decimal GetAmount()
{
return 1000.00m;
}
}
class BankAccountPublic
{
public decimal GetAmount()
{
return 1000.00m;
}
}
The GetAmount() method in Listing 19-1 is public meaning that it can be
called by code that is external to this class. Now, you can write the following
code, elsewhere in your program, to use this method:
BankAccountPublic bankAcctPub = new
BankAccountPublic();
// call a public method
decimal amount = bankAcctPub.GetAmount();
// call a public method
decimal amount = bankAcctPub.GetAmount();
All you need to do, as shown above, is create an instance of the class that contains
the method and then call the method through that instance. Because it is public,
you won't have a problem. Remember that the default access for a type member is
private, which we'll talk about next. This means that if you forget the
public modifier, and didn't use any modifier at all, you would receive
a compiler error.
Hiding Type Members with private Access
A private type member is one that can only be accessed by members within
the same type. For example, if the BankAccount class has a private member,
only other members of the BankAccount class can access or call that member.
Although the default access for type members is private, I prefer to be
explicit about my intentions when declaring type members and include the access
modifier, rather than rely on defaults. I think it makes the code easier to read
and makes it clear to other developers what my true intention is. Listing 19-2 shows
how to use the private access modifier and offers an example of why you
would want to use it.
Listing 19-2. Declaring a private Field: BankAccountPrivate.cs
using System;
class BankAccountPrivate
{
private string m_name;
public string CustomerName
{
get { return m_name; }
set { m_name = value; }
}
}
class BankAccountPrivate
{
private string m_name;
public string CustomerName
{
get { return m_name; }
set { m_name = value; }
}
}
It's common to encapsulate the state of your type with properties. In fact, I always
wrap my type state in a property. In Listing 19-2, you can see how the name of the
customer is held in the m_name field, but it is wrapped (encapsulated)
with the CustomerName property. Because m_name is declared as
private, code outside the BankAccountPrivate class can't access
it directly. They must use the public CustomerName property instead.
Now you can change the implementation of m_name in any way you want. For
example, what if you wanted it to be an ID of type int and the CustomerName
property would do a search to find the name or what if you wanted to have first
and last name values that the CustomerName property could concatenate.
There are all kinds of things happening to your code in maintenance that will causes
implementation to change. The point is that private members allow the implementation
to change without constraining the implementation or causing rippling effects throughout
your code base that would have occurred if that external code had access to the
members of your type.
The private and public access modifiers are at the two extremes
of access, either denying all external access or allowing all external access, respectively.
The other access modifiers are like different shades of gray between these two extremes,
including the protected modifier, discussed next.
Access for Derived Types with the protected Access Modifier
In some ways, the protected access modifier acts like both the private
and public access modifiers. Like private, it only allows access
to members within the same type, except that it acts like public only to
derived types. Said another way, protected type members can only be accessed
by either members within the same type or members of derived types.
Returning to the BankAccount example, what if you needed to call code to
close an account? Furthermore, what if there were different types of accounts? Each
of these different account types would have their own logic for closing, but the
basic process would be the same for all account types. If this sounds to you like
the description of Polymorphism, you would be on the right track. Back in Lesson 9,
we discussed polymorphism and how it allows us to treat different classes the same
way. You may want to refer to Lesson 9 for a refresher
before looking at the next example.
In the case of closing an account, there are several things that need to be done
like calculating interest that is due, applying penalties for early withdrawal,
and doing the work to remove the account from the database. Individually, you don't
want any code to call methods of the BankAccount class unless all of the
methods are called and each method is called in the right order. For example, what
if some code called the method to delete the account from the database and didn't
calculate interest or apply penalties? Someone would lose money. Also, if the calling
code were to delete the account first then the other methods would run into errors
because the account information isn't available. Therefore, you need to control
this situation and Listing 19-3 shows how you can do it.
Listing 19-3. Declaring protected Methods: BankAccountProtected.cs
using System;
class BankAccountProtected
{
public void CloseAccount()
{
ApplyPenalties();
CalculateFinalInterest();
DeleteAccountFromDB();
}
protected virtual void ApplyPenalties()
{
// deduct from account
}
protected virtual void CalculateFinalInterest()
{
// add to account
}
protected virtual void DeleteAccountFromDB()
{
// send notification to data entry personnel
}
}
class BankAccountProtected
{
public void CloseAccount()
{
ApplyPenalties();
CalculateFinalInterest();
DeleteAccountFromDB();
}
protected virtual void ApplyPenalties()
{
// deduct from account
}
protected virtual void CalculateFinalInterest()
{
// add to account
}
protected virtual void DeleteAccountFromDB()
{
// send notification to data entry personnel
}
}
The most important parts of Listing 19-3 are that the CloseAccount method
is public and the other methods are protected. Any calling code can instantiate
BankAccountProtected, but it can only call the CloseAccount method.
This gives you protection from someone invoking the behavior of your object in inappropriate
ways. Your business logic is sound.
At the end of this section, you'll see an example of how to call the code in Listing
19-3. For now, it is essential that you see how the other pieces fit together first.
If you only wanted the BankAccountProtected class to operate on its own
members, you could have made the protected methods private instead.
However, this code supports a framework where you can have different account types
such as Savings, Checking, and more. You will be able to add new account types in
the future because the BankAccountProtected class is designed to support
them with protected virtual methods. Listings 19-4 and 19-5 show you the
SavingsAccount and CheckingAccount classes that derive from the
BankAccountProtected class.
Listing 19-4. Derived SavingsAccount Class Using protected Members of its Base Class: SavingsAccount.cs
using System;
class SavingsAccount : BankAccountProtected
{
protected override void ApplyPenalties()
{
Console.WriteLine("Savings Account Applying Penalties");
}
protected override void CalculateFinalInterest()
{
Console.WriteLine("Savings Account Calculating Final Interest");
}
protected override void DeleteAccountFromDB()
{
base.DeleteAccountFromDB();
Console.WriteLine("Savings Account Deleting Account from DB");
}
}
class SavingsAccount : BankAccountProtected
{
protected override void ApplyPenalties()
{
Console.WriteLine("Savings Account Applying Penalties");
}
protected override void CalculateFinalInterest()
{
Console.WriteLine("Savings Account Calculating Final Interest");
}
protected override void DeleteAccountFromDB()
{
base.DeleteAccountFromDB();
Console.WriteLine("Savings Account Deleting Account from DB");
}
}
Notice how SavingsAccount derives from BankAccountProtected. SavingsAccount
can access any of the protected members of the BankAccountProtected
class which is its base class. It demonstrates this fact via the call to base.DeleteAccountFromDB
in it's DeleteAccountFromDB method. If the inheritance part of Listing
19-4 is a little confusing, you can visit Lesson 8: Class Inheritance
for a refresher and better understanding. Each method of SavingsAccount
has the protected access modifier also, which simply means that classes derived
from SavingsAccount can access those SavingsAccount members with
the protected access modifier. The same situation exists with the CheckingAccount
class, shown in Listing 19-5.
Listing 19-5. Derived CheckingAccount Class Using protected Members of its Base Class: CheckingAccount.cs
using System;
class CheckingAccount : BankAccountProtected
{
protected override void ApplyPenalties()
{
Console.WriteLine("Checking Account Applying Penalties");
}
protected override void CalculateFinalInterest()
{
Console.WriteLine("Checking Account Calculating Final Interest");
}
protected override void DeleteAccountFromDB()
{
base.DeleteAccountFromDB();
Console.WriteLine("Checking Account Deleting Account from DB");
}
}
class CheckingAccount : BankAccountProtected
{
protected override void ApplyPenalties()
{
Console.WriteLine("Checking Account Applying Penalties");
}
protected override void CalculateFinalInterest()
{
Console.WriteLine("Checking Account Calculating Final Interest");
}
protected override void DeleteAccountFromDB()
{
base.DeleteAccountFromDB();
Console.WriteLine("Checking Account Deleting Account from DB");
}
}
The CheckingAccount class in Listing 19-5 is implemented similar to SavingsAccount
from Listing 19-4. If you were writing this, the difference would be that the methods
of each class would have unique implementations. For example, the business rules
associated with the final interest calculation would differ, depending on whether
the account type was checking or savings.
Notice the call to the base class method in the DeleteAccountFromDB method
in CheckingAccount. Just like SavingsAccount, CheckingAccount
has access to BankAccountProtected's protected method because it is a derived
class. This is a common pattern in polymorphism because derived classes often have
a responsibility to call virtual base class methods to ensure critical functionality
has the opportunity to execute. You would consult the method documentation to see
if this was necessary. Without a protected access modifier, your only option
would have been to make the base class method public; which, as explained
earlier, is dangerous.
To use the code from Listings 19-3, 19-4, and 19-5, you can implement the following
code:
BankAccountProtected[]
bankAccts = new
BankAccountProtected[2];
bankAccts[0] = new SavingsAccount();
bankAccts[1] = new CheckingAccount();
foreach (BankAccountProtected acct in bankAccts)
{
// call public method, which invokes protected virtual methods
acct.CloseAccount();
}
bankAccts[0] = new SavingsAccount();
bankAccts[1] = new CheckingAccount();
foreach (BankAccountProtected acct in bankAccts)
{
// call public method, which invokes protected virtual methods
acct.CloseAccount();
}
Since both SavingsAccount and CheckingAccount derive from BankAccountProtected,
you can assign them to the bankAccts array. They both override the protected
virtual methods of BankAccountProtected, so it is the SavingsAccount
and CheckingAccount methods that are called when CloseAccount
in BankAccountProtected executes. Remember that the only reason the methods
of SavingsAccount and CheckingAccount can call their virtual
base class methods, as in the case of DeleteAccountFromDB, is because the
virtual base class methods are marked with the protected access
modifier.
A Quick Word on internal and protected internal Access Modifiers:
In practice, most of the code you write will involve the public, private,
and protected access modifiers. However, there are two more access modifiers
that you can use in more sophisticated scenarios: internal and protected
internal.
You would use internal whenever you created a separate class library and
you don't want any code outside of the library to access the code with internal
access. The protected internal is a combination of the two access modifiers
it is named after, which means either protected or internal.
Access Modifiers for Types:
So far, the discussion of access modifiers has only applied to the members of types.
However, the rules are different for the types themselves. When talking about types,
I'm referring to all of the C# types, including classes, structs, interfaces, delegates,
and enums. Nested types, such as a class defined within the scope of a class, are
considered type members and fall under the same access rules as other type members.
Types can have only two access modifiers: public or internal.
The default, if you don't specify the access modifier, is internal. Looking
at all of the classes used in this lesson, you can see that they are internal
because they don't have an access modifier. You can explicitly specify internal
like this:
internal
class InternalInterestCalculator
{
// members go here
}
{
// members go here
}
Perhaps the InternalInterestCalculator, shown above, has special business
rules that you don't want other code to use. Now, it is in a class library of its
own and can only be accessed by other code inside of that same class library (DLL).
Note: To be more specific, internal means that only code
in the same assembly can access code marked as internal. However, discussing
the definition of an assembly is outside the scope of this lesson, so I am simplifying
the terminology.
If you declared a class inside of a class library that you wanted other code to
use, you would give it a public access modifier. The following code shows
an example of applying the public access modifier to a type:
public
class BankAccountExternal
{
// members go here
}
{
// members go here
}
Clearly, a bank account is something you would want to access from outside of a
class library. Therefore, it only makes sense to give it a public access
modifier as shown in the BankAccountExternal class above.
Tip: A common gottcha in Visual Studio occurs when you create a
new class in a class library. The default template doesn't include an access modifier.
Then, when you try to write code that uses the new class in your program (which
references the class library), you get a compiler error saying that the class doesn't
exist. Well, you know it exists because you just wrote it and are looking at the
page. If you've already seen the clue I've given you so far, you'll key on the fact
that the default template left out the access modifier on the type. This makes the
class default to internal, which can't be seen outside of the assembly.
The fix is to give the class a public modifier, like the BankAccountExternal
class above.
Summary
Encapsulation is an object-oriented principle of hiding the internal state and behavior
of an object, making your code more maintainable. In C#, you can manage encapsulation
with access modifiers. For example, the public access modifier allows access
to any code but the private access modifier restricts access to only members
of a type. Other access modifiers restrict access in the range somewhere between
public and private. While you can use any of the access modifiers
on type members, the only two access modifiers you can use on types are the public
and internal.
No comments:
Post a Comment