Lesson 1 - Introduction to object-oriented programming in C# .NET
Welcome to the first lesson of the object-oriented C# .NET programming course. We finished the C# .NET basic constructs course last time with the article about Mathematical functions in C# .NET. In this course, you'll learn to program in an object-oriented way and will also develop an object-oriented way of thinking. It's a bit different than anything we've done until now. For starters, we will no longer treat programs like several lines of code that the interpreter executes one by one.
The invention of object-oriented programming, OOP hereinafter, was no coincidence, but the result of a development which led to its creation. It's a modern software development methodology supported by most programming languages. A common mistake is that people think that OOP should only be used for certain kinds of programs and that for other cases it would be needlessly complicated; however, the opposite has proven to be true. OOP is a philosophy. It's a new way of looking at a program and the communication between its parts. We should always use it whenever we create a simple utility or a complex database system. The OOP is not just a technology or a recommended program structure, it's mainly a new way of thinking. A new perspective from which we could analyze problems and a new era in software development.
As first, we'll look quickly into the history of how people programmed before OOP and what specific problems OOP resolves. It's important for us to fully understand why the OOP had to be created.
The evolution of methodologies
There is a big difference between programming nowadays and programming 40 years ago. The first computers didn't have great performance and their software was quite simple. The evolution of hardware is so fast that the number of transistors in microprocessors doubles every year (Moore's Law). Unfortunately, people aren't able to evolve as quickly as hardware does. Faster computers require more and more sophisticated and complex software (people want more and more from their computers). So much that at one point, it was found out that about 90% of all software doesn't meet deadlines, requires additional costs or isn't finished at all. Developers started looking for new ways to write programs. Several new approaches took turns. More precisely, paradigms (ways of thinking). Listed as follows:
1. Machine code
The program was just a set of instructions where we weren't able to name variables or enter mathematical expressions. The source code was obviously specific for the then current hardware (processor). This paradigm was replaced soon after it was established.
2. The unstructured paradigm
The unstructured approach is similar to the assembly languages, it's a set of instructions which are executed line by line. The source code wasn't dependent on the hardware and was human readable. This approach enabled the creation of more complex programs for a while. There were still many pitfalls: the only way to repeat something or to branch off a code was the GOTO statement. GOTO allowed to "jump" to different locations within the program. Locations were previously specified by a line number in the source code, which is obviously impractical. When we insert a new line of code somewhere, the numbers no longer match and the code is broken. Later, it was made possible to define what they then called "labels". This approach served as a way of simulating loops. This method of writing programs is of course very confusing and soon failed to be sufficient for developing complex programs.
Consider that the huge adoption of personal computers over the past few decades caused the growth of software demand and, naturally, a demand of more people who make programs (programmers). Certainly, there are people who can write bulletproof programs even in ASM or other low-level languages, but how many are there? How much does this superhuman job cost? It's necessary to write programs in a way that even less experienced programmers could write high-quality programs and not need to go through 5 years of experience to code a simple utility app.
3. The structured programming
Structured programming is the first paradigm that lasted for a longer time and was quite sufficient for the development of new programs. They would mostly program using loops and branching. Conceptually, we are in the structured programming era based on what we have learned from the first course.
The program would be decomposed into functions, methods, that we haven't discussed yet since C#, which is an object-oriented language, doesn't even allow us to declare them. There is a way to do it anyway, but I'd rather skip this intermediate step and get right into OOP. In structured programming, we meet a functional decomposition principle. A problem is decomposed into several subproblems and each subproblem is then solved with some parametrized function. The disadvantage to it is that a function can only do one thing and when we want a different behavior, we have to write a new one. There is no way to reuse old code and modify it. We need to write it again and again - this creates unnecessary, potentially costly, errors. This disadvantage can be partially worked around by using parametrized functions or using global variables. However, such universal functions usually require a lot of parameters to pass and are hard to use and maintain.
With global data, there's another pitfall. Functions can access the data of other functions. This is the beginning of the end, we can't guarantee that global data isn't being overwritten somewhere between functions. It leads to uncontrollable problems. The entire program will consist of unencapsulated code blocks and can hardly be maintained. Any modification increases the complexity of the program, and then the program will necessarily come to a situation where the cost of adding new features will overbalance the value added by these features. Languages using this approach are, for example, the C language and Pascal.
Between the structured programming and the object-oriented programming, there was one more intermediate approach called modular programming. It involved the encapsulation of specific functionality into modules. Regardless, there was no way to modify and reuse already written code.
As I mentioned at the beginning of the article, it's sometimes said that simple programs mustn't be written in an object-oriented way, but structural, which isn't true. If we program in a structural fashion, we will end up making a blob that will be almost unreadable by most people. Then again, it would come to a point where the program wouldn't even be upgradable and we'd either have to throw it away or rewrite it using the OOP.
The non-object-oriented methods of writing code are called "spaghetti code" because of their lack of clarity (everything is tangled together like spaghetti).
The object-oriented approach
OOP is a philosophy and a way of thinking, designing and implementing solutions that focuses on reusability. This approach is inspired by the industrial revolution - the invention of basic components. For example, when we build our house, we don't burn our own bricks and forge nails, we order them.
Making a "component program" is smarter and cheaper. Components don't fail, they're tested and maintained. If there's a problem, it is most likely in the code you have written in one specific location. We're motivated to write clear code since it can be used by others or by ourselves in other projects. Let's face it, humans are lazy by nature and if we thought that our code wouldn't ever be reused, we simply wouldn't write it good enough ).
Of course, we'll use the knowledge we have gained until now. The main difference is that now, our code will be structured differently into multiple communicating objects.
How the OOP works
It tries to simulate reality as we're used to see it. We can say that we abandon the idea how the program is seen by the computer (machine) and write it from the programmer's (human's) point of view. As we had replaced the assembler with human-readable mathematical notations, now we're going even further and replace those, too. OOP is, therefore, a certain level of abstraction above the program. This has significant advantages because it's more natural and readable for us.
The basic component is object which corresponds with some object from the real world, e.g. a human object or a database object).
Objects have attributes and methods.
Object attributes are properties or data that it stores,
e.g. the human's
age. For the database object
it could be the
password or whatever the objects requires to work.
Attributes are just like ordinary variables with which we've worked a hundred
times. Sometimes they're called the "object's internal state".
The term "property" was reserved by Microsoft for some specific usage and the same is with the term "attribute". Object variables are called "fields" in C# .NET. However, other languages often call them properties (PHP) or attributes (Java) and I might call them attributes as well in further texts.
Methods are abilities that the object can perform. Human, for example, could have the methods GoToWork(), Greet() or Blink(). For the database, it could be AddEntry() or Search(). Methods can have parameters and can also return values. You've actually used them before without even knowing! Remember the Split() method on the string object? A string is actually an object that represents text. You can see that we can easily imagine that we're dealing with text. We can modify it, ask it to return its length, combine it with other strings, etc.. It contains methods that a text can perform, copying, deleting, splitting, and also has fields, e.g. Length which contains its length.
In older languages, methods didn't belong to objects but were loosely placed in modules (units). We could call them by typing Split(text) instead of text.Split(). The disadvantage to that was, of course, that the Split() method didn't belong to anything. There was no way to list what string could do and the code was messy. Additionally, we couldn't have two methods with the same name. In OOP we can have both user.Remove() and article.Remove(). It's very clear and simple. In a structured program we'd have to write: remove_user(user) and remove_article(article). We'd have to create thousands of silly, unnecessary methods. If you're thinking, hey, isn't that what the PHP language does? You are absolutely right. PHP is terrible when it comes to things like this, and for that same reason, its design is considered old. It became fully object-oriented later, but its foundations will probably never change. C# is a modern language and the .NET framework is strongly built on objects.
In this article, we're going to explain the basics of how to create objects and how to encapsulate their internal logic. Other OOP features, mainly inheritance, will be explained in the following lessons, to not overload your brain today
We have already encountered the term "class". We understood it as a set of commands. A class, however, allows us to do so much more. A class is a pattern that we use to create objects. It defines their properties and abilities.
An object created according to a class is called an
instance. Instances have the same interface as
the class according to which they were created, but they mutually differ in
their internal data (fields). For example, let's consider a
class and creating the
jack instances from
it. Both instances will have the same fields as the class (e.g.
age) and methods (
greet()), but the values in them will be different. For example,
the first instance will have the value
"Carl" in the name attribute
age, the second will have the values
The communication between objects is performed by messaging which makes the
syntax clear. The message usually looks like this:
recipient.methodName(parameters). For example,
carl.greet(neighbor) could cause the
carl instance to
OOP is based upon three core concepts:
Let's look into the first of them:
Encapsulation allows us to hide some methods and fields so they can remain available only from inside of the class. The object can be thought of as a black box that provides an interface through which we can pass instructions/data to be processed by it.
We don't know how the object works internally, but we know how it behaves on the outside and how we should use it. We can't cause errors because we are only allowed to use it in a way its creator meant it to be used.
An example might be the
Human class having a
birthDate field and a few more based on its value:
fullAged and age. If someone changed birthDate from
outside the object, the values in fullAged and age variables
could become invalid. Which means that the internal state of the object would be
inconsistent. This could happen to us in structured programming. In OOP,
however, we encapsulate the object and mark the birthDate attribute as
private so it won't be visible from the outside. To the outside, we'd provide a
ChangeBirthDate() method which would store a new birth date into a
birthDate variable and also perform a necessary age re-calculation and
full-age re-valuation. Using the object would be always safe and the application
Encapsulation forces programmers to use objects only in the right way. The class interface is divided into publicly accessible (public) and internal (private) members.
In the next lesson, First object-oriented application in C# - Hello object world, we'll create our first object-oriented program.