Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.
Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.
Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 2.4: Inside a Class

Recap
In this chapter, we’ll explore the program code that defines a class. In doing so, we combine what you know about defining a singleton object (Chapter 2.2) with what you just learned about classes and their relationship to objects (Chapter 2.3). Let’s begin with a quick review of key facts that you’ll need.
Things you already know about classes and objects:
Classes define types of data that a program operates on. A class represents a generic concept that can be instantiated. For example, a class may represent the general concept of student.
Instantiating a class means creating an object that is an instance of that class. In other words, to instantiate a class is to create an object of the type that the class defines. Such an object is a specific case of the general concept. For example, the object may represent an individual student.
When you instantiate a class to create an object, you often pass in one or more constructor parameters. The class may use these parameters to initialize attributes specific to the new object, such as a student’s name or student ID.
Each object of the same class has its own attributes (e.g., student objects may have different names). However, these attributes share the same data type (e.g., each student’s name is a string).
Objects of the same type have the same methods. For example, if the student class defines an enrollment method, all student instances will have that method.
Things you already know about singleton objects:
You define a singleton object without separately defining a class for it.
Within a singleton object’s program code, you can define variables that store the object’s attributes. You can also implement methods on the object.
Within a method implementation, the word
this
refers to the object itself, so you can use expressions likethis.someVariable
to access the object’s own variables.
Class Definition vs. Singleton Definition
In Chapter 2.2, we implemented and used a singleton object that represents an individual
employee. In contrast, in Chapter 2.3, we used a class Employee
that represents
employees more generally and was therefore more useful than the original singleton.
The code of the singleton object from Chapter 2.2 is reproduced below. Below it, you’ll
find a new piece of code that defines the Employee
class. Comparing the two, we see
that they are in many respects identical. There are a few key differences, however, as
highlighted by the green boxes between the two code fragments.
object employee:
var name = "Edelweiss Fume"
val yearOfBirth = 1965
var monthlySalary = 5000.0
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) =
this.monthlySalary = this.monthlySalary * multiplier
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
end employee
After the class name, we list the constructor parameters that need to be provided when instantiating the class. A singleton object doesn’t have this bit.
We can use the parameter variables to initialize the object.
Here, we store the parameter values in the attributes of the
new Employee
instance. (More on that below.)
The methods of our singleton and class are exactly the same!
class Employee(nameParameter: String, yearParameter: Int, salaryParameter: Double):
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) =
this.monthlySalary = this.monthlySalary * multiplier
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
end Employee
Now, let’s dig into how the class works.
Instance Variables
How does a class definition determine what happens when we create a new instance?
The answer is illustrated in the following animation. The animation also introduces an important new concept: the instance variable (ilmentymämuuttuja).
You can also consider the instantiation process in terms of the form metaphor that we used earlier. Here’s an update to the illustration from Chapter 2.3:
Instantiation and a class’s program code (a clarification)
As shown in the animation above, the commands inside a class definition are executed when a new instance is created. For example, the instance variables of a new employee object are assigned (initial) values at that point.
Note that this applies only to the commands that appear directly within the class definition — not the commands located inside methods. A method body runs only when we instruct the computer to execute it by calling the method.
For example, when we create an instance of the Employee
class, none of the methods
ageInYear
, monthlyCost
, raiseSalary
, or description
are executed, even though
these methods are defined within the class’s program code. The class’s user can call
those methods after creating an instance, as we’ve done in earlier examples.
Check your understanding: constructor parameters and instance variables
Constructor parameters vs. instance variables
A method’s parameter variables exist — and their values are held in memory — only while the method is running. Similarly, constructor parameters exist only while the new instance is being created. They are stored in a frame on the call stack, and that memory is released for other use as soon as the object has been initialized.
Instance variables, on the other hand, are members of an object. Their values remain in memory after the object’s been created and persist between method calls.
An instruction such as val name = nameParameter
essentially takes a value received as a
parameter and stores it in a place where it can be accessed later.
In the Employee
and Pet
classes, the only thing we did with the constructor parameters
was to copy their values into instance variables. You might reasonably wonder why we even
need separate variables to store the constructor parameters, if those parameters are there
just to be copied into another set of variables.
It does make sense to be able to define constructor parameters separately from instance variables, because copying their values isn’t the only thing we can do with the parameters; you’ll see examples of that later. But since this kind of copying is so common, it should be possible to express that more succinctly. And indeed Scala does provide a way to tighten our code:
A Convenient Way to Write a Class
Below are two versions of the class Employee
. The first version you saw already; the
second is more compact. Both accomplish the same thing.
class Employee(nameParameter: String, yearParameter: Int, salaryParameter: Double):
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
// methods not shown
end Employee
We can write var
or val
at the top of the class definition,
as shown below. This means two things: the class’s user must
pass in these three things as constructor parameters and
the class automatically stores the received values in the new
object’s instance variables.
In the more verbose version above, we had to give distinct names to the parameter variables separately. Those names don’t appear at all in the compact version of the class. In the shorter code, the constructor parameters have the same names as the instance variables; their values are copied into the instance variables without an explicit command to do so.
Our example class also has a fourth instance variable, which doesn’t get its value from a constructor parameter. Its definition is identical in both versions.
class Employee(var name: String, val yearOfBirth: Int, var monthlySalary: Double):
var workingTime = 1.0
// methods not shown
end Employee
If you want, you can view the following animation, which uses the compact version of
Employee
.
The compact version is shorter and, perhaps, easier to read. In any case, the compact style is extremely common in Scala programs and you should get accustomed to it. In O1, too, we’ll adopt this compact style of writing classes.
Methods on Instances
Methods operate on the instances of classes in the same way as they operate on singleton
objects. As you saw in Chapter 2.2, we can use the word this
to refer to the object
whose method has been called. A class’s instance, too, can access its own instance
variables through expressions like this.variableName
.
If this seems perfectly clear to you, feel free to skip the following animation; it has no other new content. Otherwise, watch the animation.
Assignment: A Compact Rectangle
Assume we have access to a class Rectangle
that behaves as follows:
val test = Rectangle(10.0, 15.5)test: o1.Rectangle = o1.Rectangle@152a308 test.side1res0: Double = 10.0 test.side2res1: Double = 15.5 test.areares2: Double = 155.0
As shown, we can ask a Rectangle
object for the lengths of its sides as well as its
area.
Here’s one way to define a class that works as shown above:
class Rectangle(givenSideLength: Double, anotherGivenSideLength: Double):
val side1 = givenSideLength
val side2 = anotherGivenSideLength
def area = this.side1 * this.side2
// Etc. You may write additional methods here.
end Rectangle
This definition is unnecessarily wordy. Write a more compact class definition by defining
the instance variables in the class header, as demonstrated with class Employee
above.
Write your solution in Rectangle.scala
in the IntroOOP module. The given file contains
the above implementation for you to modify.
Use the REPL to confirm that your new implementation works as illustrated.
A+ presents the exercise submission form here.
Mini-Assignment: Describing a Rectangle
Assignment: An Account Class
Further practice: a Pic
-returning method
As noted, the class Rectangle
represents certain mathematical attributes
of rectangles. If we want to, it’s possible to extend the class with
methods that operate on rectangles as graphical elements.
Add a method makePic
in class Rectangle
. The method should:
take in a single parameter of type
Color
; andreturn an image (
Pic
) of a rectangle whose width is equal toside1
, whose height is equal toside2
, and whose color is determined bymakePic
’s parameter.
A+ presents the exercise submission form here.
In the practice task just above, we considered the drawing color to be an additional piece of information that’s passed to the rectangle object whenever we wish it to produce an illustration of itself.
Alternatively, we may choose to represent color as an inherent attribute of each rectangle.
Stay in Rectangle.scala
and add another class there, ColoredRectangle
.
As a starting point, you can copy and paste the definition of Rectangle
and rename it. Then make the following changes to the new class:
Add a third constructor parameter, of type
Color
.See to it that the rectangle’s color is accessible through an instance variable called
color
.Edit
makePic
so that it takes no parameters and instead uses the rectangle object’s color attribute as it produces an image of the rectangle.
A+ presents the exercise submission form here.
Which of the two classes is better? As far as our toy example is concerned, there is little difference, and in any case the answer depends on what we might wish to use the class for.
Here is a more interesting general question that you may want to consider: if you have a class that represents an abstract concept (such as rectangle), should its definition include anything that pertains to the program’s outward appearance (such as colors and graphics)? We’ll return to that question later on.
A Monster Example
Let’s define a class that could represent monsters in a simple, imaginary game.
Monsters have a kind (e.g., "orc", "vampire") and a health score. We plan to use our monster class as illustrated in the example below.
val creature = Monster("dragon", 200)creature: Monster = Monster@597f1753 creature.kindres3: String = dragon creature.healthMaxres4: Int = 200 creature.healthNowres5: Int = 200 creature.descriptionres6: String = dragon (200/200)
We create an instance of the class, a monster object. The constructor parameters specify the kind of this monster and its full-strength health.
We can ask any monster object to tell us its kind as well as its maximum health and current health. A monster starts at full health, so the two numbers are identical for now.
We can also ask the monster for a string that describes it. This description includes information about the monster’s current and maximum health.
A monster’s state changes when it suffers damage (having been attacked by a hero or something):
creature.sufferDamage(30)creature.healthNowres7: Int = 170 creature.sufferDamage(40)creature.healthNowres8: Int = 130 creature.descriptionres9: String = dragon (130/200)
We pass a parameter to the sufferDamage
method to indicate how
many health points the monster loses. This effectful method only
adjusts the monster’s state and does not return anything.
We can observe the change by asking the monster for its updated health or description.
Now let’s see about implementing such a class:
The term “constructor”
This chapter has said plenty about constructor parameters. That term, which refers to the parameters used when instantiating a class, comes from the concept of a constructor (konstruktori).
A constructor is a subprogram that’s executed as soon as an object is created; its purpose is to initialize the object. Commands that assign values to instance variables are common in a constructor.
In Scala, the commands we write directly into a class definition — outside of its methods — serve as a constructor for the class. In many other programming languages, constructors are definied separately within a class, much like methods.
Assignment: Odds (Part 1 of 9)
There are different ways to express the likelihood, or odds, of an event happening.
For example, to describe the chance of rolling a six on a six-sided die, we might say that “the odds are one out of six”. Another way to phrase this is “5/1”, which means that there are five ways to not roll a six and one way to roll it. In other words, it’s five times as likely to not roll a six as it is to roll it. The event’s likelihood may also be expressed as the percentage 16.67%, which can be alternatively written as 0.1667.
Let’s create a class for expressing the odds of events in different formats, and, eventually, computing the probabilities of combinations of events. In this chapter, we’ll begin our work on this class; in future chapters, we’ll return to improve on what we accomplish here.
Introduction: what we want from our Odds
class
The basic idea is that one instance of the Odds
class stands for the likelihood of
a single event, such as rolling a six or a bookmaker’s estimated odds of Norway winning
the next Eurovision Song Contest. We plan to create Odds
objects like this:
val rollingSix = Odds(5, 1)rollingSix: Odds = o1.odds.Odds@1c60524
Now the variable rollingSix
stores a reference to an object of type Odds
. We passed
two constructor parameters: first the number of non-occurrences (5), then the number of
occurrences (1). Together, these two numbers specify the event’s odds.
We can then ask the object to describe these odds in a variety of formats. Here’s
the probability
of rolling a six (i.e., the value of 1 ∕ (5 + 1)), as a Double
:
rollingSix.probabilityres10: Double = 0.16666666666666666
The fractional
method describes the odds as a ratio of non-occurrences to occurrences:
rollingSix.fractionalres11: String = 5/1
As shown above, the method returns a String
that connects the two constructor parameters
with a slash.
The decimal
method describes the odds in “one-in-how-many” terms. That is, it returns
the reciprocal of what probability
returns. Our example event has a one-in-six chance
of happening:
rollingSix.decimalres12: Double = 6.0
Consider another example. Many international betting agencies report odds in fractional
format (as returned by fractional
). We can use an Odds
object to represent the odds
offer on a bet.
For example, suppose that odds of 5/2 are on offer for the event of Norway winning
Eurovision. We’ll model this as an Odds
object:
val norwayWin = Odds(5, 2)norwayWin: Odds = o1.odds.Odds@1e75d66 norwayWin.probabilityres13: Double = 0.2857142857142857 norwayWin.fractionalres14: String = 5/2 norwayWin.decimalres15: Double = 3.5
In a betting context, decimal
’s return value tells you how much a successful bet
multiplies the bettor’s investment. For example, with 5/2 odds, the lucky bettor
receives 3.5 times what they bet: their money back and 2.5 times that much extra.
With that in mind, let’s sketch out one more method for our class. In this example, we
compute the winnings
from a successful 20-unit bet on Norway:
norwayWin.winnings(20.0)res16: Double = 70.0
(The result is the bet of 20 multiplied by a factor of 3.5, which is the factor that
was returned by decimal
.)
Finally, here’s one more example. It demonstrates that it’s perfectly possible for the first constructor parameter to be smaller than the second, if the event is likely:
val threeOutOfFiveChance = Odds(2, 3)threeOutOfFiveChance: Odds = o1.odds.Odds@dfdd0c threeOutOfFiveChance.probabilityres17: Double = 0.6 threeOutOfFiveChance.fractionalres18: String = 2/3 threeOutOfFiveChance.decimalres19: Double = 1.6666666666666667 threeOutOfFiveChance.winnings(123.45)res20: Double = 205.75
Your assigment
Fetch the Odds module and locate the definition of the Odds
class therein. (For now,
pay no mind to the module’s other contents.)
You’ll find the class lacking in functionality: probability
is there, but fractional
,
decimal
, and winnings
are missing. Implement those methods so that they work as
illustrated above.
Instructions and hints
Two instance variables have already been defined for you:
wont
andwill
. They receive their values directly from constructor parameters (in the manner described above at A Convenient Way to Write a Class).You can use these instance variables in the methods you write.
You won’t need other instance variables.
There’s no need to change
probability
; it works. But you can take a look at how it works.As long as you understand what each method is supposed to accomplish, implementing the methods with a combination of basic arithmetic operations shouldn’t take a lot of code.
Use the REPL to test that your methods work as specified.
Make sure to select the Odds module as you launch the REPL.
Remember to reset the REPL after making changes.
Submit your program only when you’re satisfied with how it works.
When you implement
fractional
, don’t try to reduce the fraction. For example, if the constructor parameters are 6 and 2, return6/2
, not3/1
.If you run into trouble constructing the string, take another look at the
description
method in the rectangle assignment.
Can you implement
decimal
andwinnings
by calling another method of the same class in the method body? (This isn’t required, but it is convenient.)
A+ presents the exercise submission form here.
Should I Have a Method or a Variable?
A couple of excellent questions from students:
When should I write a def
and when a val
? I’m not quite
grasping it.
In the Odds task, why are probability
, fractional
, and decimal
supposed to be methods? Since the methods don’t take any parameters,
wouldn’t this program work just as well if we turned them into
instance variables? I mean, it does work, I tried it. So why have
them as methods?
Consider, for instance, the outline of Odds
below. We could replace
the first three def
s with val
s, and the code would still work.
class Odds(…):
def probability = …
def fractional = …
def decimal = …
def winnings(bet: Double) = …
end Odds
If probability
, fractional
, and decimal
are def
ined as methods like so, each
of those methods’ program code is run only when — or if — that method is actually
called on an Odds
object. On the other hand, the computations written into the method
body will be carried out each time the method is called, which means that calling the
same method many times will make the computer compute the same value again and again.
Had we used val
instead, our Odds
object would have had three additional instance
variables. Giving initial values to those instance variables would be part of an Odds
object’s initialization (its constructor), and the corresponding computations would be
performed as soon as each Odds
object is created. The results of those computations
would then remain stored in memory as part of the object.
Under that approach, when we “ask” the object to provide that information, it simply
accesses the stored values. The computer wouldn’t have to keep recomputing, say,
probability
even if we access it repeatedly. On the other hand, the computer would
always compute each of these three values, once, for every single Odds
object that
we create — even though we might not have a use for all three values on each object.
And because we store the results, each Odds
object would also take up a little more
space in the computer’s memory.
Such decisions thus affect what gets stored in memory and what the computer recomputes on request. In other words, these decisions influence how much memory a program needs and how fast it runs.
How important that impact is depends on the methods/variables involved, and on what
the program does with them. In our tiny example, the decision is inconsequential, because
we create only a handful of Odds
objects and invoke their methods only a few times,
and because each Odds
object takes up only a minuscule amount of memory either way.
However, memory use and efficiency aren’t the only factors to consider when choosing
between a method and a variable. Often it is absolutely crucial whether something is
stored in memory (val
or var
) or recomputed on request (def
); this may decisively
affect whether the program works right at all.
For example, recall the Monster
class above, which didn’t work until we turned its
description
from a variable into a method that generates a report from the monster’s
current state, whenever the method is called.
(And here’s a hint: in a near-future chapter, you’ll encounter the reverse: a case where you must use a variable rather than a method.)
Oh, and here’s a simple-looking but significant question to think about
When you solved the assignment above, you probably stored
Odds
’s two constructor parameters in val
s, no? (Hope so!)
Now consider the student questions about replacing def
s
with val
s in Odds
. How would things change if you had
the constructor parameters in var
s rather than val
s?
Give it a think. I’ll put the answer in weekly bulletin 3.0, which will be published after the Week 2 deadline.
A Singleton Object as an Extension of a Class
To conclude this chapter, let’s fool about with a silly little class. As we do so, we’ll pick up a few tricks that’ll come in handy in the next chapters.
class Person(val name: String):
def say(sentence: String) = this.name + ": " + sentence
def reactToSriracha = this.say("What a nice sauce.")
def reactToKryptonite = this.say("What an odd mineral.")
end Person
Each person object has a method that commands the person to say something. The method returns a string that contains the person’s name and the sentence they utter.
A person can also be commanded to react to certain things. The person reacts by saying stuff, and therefore...
... it’s convenient to implement these methods by calling the
person’s own say
method. You can think of this as the person
object sending itself a message: “When commanded to react to
something, I’ll command this
— which is me — to execute
the say
method with a specific parameter value.”
Let’s see the class in action:
val first = Person("Jimmy")first: Person = Person@5cf6635a first.say("Super-Duper!")res21: String = Jimmy: Super-Duper! first.reactToSrirachares22: String = Jimmy: What a nice sauce. first.reactToKryptoniteres23: String = Jimmy: What an odd mineral. val second = Person("Lois")second: Person = Person@645b797d second.reactToKryptoniteres24: String = Lois: What an odd mineral.
Every Person
likes sriracha and is puzzled by kryptonite, since that’s what the class
says people should do. That’s pretty much all a Person
can do; they can’t fly, for
example. But not everyone is bound by the same laws:
Adding an instance-specific method
object superman extends Person("Clark"): def fly = "WOOSH!" end superman// defined object superman
We define a singleton object that is a special case of the
person class. The extends
keyword enables us to do this: we
“extend” the definition of the class for the purposes of this
particular object.
The Person
class requires us to specify a constructor
parameter (person name) for any object that we create. For
the special person that we’re creating here, we pass the
constructor parameter as part of the extends
definition.
We add a method for this one person alone. Other instances
of Person
do not have it.
The end marker is again optional, and it would be fine to omit it from such a short and simple object definition.
Superman can now fly in addition to being able to state his name and react to things like a normal person. This object, too, is a person object.
superman.flyres25: String = WOOSH! superman.nameres26: String = Clark superman.reactToKryptoniteres27: String = Clark: What an odd mineral.
We managed to add a tailor-made method to a specific object. What about adapting a class’s behavior?
Overriding a method
Green kryptonite is Superman’s weakness. If we want to represent Superman as a special
case of class Person
, can we take that into account?
What we need is a way to make a particular person behave differently from how the other person objects behave. We can do that:
object realisticSuperman extends Person("Clark"): def fly = "WOOSH!" override def reactToKryptonite = "GARRRRGH!"// defined object realisticSuperman realisticSuperman.reactToKryptoniteres28: String = GARRRRGH!
We redefine a method already defined in class Person
,
for this one person alone.
The redefinition overrides (korvata) the definition in
the class. To prevent us from doing something like this without
intending to, Scala sensibly requires us to explicitly state
that we actually want to override an existing method definition.
(Had we omitted the override
keyword, we’d have gotten an
error message.)
Summary of Key Points
A class’s program code defines the instance variables and methods that the class’s instances (objects) have.
When a class is instantiated, the new object gets its own copies of the instance variables, with values specific to that object.
A class also defines which constructor parameters you must provide when you create a new instance. It’s common to store the values of constructor parameters in the new object’s instance variables.
It’s possible, and occasionally useful, to attach additional methods to a specific instance of a class. It’s also possible to override an existing method definition with an instance-specific implementation.
Links to the glossary: class, instance, instance variable, to instantiate, constructor parameter;
this
; to override.
This is important stuff
Instance variables and their relationship to constructor parameters are among the trickier concepts in introductory object-oriented programming. Since they are absolutely crucial to class-based OOP and the rest of O1, we have devoted quite a few words to them in this chapter.
We strongly suggest that you try to make sure you understand these concepts before advancing to the chapters that follow. Review the examples and animations above as needed, and chat with the teaching assistants and your fellow students.
Feedback
Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.
Credits
Thousands of students have given feedback and so contributed to this ebook’s design. Thank you!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.
The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Kai Bukharenko, Nikolas Drosdek, Kaisa Ek, Rasmus Fyhrqvist, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Kaappo Raivio, Timi Seppälä, Teemu Sirkiä, Onni Tammi, Joel Toppinen, Anna Valldeoriola Cardó, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.
The other diagrams and interactive presentations in the ebook are by Juha Sorva.
The O1Library software has been developed by Aleksi Lukkarinen, Juha Sorva, and Jaakko Nakaza. Several of its key components are built upon Aleksi’s SMCL library.
The pedagogy of using O1Library for simple graphical programming (such as Pic
) is
inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and
Krishnamurthi and Picturing Programs by Stephen Bloch.
The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.
The definition of a singleton object begins with
object
. A class definition begins withclass
. You may optionally define anend
marker on either one. For clarity, it’s often a good idea to do so.