If I could pick only one thing that a senior-level .NET developer should know, it would be Object-Oriented Programming (OOP). True, OOP is not .NET specific, and indeed I think at this point a senior developer on virtually any platform should be familiar with OOP, it’s especially important here in the .NET world. Read on to find out why I think OOP is so important and to get a quick introduction and refresher on the four major tenets of OOP.
About this series: What does it mean to be a “senior” developer? This series will explore the principles, tools, techniques, patterns, and APIs that I expect any senior-level .NET developer to be familiar with. My hope is that this series will force me to fill in some holes I know I have in my toolset as well as to help others identify areas they need to improve on. Please let me know what you think!
What is Object-Oriented Programming, and Why Should I Care?
Object-Oriented Programming is paradigm of creating software programs through the use of data structures, called objects, that contain both data and behaviors. You should care about OOP because, as a .NET developer, you are working on an Object-Oriented platform. Aside from that, OOP techniques can help you manage the complexity inherit in building real-world applications, leading to solutions that are both easier to extend and easier to maintain. Correctly applied, OOP techniques will help you organize your applications in ways that reflect the true domain, making them easier to understand and therefore easier to maintain. OOP techniques will also help you avoid (but sadly not prevent) creating lengthy spaghetti code that is difficult to understand and extend. OOP is not The Silver Bullet, but it is certainly an improvement over the procedural approaches that dominated for the last two decades (and which are unfortunately still all too common even on object-oriented platforms such as .NET).
Sidenote: While there are indeed languages that run on the CLR that are not object-oriented, most (if not all) still provide some way to access objects and therefore support some object-oriented concepts. Besides, the vast majority of .NET developers are utilizing either C# or Visual Basic, both of which are object-oriented languages.
There’s a wealth of information available on the topic of OOP. Entire books have been written around them. While a proper treatment would take me years to cover at my current rate of blogging, I can at least tell you about the four core tenets of OOP. I’m far from being an authority on the topic, but hopefully these will at least help point you in the right direction.
Abstraction is, in a nutshell, taking some concept and boiling it down to a representation that’s relevant and useful for a particular application. This is where we take some entity/idea/thing from our application’s business domain and model it in our application. This model won’t match 1-to-1 the real-world concept, and that’s a good thing. Consider an application that manages inventory for a car dealership. For this application, is it really important to model that a car’s engine has pistons, that each piston has a crown, one or more compression rings, a skirt, a connecting rod, bearings, and bolts? Of course not. Modeling the domain at that level for the simple purpose of tracking which cars are on the lot would add unneeded, useless complexity. We may choose to only model a few attributes of an engine for such an application, such as the number of cylinders and the amount of horsepower it generates. This choice is abstraction. We’re simplifying the real-world concept into something more manageable for our application.
We may (and indeed often should) introduce multiple levels of abstractions in our applications. In .NET, these abstractions can take the form of interfaces or base classes from which we build more complex types. Abstractions at different levels of the application may omit details. For example, interfaces in .NET simply define what something looks like, not how it works.
Abstraction is an important idea. It helps us break down something complex into something simpler that we can work with. But how do we decide what parts of our model belong where in our abstractions? That’s where the next tenet comes in.
Encapsulation is my favorite tenet of OOP. It’s also the most misunderstood. I freely admit that it wasn’t until Derick Bailey was interviewing me for a job that I fully grasped the concept. If you ask 10 .NET developers to define encapsulation, I think 9 of them would probably tell you that encapsulation is data hiding: putting data behind properties with getters and setters. A couple of years ago, that would have been my answer as well. But this is wrong. Encapsulation is actually the hiding of both data and behavior. More completely, encapsulation is aggregating data as well as the behaviors that operate on that data behind the operations exposed by an object. Just slapping properties around private fields is only half the battle. You should also strive to pull in behaviors that affect the data. A well-encapsulated class, by this definition, is a cohesive one, and cohesion is one of those qualities that typical leads to maintainable software applications.
Sidenote: If you read that and thought “BUT WHAT ABOUT THE SINGLE RESPONSIBILITY PRINCIPLE?!?!”, good for you. I will address exactly that point in a future post in this series.
Inheritance is interesting. It is a simple concept, but a very powerful one. It is also my least favorite of the OOP tenets. In .NET, inheritance allows objects to inherit both behavior and data from another object. For example, the MemoryStream type inherits from the Stream type and therefore contains a superset of Stream’s data and operations. Inheriting from an object establishes an “is a” relationship between the objects. For example, MemoryStream is a Stream: it has all the members that Stream has (including the non-public ones), but adds additional members of its own. This concept will be explored further when we talk about the final tenet.
You may be wondering why I said that inheritance is my least-favorite tenet. That’s because it’s quite easy to abuse and can quickly lead to software that is difficult to extend and to maintain. The use of inheritance should always be questioned. It certainly has its uses, but often times a compositional approach will yield a better, more maintainable design than one based on inheritance. Use it with caution.
Sidenote: In .NET, inheritance is quite different from interface implementation. The two are both semantically and syntactically similar, but they are not the same. By implementing an interface, a type is agreeing to expose some set of data and operations, but it is fully responsible for the actual implementation of those members. With inheritance, the deriving type acquires any non-abstract data and behavior from the base type. It may choose to reimplement virtual members from the base type, but it does not have to.
By way of inheritance we can establish “is a” relationships between our objects. Through the final OOP tenet, polymorphism, we can actually treat one object as having many forms. A MemoryStream can be treated (obviously) as a MemoryStream, but it can also be treated as its base type, Stream. Why is this important? Polymorphism introduces a great deal of flexibility and extensibility to our applications. Consider the XmlSerializer class. Without polymorphism, the XmlSerializer would have to explicitly implement support for every single type of object that could be serialized, ever, which is not possible since the BCL authors cannot foresee which objects you will need in your particular domain. Without polymorphism, Stream would also have to explicitly implement support for every target stream type that an object can be serialized to, such as MemoryStream, FileStream, and so-on. But instead, because .NET is an object-oriented platform that supports polymorphism, the XmlSerializer class can simply operate on generalizations instead. It can serialize an instance to any object that inherits from Stream, and it is capable of serializing any type because it operates on the Object class instead of a more specific type.
Sidenote: Because all classes in .NET inherit from the Object class, you can treat any object as an instance of Object. You can actually treat structs as Objects as well thanks to the magic of boxing, which is performed automatically by the CLR.
There’s another aspect of polymorphism that’s also important: method overloading. An object may expose multiple methods with the same name that differ only in the types of their parameters. For example, the StreamWriter class defines numerous versions of the Write method, each accepting a different type as input. When you invoke the Write method, the compiler will automatically select the most appropriate version of the Write method to invoke. All this is thanks to polymorphism.
Where do you stand?
And there you have it: the four main tenets of Object-Oriented Programming. If you had a general idea of the four main tenets of OOP and their definitions, congratulations, I hope you at least found this article somewhat useful as a refresher. However, if these ideas were totally foreign to you, it’s time to play catch-up. But don’t fret, there’s a wealth of information about OOP freely available on the web that can help get you up to speed. Learning about OOP and, more importantly, learning how to apply OOP is a very worth-while investment that will help you create better software.
Up next in Part 2, we’ll look at taking OOP to the next level with everyone’s favorite principles: SOLID.
Final sidenote: I’ve long been considering doing a series of screencasts to cover OOP in depth using C#. If you think you would find such a series useful, please let me know.