In previous lessons, the topic of "classes" and "objects" has been mentioned, but not detailed. The reason for this is because this topic has to do with Object-Oriented Programming (or “OOP”), which is a shift in thinking that takes time to really understand its significance. OOP takes the basic concepts you already understand (variables, methods, scope) and separates them into a more conceptual framework; grouping one set of variables and methods in one class and grouping others in another, for example. And from those classes you can build “instances” that are bound up in a special kind of variable called an object. Those objects then work together as communities to solve problems. Objects (and therefore, classes) have a host of special properties that we have yet to see. They can inherit from other classes in order to add functionality and create a more flexible software system, as well as reducing the amount of dependency in your system. Dependency becomes a problem when one piece of code breaks, causing another piece of code that is dependent on it to break as well. Classes are an important step towards reducing dependency and allowing you to enforce the “separation of concerns” principle we spoke of earlier.
By keeping code as separate as possible, using various object-oriented techniques, you are de-coupling dependencies within your code. Coupling is most glaring when you are debugging and start to notice how “whacking” a bug in one part of your code causes another bug to “pop up” elsewhere. And if it only happens under certain runtime conditions, you have the makings for a real coding nightmare.
To distinguish OOP from non-OOP you could contrast it with the style of programming we have been mostly doing up to this point which is based on a procedural style. This is an old style of programming where you work in terms of data in/data out. This style of programming typically uses carefully named variables and methods to represent data points and processes. These data points might be loosely coupled together - perhaps by some kind of naming convention - but the main distinction is that we haven’t been thinking of the data as a representative piece of a larger architecture. And beyond the problem-solving logic, we haven’t considered maintainability or how change introduced within the system will affect the existing code, such as new requirements or functionality introduced later on. We also haven’t really worried about reusability of code we've already written for use in another project.
In contrast to the procedural style of programming detailed above, OOP prioritizes interaction of objects throughout their creation. In other words, when we think in terms of objects that need to talk to other objects in order to accomplish a solution as a piece within a broader problem, we are putting a priority on all of these elements that procedural programming doesn’t concern itself with. This added complexity often leaves beginners wondering if this higher layer of concern – beyond just solving the problem – is worthwhile for the average programmer, especially considering that most beginning applications are actually quite simple. What you will come to learn is that the conceptual nature of OOP eventually allows you to create more complex applications without the burden of confronting their complexity upfront. OOP ultimately allows you to concern yourself less with mundane issues, instead abstracting them away into nice little bundles that you rarely have to look at and understand in full. It will allow you to focus better on individual problems that need solving, within a broader puzzle, and in the software development world there is always a broader puzzle.
The subject of Object-Oriented-Programming is both relatively easy to pick up on and extremely deep if you want to continue pursuing it. It can take time to fully understand, and appreciate, how much it offers. Don't feel bad if you don't really understand it all the first time through. The best way to look at learning OOP is its return on time investment: the relatively modest amount of extra time you put in now to learn its secrets pales in comparison to the time you gain back when using it to build robust and extensible applications. Just keep working through the examples over the next few lessons and you will eventually start seeing the world of code around you as made up of objects.
Virtually everything in C# - including the part of the .NET Framework that deals with ASP.NET functionality – is either a class, or part of a class. In all of the previous lessons, we have been using ASP.NET Web Forms with a Default.aspx file. What you may not have realized is we were just creating a class definition – a set of methods and properties for a class - in that file.
What you haven’t yet seen is that the ASP.NET Runtime will create an instance of this Default class whenever the user requests it over the internet. Once it creates an instance of that class, it will begin calling methods or setting/retrieving properties of that class. Part of that process is being determined by what you write in the code for this Default class. In case you are wondering, the ultimate responsibility for this class is to generate HTML that represents a webpage called “default.”
The purpose of this lesson and the next few lessons is simply to understand classes and their ubiquity when working with C#.
For this lesson, suppose that you wanted to create an application that works with cars in some way. You may have a car lot with an inventory of all the cars that are for sale, or you may want to keep related information about a single car in one container. That container can be created using a class, and allows you to keep all of this related information that represents a car within the class.
Begin by creating a new ASP.NET project with a single Server Control resultLabel:
In Default.aspx.cs, create a new class called Car alongside the Default class, keeping both classes within the broader “CS-ASP_036” namespace:
What we’re doing here is naming a code-block (“Car”) similar to how methods are named blocks of code that perform some particular process, we can later reference classes as we do with any variable or method, setting or retrieving their properties. Within this code block, we have two basic “members” that immediately belong to it and those members are typically variables (properties) and methods. Class-level properties are much like any variable we have worked with thus far, but are usually meant to describe attributes common to that object’s class. For example, a car is typically defined by properties such as:
Class-level methods also define something about the class, and that is what the class can do (tasks, processes, behaviors, actions, and so on). A car can among other things do things such as:
In this sense, classes often are created to model - insofar as it’s important to the functioning of our application - the behavior and physical properties of their real-world counterparts. Let’s start off by adding the following properties to this class:
Notice that these are just like ordinary variables – with a type, and an identifier – except for the accessibility prefix (in this case, public) as well as the get; and set; postfix, which can be seen as the read/write attributes. You can access the code snippet for this by typing in “prop” and hitting the tab key twice:
Now, to create an instance or actual object of our car in code we can do so by referencing the class as we do any variable type:
By creating an instance of the class we’ve defined, you can look at it like the class is a blueprint, while the object instance is the actual object itself from that blueprint. And just like a blueprint can be reused to create several objects in the real world, you can do so as well with blueprints and objects in code:
Each instance can now have its own unique values for its properties that you can set as you would any variable:
Each individual value is stored in sections of memory, while another section of memory hold onto myNewCar as a reference to these memory sections and their values:
Now that you have an instance of a Car in memory, with its included properties, you can get those values in memory (again, just like any ordinary variable):
Since Car is a type all its own, you can use it and reference it anywhere. For instance, as an input parameter in a method:
Notice here that the variable identifier (name) is the same as the type, except that has a different casing. This is perfectly acceptable – even if they share the exact same casing – as the compiler can distinguish between the object’s name and its type. Also, notice that the input parameter is not used in this method, however, you would want to make use of the input parameter in a real-world example. But for the sake of brevity imagine that this method is doing something useful with the information we have about the car that we supply as an input argument at the method call:
And now you can add the value returned into myMarketValueofCar to the resultLabel:
To make this a bit more interesting, let’s actually show how you would go about referencing the input parameter within the method body for determineMarketValue() to modify the result depending on the age of the car:
If you think back to what was said in previous lessons about “Separation of Concerns” it seems that the determineMarketValue() method violates this principle somewhat. In specific, it’s a method that calculates the value of a car, so putting it in the Default class (which is about rendering web page data) is a bit off the mark. Let’s transplant this method to the Car class, where it conceptually fits in a bit better, but with some modifications first:
Change the accessibility to “public,” so that outside classes can call this method.
Remove the input parameter as it is no longer required.
Directly reference the Year property, now that the method can “reach-out” of its scope and reference it at the class-level.
Now that the method belongs to the Car class, we will have to reference it through the Car instance in outside classes, just as we would the Car properties:
As you’ve seen, you can use the dot accessor (period) to peer into a class containment, in order to get/set properties or call methods. In later lessons you will see how class properties can, themselves, be instances of other classes. This creates multiple levels of containment that can be accessed by using multiple dot accessors; peering through one containment level after another.
Lesson 36 - Introduction to Classes and Objects