Sealed classes and methods
You are encouraged to solve this task according to the task description, using any language you may know.
- Definition
In object-oriented programming, a sealed or final class is one which cannot be inherited from.
Classes are sometimes made non-subclassable in this way if the author feels that it would not be useful or even undesirable for subclasses to be created from them. Moreover, in a compiled language, knowing that a class cannot be subclassed, may enable optimizations to be made.
Rather than sealing the entire class, it may be possible to just seal certain methods for the same reasons and with the same potential benefits.
- Task
If your language supports the object oriented paradigm, explain what support it has for sealed classes and/or methods and, if there is no support, what if anything can be done to simulate them. If possible illustrate your answer with an example.
If your language does not support the object oriented paradigm (or its inheritance aspect), you may either simply say so, omit the task altogether or describe any equivalent structure(s) and restrictions on use which the language does have.
- Reference
- Related task
C
C isn't an object oriented language though it can and has been used to create other languages which are.
It has structs rather than classes which are just a collection of fields. Methods on a struct can be simulated by functions whose first argument is a pointer to the struct.
To simulate inheritance, one can embed a 'parent' field first in the 'child' struct and then pass the address of that field to 'parent' methods.
However, there is no way that the method can tell whether it's receiving a pointer to a 'parent' instance or a pointer to a 'child' field. To use the Wren technique for simulating sealed methods, we therefore need to pass a further parameter, namely a type identifier, as the following code illustrates.
#include <stdio.h>
typedef enum {
PARENT,
CHILD
} typeid;
typedef struct {
const char* name;
int age;
} parent;
typedef struct {
parent p;
} child;
void watchMovie(parent *p, typeid id) {
if (id == CHILD && p->age < 15) {
printf("Sorry, %s, you are too young to watch the movie.\n", p->name);
} else {
printf("%s is watching the movie...\n", p->name);
}
}
int main() {
parent p = { "Donald", 42 };
child c1 = { "Lisa", 18 };
child c2 = { "Fred", 10 };
watchMovie(&p, PARENT);
watchMovie(&c1.p, CHILD);
watchMovie(&c2.p, CHILD);
return 0;
}
- Output:
Donald is watching the movie... Lisa is watching the movie... Sorry, Fred, you are too young to watch the movie.
C++
Classes and functions can be sealed in C++ by using the final keyword.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class MovieWatcher // A base class for movie watchers
{
protected:
std::string m_name;
public:
explicit MovieWatcher(std::string_view name) : m_name{name}{}
virtual void WatchMovie()
{
std::cout << m_name << " is watching the movie\n";
}
virtual void EatPopcorn()
{
std::cout << m_name << " is enjoying the popcorn\n";
}
virtual ~MovieWatcher() = default;
};
// ParentMovieWatcher cannot be inherited from because it is 'final'
class ParentMovieWatcher final : public MovieWatcher
{
public:
explicit ParentMovieWatcher(std::string_view name) : MovieWatcher{name} {}
};
// ChildMovieWatcher can be inherited from
class ChildMovieWatcher : public MovieWatcher
{
public:
explicit ChildMovieWatcher(std::string_view name)
: MovieWatcher{name}{}
// EatPopcorn() cannot be overridden because it is 'final'
void EatPopcorn() final override
{
std::cout << m_name << " is eating too much popcorn\n";
}
};
class YoungChildMovieWatcher : public ChildMovieWatcher
{
public:
explicit YoungChildMovieWatcher(std::string_view name)
: ChildMovieWatcher{name}{}
// WatchMovie() cannot be overridden because it is 'final'
void WatchMovie() final override
{
std::cout << "Sorry, " << m_name <<
", you are too young to watch the movie.\n";
}
};
int main()
{
// A container for the MovieWatcher base class objects
std::vector<std::unique_ptr<MovieWatcher>> movieWatchers;
// Add some movie wathcers
movieWatchers.emplace_back(new ParentMovieWatcher("Donald"));
movieWatchers.emplace_back(new ChildMovieWatcher("Lisa"));
movieWatchers.emplace_back(new YoungChildMovieWatcher("Fred"));
// Send them to the movies
std::for_each(movieWatchers.begin(), movieWatchers.end(), [](auto& watcher)
{
watcher->WatchMovie();
});
std::for_each(movieWatchers.begin(), movieWatchers.end(), [](auto& watcher)
{
watcher->EatPopcorn();
});
}
- Output:
Donald is watching the movie Lisa is watching the movie Sorry, Fred, you are too young to watch the movie. Donald is enjoying the popcorn Lisa is eating too much popcorn Fred is eating too much popcorn
FreeBASIC
Type Parent
nombre As ZString * 7
edad As Byte
Declare Operator Cast () As String
End Type
Operator Parent.cast As String
Return this.nombre & " is watching the movie..."
End Operator
Type Child Extends Parent
Declare Operator Cast As String
End Type
Operator Child.cast As String
If this.edad < 15 Then
Return "Sorry, " & this.nombre & ", you are too young to watch the movie."
Else
Return this.nombre & " is watching the movie..."
End If
End Operator
Dim As Parent p1, p2
p1.nombre = "Donald" : p1.edad = 42
p2.nombre = "Dougal" : p2.edad = 12
Print p1
Print p2
Dim As Child c1, c2
c1.nombre = "Lisa" : c1.edad = 18
c2.nombre = "Fred" : c2.edad = 10
Print c1
Print c2
Sleep
- Output:
Donald is watching the movie... Dougal is watching the movie... Lisa is watching the movie... Sorry, Fred, you are too young to watch the movie.
Go
Go isn't really an object oriented language - not in a conventional sense anyway.
It has structs rather than classes which are just a collection of fields. It does however have methods which are declared outside the struct itself but within the same package and whose receiver is either a struct instance or a pointer to one. Non-struct types can also have methods though this isn't relevant here.
As a general rule, all entities are accessible within the same package but are only accessible to other packages if their name begins with an upper case letter.
Inheritance is not supported but can be simulated to some extent by embedding one struct inside another. The latter is then able to access the former's fields directly and to call its methods.
Consequently, a Go struct and its methods are effectively sealed unless the struct is embedded in another one. However, the only way to prevent embedding from outside the package would be to make the struct private to its package which may not be an acceptable solution unless it and/or its methods could be exposed indirectly.
Fortunately, as the following example shows, the Wren technique for sealing methods can still be used provided we pass a further parameter (a type identifier) to the method so that it knows whether its being called with a pointer to a 'parent' or to a 'child' instance. This information is needed because the type system is such that the runtime type of the receiver will always be 'parent'.
package main
import "fmt"
type typeid int
const (
PARENT typeid = iota
CHILD
)
type parent struct {
name string
age int
}
type child struct {
parent // embedded struct
}
func (p *parent) watchMovie(id typeid) {
if id == CHILD && p.age < 15 {
fmt.Printf("Sorry, %s, you are too young to watch the movie.\n", p.name)
} else {
fmt.Printf("%s is watching the movie...\n", p.name)
}
}
func main() {
p := &parent{"Donald", 42}
p.watchMovie(PARENT)
c1 := &child{parent{"Lisa", 18}}
c2 := &child{parent{"Fred", 10}}
c1.watchMovie(CHILD)
c2.watchMovie(CHILD)
}
- Output:
Donald is watching the movie... Lisa is watching the movie... Sorry, Fred, you are too young to watch the movie.
J
No J compilers have been released (though some people have claimed to be working on such things). That said, J does provide a locked script mechanism which might be thought of as compiled code (which depends on libj).
J, by default, does not support sealed classes (nor methods). However, sealed classes could be implemented by altering J's coinsert
(which implements inheritance) to omit sealed classes.
For example, we could say that a class which contained any implementation of final
is a sealed class:
coinsert=: {{
l=. (#~{{0>nc<'final__y'}}"0);: :: ] y
p=. ; (, 18!:2) @ < each l
p=. ~. (18!:2 coname''), p
(p /: p = <,'z') 18!:2 coname''
}}
J does not provide a mechanism to seal individual methods.
Java
Classes and methods can be sealed in Java by using the 'final' keyword.
import java.util.List;
public final class SealedClassesAndMethods {
public static void main(String[] args) {
List<MovieWatcher> movieWatchers = List.of( new ParentMovieWatcher("Donald"),
new ChildMovieWatcher("Lisa"),
new YoungChildMovieWatcher("Fred") );
for ( MovieWatcher movieWatcher : movieWatchers ) {
movieWatcher.watchMovie();
movieWatcher.eatPopcorn();
}
}
}
// Base class for MovieWatcher's
class MovieWatcher {
public MovieWatcher(String aName) {
name = aName;
}
public void watchMovie() {
System.out.println(name + " is watching the movie");
}
public void eatPopcorn() {
System.out.println(name + " is eating popcorn");
}
private final String name;
}
// ParentMovieWatcher cannot be extended because it is 'final'
final class ParentMovieWatcher extends MovieWatcher {
public ParentMovieWatcher(String aName) {
super(aName);
}
}
// ChildMovieWatcher can be extended.
class ChildMovieWatcher extends MovieWatcher {
public ChildMovieWatcher(String aName) {
super(aName);
name = aName;
}
// The method eatPopcorn() cannot be overridden because it is 'final'
public final void eatPopcorn() {
System.out.println(name + " is eating too much popcorn");
}
private String name;
}
class YoungChildMovieWatcher extends ChildMovieWatcher {
public YoungChildMovieWatcher(String aName) {
super(aName);
name = aName;
}
// The method watchMovie() cannot be overridden because it is 'final'
public final void watchMovie() {
System.out.println(name + ", you are too young to watch the movie.");
}
private String name;
}
- Output:
Donald is watching the movie Donald is eating popcorn Lisa is watching the movie Lisa is eating too much popcorn Fred, you are too young to watch the movie. Fred is eating too much popcorn
Julia
Julia's multiple dispatch and type system sit firmly outside the context within which sealed (non-inheritable) classes make sense.
First, within Julia's class type system, all inheritance is between abstract types. Objects can have inheritance from abstract types, but objects cannot inherit from other objects. Thus, in Julia, all concrete objects are final.
Second, because of Julia's multiple dispatch, all object methods (except certain constructors, called inner constructors) can be overloaded within user code. Thus, only inner construction methods are final methods.
Thus, Julia both enforces a kind of sealed classes for inheritance with all of its objects (the first above) and yet prevents any simple sealing for those same objects' methods (the second above).
Nim
Nim allows to define object types. There is two ways to make a type inheritable. Here is an example:
type T1 = object # Non inheritable.
type T2 = object of RootObj # Inherits from Root and is inheritable.
type T3 {.inheritable.} = object # New object root which is inheritable.
type T4 = object of T2 # Inheritable.
type T5 = object of T3 # Inheritable.
type T6 {.final.} = object of T2 # Non inheritable.
As we can see in the example, if a type inherits from another type, it is also inheritable unless it is annotated with the final
pragma.
In Nim, one can define methods on objects. There is no way to declare a method as final to forbid its overriding.
Phix
No support, though it would probably not be particularly difficult to add a "final" keyword if ever needed.
Phix supports object orientation for the die-hards (desktop/Phix only), but does not require it be used at all.
In Phix, "private" determines availability from outside [sub-]class code, but there is nothing at all to prevent Child from
having a public method that (internally) invokes a private method of the Parent, and of course/not unlike it would make no difference were name and age made private below.
The Phix compiler can make exactly the same optimisations were it to spot at EOF that a method was not overidden as it could were it told up-front.
Extended to include an under-age parent
without javascript_semantics -- no classes under p2js, sorry include builtins\structs.e -- (needed for get_struct_name) class Parent string name integer age procedure watch_movie() if get_struct_name(this)!="Parent" and age<15 then printf(1,"Sorry, %s, you are too young to watch the movie.\n",{name}) else printf(1,"%s is watching the movie...\n",{name}) end if end procedure end class class Child extends Parent end class Parent p1 = new({"Donald", 42}), p2 = new({"Dougal", 12}) p1.watch_movie() p2.watch_movie() Child c1 = new({"Lisa", 18}), c2 = new({"Fred", 10}) c1.watch_movie() c2.watch_movie()
- Output:
Donald is watching the movie... Dougal is watching the movie... Lisa is watching the movie... Sorry, Fred, you are too young to watch the movie.
Of course you would get the same output from and it is usually much more sensible to do something like this:
class Parent private string name private integer age procedure watch_movie() printf(1,"%s is watching the movie...\n",{name}) end procedure end class class Child extends Parent procedure watch_movie() if age<15 then printf(1,"Sorry, %s, you are too young to watch the movie.\n",{name}) else printf(1,"%s is watching the movie...\n",{name}) end if end procedure end class
Note however in Phix there is no possibility of invoking parent.watch_movie() from the else branch, because you have completely replaced that routine in the child instance. Should you want to share code in that kind of fashion you would need to give it a different and completely unambiguous name.
Raku
Raku doesn't have final class but Roles is normally used to mimic the goal.
# 20240626 Raku programming solution
role MovieWatcherRole { has Str $.name;
method WatchMovie() { say "$.name is watching the movie." }
method EatPopcorn() { say "$.name is enjoying the popcorn." }
}
class MovieWatcher does MovieWatcherRole {
method new(Str $name) { self.bless(:$name) }
}
class ParentMovieWatcher is MovieWatcher { }
role ChildMovieWatcherRole {
method EatPopcorn() { say "$.name is eating too much popcorn." }
}
class ChildMovieWatcher is MovieWatcher does ChildMovieWatcherRole { }
role YoungChildMovieWatcherRole {
method WatchMovie() {
say "Sorry, $.name, you are too young to watch the movie."
}
}
class YoungChildMovieWatcher is ChildMovieWatcher does YoungChildMovieWatcherRole { }
for ParentMovieWatcher.new('Donald'),
ChildMovieWatcher.new('Lisa'),
YoungChildMovieWatcher.new('Fred')
{ .WatchMovie and .EatPopcorn }
You may Attempt This Online!
Wren
Although Wren is an object-oriented language, it does not support sealed classes or methods. Nor does it have 'private' methods. All instance methods (though not constructors or static methods) are automatically inherited by subclasses.
It might be argued that such a restriction would be out of place anyway in a simple embedded scripting language, particularly when libraries can only be distributed in source code form at the present time.
Nevertheless, it is possible to simulate sealed methods by simply stopping them from executing normally at runtime unless they are being accessed by an object of the same class. In fact, as the following example shows, this technique allows methods to be conditionally (rather than absolutely) sealed where the circumstances warrant this.
Note that:
1. To seal the entire class one would need to apply the same technique to all instance methods though this would be very tedious in practice for classes having a lot of methods.
2. With the exception of Object and Sequence, it is not possible to inherit from Wren's built-in classes anyway for technical reasons. Nor is it possible to inherit from 'foreign' classes i.e. classes which are instantiated from C rather than Wren.
3. Using the 'is' operator (i.e. a is C) to detect the type of 'a' wouldn't work here as this would return 'true' if 'a' were either a 'C' object or an object of a subclass of 'C'. It is possible to spoof the 'is' operator by overriding its normal behavior, though this is definitely not recommended!
class Parent {
construct new(name, age) {
_name = name
_age = age
}
watchMovie() {
if (this.type != Parent && _age < 15) {
System.print("Sorry, %(_name), you are too young to watch the movie.")
} else {
System.print("%(_name) is watching the movie...")
}
}
}
class Child is Parent {
construct new(name, age) {
super(name, age)
}
}
var p = Parent.new("Donald", 42)
p.watchMovie()
var c1 = Child.new("Lisa", 18)
var c2 = Child.new("Fred", 10)
c1.watchMovie()
c2.watchMovie()
- Output:
Donald is watching the movie... Lisa is watching the movie... Sorry, Fred, you are too young to watch the movie.