Lesson 1 - Introduction to object-oriented programming in Kotlin
Welcome to the first lesson of the object-oriented Kotlin programming course. We finished the Kotlin basic constructs course last time with the article about Mathematical functions in Kotlin. 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, we 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 the development of 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 the 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 achieve the same results.
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. 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 an old code and modify it. We need to write it again and again - it 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 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 focus 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 the 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 wouldn't write it in the first place ).
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.
Attributes
Object attributes are properties or data that it stores,
e.g. the human's name
and 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".
Object variables are called properties in Kotlin. However, other languages often call them fields (Java) or attributes and I might call them attributes as well in further texts.
Methods
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 properties, 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. Kotlin is a modern language and its
libraries are strongly built on objects.
In today's lesson, 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
Class
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 (properties). For example, let's consider a
Human
class and creating the Carl
and
Jack
instances from it. Both instances will have the same
properties as the class (e.g. name
and age
) and
methods (goToWork()
and greet()
), but the values in
them will be different. For example, the first instance will have the value
Carl
in the name attribute and 22
in age
,
the second will have the values Jack
and 45
.
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
greet the neighbor
instance.
OOP is based upon three core concepts:
- encapsulation
- inheritance
- polymorphism
Let's look into the first of them:
Encapsulation
Encapsulation allows us to hide some methods and properties 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 in a way its creator meant it to be used.
An example might be the Human
class having a
birthDate
property 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 stable.
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 app in Kotlin - Hello object world, we'll create our first object-oriented program.