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:
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.
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.
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.
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();
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.
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.
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.
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.
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.
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.
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();
}
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.
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.
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
}
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
}
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.
- 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. |
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;
}
}
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();
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; }
}
}
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
}
}
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");
}
}
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");
}
}
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();
}
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
}
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
}
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.
No comments:
Post a Comment