|
|
|
In this age of technology, encapsulation is everywhere around us. For example, do you understand all the details of how your car works? Probably not, and with good reason: even a relatively cheap car is still an incredibly complex piece of machinery. Furthermore, you don't need to understand all the details of how your car works, because the auto industry has provided you with a simple interface that any human can learn to use - a steering wheel, a clutch, and two floor pedals. These four devices cause a fantastic number of things to happen under your hood, but you don't need to worry about any them. You just turn the wheel, push the petals and work the clutch, and all those thousands of little details are pretty much taken care of for you. Your microwave works the same way; you push a few buttons on the front of the unit and let it cook your food, without having to know what every circuit and switch inside the microwave is actually doing. And the same thing can be said about stereos, plasma televisions, iPods and a zillion other little gadgets that we take for granted. The point is that we can use all of these things at a very simplified, abstract level, and ignore all the billions of "under the hood" details that are hidden from plain view. Well, guess what? OOP lets us do the same thing with our code! Why think of your program in terms of integers and strings, when you can think about it in terms of Pizzas, Employees, or whatever it is that you're actually writing about? Just like that GetInt() method we wrote in the previous unit, classes allow us to encapsulate an infinite number of details, and only expose the ones we actually want the user to see. We're not trying to be underhanded or sneaky; we're simply hiding all the stuff that isn't going to help us. And the way to do this is with the "private" keyword... class Pizza { private string CookName; } We've added a new member to the Pizza class from the previous lesson, and called it "CookName". But since we've used the "private" keyword, instead of the "public" keyword, only the methods and constructors within the Pizza class have access to it. Main(), and any other code outside the Pizza class, has no idea that CookName even exists. It's a detail that we've hidden away. The good thing about this is that no one outside the Pizza class can change the name of a pizza object's cook. The bad thing is that no one outside the Pizza class can see or use the name, either. What if we want to let people know who the chef is, without letting them change the name? Well, one way is to write a public method that provides limited access to the data: public string GetCookName() { return CookName; } Other classes, as well as Main(), still know nothing about the Pizza class's private "CookName" variable. But they do know about the Pizza class's GetCookName() method, which tells them the same thing. In this manner, we can control the manner in which users access an object's data. By making GetCookName() public, we allow outside code to access a private member, but only through that method. Like the steering wheel of a car, it provides a public interface to a private implementation. And we don't even need to be "honest" about it; we can write GetCookName() to do whatever we want: public string GetCookName() { return "Billy Bob"; } Now, GetCookName() isn't even providing the "real" value of CookName! Of course, the code that calls this method will never know the difference. And that's a very good thing, because this is a critical concept of OOP: it's none of anybody's business what the object is actually doing internally! If the class is programmed so that GetCookName() always returns "Billy Bob", then the rest of the program just has to assume that that's what the method is supposed to do. A class can provide as little or as much access to its data as the programmer deems necessary, in any manner that it deems appropriate. As a rule of thumb, start by assuming that everything in the class will be private, and then provide public access as you need it. But never show more than you have to, because every exposed interface is just one more route for programming bugs to creep in! The fewer paths that code has to private data, the better. For example, now let's say that we want to allow external code to change the value of CookName, but we only want to permit a few different values. We can accomplish that as follows: public void SetCookName(string newName) { // Only allow the name to be set to "Tony" or "Luigi" if( newName == "Tony" || newName == "Luigi" ) CookName = newName; } Once again, we've provided access to private data, but we still have control over what happens. The user can call our "set" method, but nothing's going to happen unless they play by our rules. This whole "get/set" thing is like putting a moat around a castle: the only way in is across a little bridge that we can control. Still, it would be nice if we could get that effect without having to write two methods for each variable that needs get/set access, wouldn't it? Well, have no fear! C# has invented a brand-new, built-in construct for this very purpose: public string PizzaCookName { get { return CookName; } set { if( value == "Tony" || value == "Luigi" ) CookName = value; } } What you're looking at is called a "property". It's signature is comparable to a method, but without the ( ) brackets. To any code outside of Pizza, it looks and works exactly like a public member. Main() can now say "pizza1.PizzaCookName = "Luigi"", just as if "PizzaCookName" was an actual public member. Under the hood, however, we can see that it's actually just a fancy interface for our private CookName member. The rules regarding line breaks are really relaxed in a property, by the way. The following is perfectly legal: public string PizzaCookName { get { return CookName; } set { if( value == "Tony" || value == "Luigi" ) CookName = value; } } We could even cram it all on one line, which is what I usually do with properties that don't have any code in them (besides returning the value or setting the value, of course). In the "set" clause, you might be wondering where the word "value" is coming from. The simple and frank answer is, nowhere. It's a special keyword that only applies in a "set" clause, and it contains the value that the outside code is attempting to assign to your internal member. You can do anything with "value" that you would be able to do with a variable that's the same datatype as the property itself. By the way, a property doesn't need both a get and a set; you only actually need one of them. You can omit the "set" for a read-only property, or the "get" for a write-only property (why you'd want a write-only property is beyond me, though). Of course, "PizzaCookName" is a slightly verbose name. In fact, prefixing a public member or method with "Pizza" is downright stupid, because it's already in a class called "Pizza". The only reason I called it that was to avoid a naming conflict with our private member, which was already called "CookName". This is why many programmers prefix a class's private members with an underscore. It both highlights the fact that it's a private variable, and allows us to give the simpler, un-prefixed name to the interface that the rest of the code will actually see: class Pizza { private string _cookName; public string CookName { get{ return _cookName; } set{ _cookName = value; } } } In the example above, I omitted the "if" that I was doing in the "set" clause, just to simplify things. That being said, however, a lot of properties end up looking exactly like the one above: a get and a set that poses no special restrictions on data access. Doesn't that technically mean that the property is redundant, since it works exactly the same as if we'd simply made _cookName public? Not exactly. Although the CookName property is not adding any new functionality to the class, it is making it more expandable, and that's another big feature of OOP. For example, just because we don't have any restrictions right now doesn't mean that we won't want to add a few later on. If we've been using a property all along, then all we need to do is add the required code to the "get" or "set" clause. Similarly, if we want to make the member read or write-only, and we've been using a property all along, we just have to remove the get or set. In both of these cases, however, you've got nowhere to go if you've been using a public member. The simple fact of the matter is that there's nothing a public member can do that a public property won't do better. Thus said, I would highly recommend that you limit your class's public interface to methods, constructors and properties. Leave members out of the public picture entirely. Okay, having said all that, you still might be thinking something along the lines of, "OK, so this encapsulation stuff is a good practice and all that, but why should I bother to go to all that work? The only person using this code is me; do I really need to hide stuff from myself?" The plainest answer is that you don't have to, but you certainly should. When your code can access and manipulate data anywhere it wants, in any way it chooses, you are opening up a huge host of potential bugs and maintainability issues down the road. And worst of all, those kind of bugs usually don't surface until the program is larger, and you've already invested a great deal of time and effort in the project. Now, if you're just making a "quick-and-dirty" 15-minute app that does some wimpy little task, will using public members instead of properties really cause problems? Probably not. But even "quick-and-dirty" applications have a funny habit of evolving into large-scale projects. It takes half a minute to turn a public member into a property, but it can take hours or even days to track down an intermittent bug that involves an unrestricted variable. Why not do things right the first time, instead of after bugs start appearing? The very fact that you can debug inside a "set" clause, but not within a public member declaration gives properties a massive advantage in tracking down problems. Besides, programming is a big task, requiring you to think of many things at once. The more details that you can hide from your view and not worry about, the better. That being said, typing all that no-brainer code can get very repetitive. My solution was to write a simple application that accepted a list of variable names and their datatypes, and then generated all the property/member declaration code for me. That's right: in case you've never considered the possibility before, don't forget that you can write programs that help you write programs, generating the boring parts for you! After all, code is just text, and it's child's play to create a text file. But I digress. Don't forget that you can make methods private, as well. If you have a public method that's 100 lines long, you can often break large sections of it off into smaller, private methods to modularize it more effectively. And of course, as long as your original public method stays put, the rest of your program will never know the difference. Long live encapsulation! Also, any member, method or property in a class that doesn't have "public" or "private" in front of it is assumed to be private by C#. Before we move on to this lesson's code sample, a brief word on the properties we discussed earlier: you don't have to use them. As mentioned at the outset, you can write "ordinary" methods that get or set a class's private members, to the same effect. In fact, Java and C++ do not support properties at all. Furthermore, it is argued that properties break the rules of OOP, in part because they masquerade as exposed public members. Whether you write properties or methods to access private data is up to you. They both serve exactly the same purpose, and have essentially the same capabilities (properties are a little more convenient, but methods can have parameters). For the record, I think that properties are pretty cool, and I use them all the time. The .NET framework is already saturated with properties, so I prefer to follow suite in my own programs (in all fairness, .NET uses get/set methods too, and the result is a little bit disorganized-looking when mixed with properties). Just be aware that there are veteran programmers out there who dislike properties, and they may challenge you if they see them in your code! In this lesson's code sample, I've made the members of the Pizza class private, and given them properties. The "Topping" property simply provides get/set access to _topping. The "Diameter" property provides get/set access to _diameter, but it will only change _diameter's value if you give it a positive value. I made the "CookName" property prefix the return value with "Chef", just to demonstrate why you might customize the "get" clause. I also made it read-only, since the person who cooked the pizza will always stay the same. These are prime examples of the decisions you'll make when you are writing a class. using System; namespace B_BetterClass { class Class1 { static readonly string ln = Environment.NewLine; [STAThread] static void Main(string[] args) { // Create a "default" pizza, and print its information Pizza pizza1 = new Pizza(); Console.WriteLine("PIZZA1 after construction:"); Console.WriteLine(" Topping = " + pizza1.Topping); Console.WriteLine(" Diameter = " + pizza1.Diameter); Console.WriteLine(" Radius = " + pizza1.GetRadius()); Console.WriteLine(" CookName = " + pizza1.CookName); // Change the pizza's information pizza1.Topping = "Pineapple"; pizza1.Diameter = 9.0; // Re-print its information Console.WriteLine(ln + "PIZZA1 after changing its information:"); Console.WriteLine(" Topping = " + pizza1.Topping); Console.WriteLine(" Diameter = " + pizza1.Diameter); Console.WriteLine(" Radius = " + pizza1.GetRadius()); Console.WriteLine(" CookName = " + pizza1.CookName); // Create a "custom" pizza, and print its information Pizza pizza2 = new Pizza("Pepperoni", 6.0, "Tony"); Console.WriteLine(ln + "PIZZA2 after construction:"); Console.WriteLine(" Topping = " + pizza2.Topping); Console.WriteLine(" Diameter = " + pizza2.Diameter); Console.WriteLine(" Radius = " + pizza2.GetRadius()); Console.WriteLine(" CookName = " + pizza2.CookName); PromptForExit(); } static void PromptForExit() { Console.Write(ln + "Program complete! Hit enter to exit..."); Console.ReadLine(); } } // A class for simulating pizzas! class Pizza { private string _topping; private double _diameter; private string _cookName; public string Topping { get{ return _topping; } set{ _topping = value; } } public double Diameter { get{ return _diameter; } set{ if( value >= 0 ) _diameter = value; } } public string CookName { get{ return "Chef " + _cookName; } } public Pizza() { _topping = ""; _diameter = 0.0; _cookName = "Anonymous"; } public Pizza(string topping, double diameter, string cookName) { _topping = topping; _diameter = diameter; _cookName = cookName; } public double GetRadius() { return _diameter / 2.0; } } } |
|