Lecture 14

2023-05-17 | Week 7 | edited by Ashwin Ranade

(originally written 2022-11-07 by Carey Nachenberg)

Ashwin here! This lecture covers OOP Palooza - slides 26-67.

Table of Contents

Deep Dive: Classes, Interfaces, Objects

  • Class: A class is a blueprint that specifies a public interface, code, and fields that make up a type of object.
  • Interface: An interface is a related group of function prototypes that describes behaviors that we want one or more classes to implement.
  • Object: An object is a distinct value/entity, often created from a class blueprint – each object has its own logical copy of data and methods.

Classes

Here’a C++ class which shows most of the major component of a typical class:

// Class Declaration
class Nerd { 
 public:
  Nerd(const string& name);
  ~Nerd();
  string talk() const;

 private:
  string name_;
  int IQ_;

  void study(int hrs);
};

// Implementation
Nerd::Nerd(const string& name) {
  name_ = name;
  IQ_ = 100;
  study(3);
}

Nerd::~Nerd() { IQ_ = 0; }
string Nerd::talk() const  { return name_ + " likes closures."; }
void Nerd::study(int hrs) { IQ_ += hrs; }

Key elements of classes include: the class name, constructors, destructors, public and private member variables/functions, the public interface, and method implementations.

Every time you define a class you define a new type

When you define a new class, like Nerd above, it also defines a new type. If the class is a concrete class (i.e., all of its methods have { definitions }) and not an abstract class (which is missing one or more method { implementations }) then you can use this new type to define all of the following: objects, object references/pointers, and references. A class is a class, and a type is a type, and they’re different entities.

Interfaces

An interface is a related group of function prototypes (declarations without { bodies }) that describes behaviors that we want one or more classes to implement. An interface specifies what we want one or more classes to do, but not how to implement these interfaces.

// Java interface definition for shapes
interface Shape {
  public double area();
  public double perimeter();
  public void scale(double factor);
}

And here’s an example from C++:

// C++ interface definition for shapes
class Shape {
 public:
   virtual double area() = 0;
   virtual double perimeter() = 0;
   virtual void scale(double factor) = 0;
};

As you can see above, C++ we use pure-virtual functions within a class to define an interface.

Once we define an interface, we can ask a class to implement or inherit from it. Here’s an example from Java that implements the Shape interface we defined above. You can see that the Square class provides { definitions/implementations } for each function declared in the interface. We can now say that a Square supports, implements, or inherits the Shape interface (that’s the terminology you’d use).

class Square implements Shape {
 ...
 public double area()
   { return w_ * w_; }
 public double perimeter()
   { return 4 * w_; }
 ...
}

Interfaces enable otherwise-unrelated classes to provide a common set of behaviors without actually inheriting any code like we learned about in CS32.

Every time you define an interface you define a new reference type

When you define a new interface, like Shape above, it also defines a new type - specifically a reference type. Since an interface lacks any function { definitions/code }, you can’t use a reference type to define full objects:

Shape x = new Shape();  // Trying to allocate a "new Shape()" doesn't work!

But you can use reference types to define all of the following: object references, pointers and references, depending on which of the above your language supports. So we can use reference types like this:

  void someFunc(Shape s) { ... }  // s is an object reference in Java, so this works

So here’s a question: If each interface defines a type, and each class defines a type, and a class implements an interface – does the class have two types? The answer is Yes! For example our Square class actually has TWO types - it is of the Square type AND it is of the Shape type! And you can pass a Square to any function that accepts either a Square or a Shape object reference, pointer or reference!

In contrast to an interface, when you define a class with all of its methods { implemented }, you create what’s called a “value type” or a concrete type - and you may define all of the following: objects, object references/pointers, and references.

Objects

An object is a distinct value, often created from a class blueprint – each object has its own copy of fields and methods.

In most languages, objects are instantiated by using a class, e.g.:

class Circle { ... };

Circle x;  // instantiates a Circle object named x in C++

A class serves as a blueprint for creating new objects. But this is not always the case! In some languages, like JavaScript, there are no classes, only objects! Here’s some code to creat a javascript object containing fake information about famous computer scientist Alan Kay. It has a bunch of fields (key-value pairs) as well as a method, fullName(). You can also see that we can update our object to add a function - we add tellJoke() to our object. Finally, we use our object to ask Alan to tell a joke. No classes are involved in creating this object.

// Javascript had objects but not classes!
var student1 = {
  first: "Alan",
  last: "Kay",
  phone: "818-555-1212",
  student_id: 958333245,
  fullName: function() 
    { return this.first + " " + this.last; }
};

student1["tellJoke"] = function() { return "Two mice walk into a bar"; }

console.log(student1.fullName() + " says " + student1.tellJoke());

Classes, Interfaces, Object Challenges

Q: Why even support the notion of a class in a language? What are the pros and cons of using a model like JavaScript with just objects?

A: Classes enable us to create a consistent set of objects, all generated in a consistent way based on the class specification. Classes enable us to define a new type, which we’ll see is useful for polymorphism in statically typed languages

Q: JavaScript can dynamically add methods to objects, is there any reason we can’t dynamically add/remove methods to/from classes?

While most/all static languages don’t allow this, dynamically typed languages absolutely allow this, Python being one example. In a statically typed language, adding a new method would potentially change the class interface which would complicate compilation/interpretation, but technically this could be done too.

Shallow-dive Into Classes

Let’s talk about some key aspects of classes that you may not have known before.

Class Fields and Class Methods

Normally, each object has its own copy of every member variable (e.g., each Stack object has its own array/count). But sometimes we want to allow the overall class to have its own fields. A “class field” is one that’s associated with the overall class – it’s stored once in memory and that single copy is used by all of the class’s objects. A “class method” is similarly associated with the overall class, and may only operates on class field.

What are the uses for class fields/methods?

  • Defining Class-level Constants: If we have a constant that’s shared by all objects of a class, we make it a class field.
  • Counting the Number of Objects: If we want to keep a count of how many objects have been instantiated, the class can track this with a class field.
  • Assigning Each Object a Unique ID: If we want to assign each object a unique ID number, we can use a class field to hold the next ID to be assigned.

Class Fields

Here’s an example from C++. We’ll update our Nerd class to not only track names/IQs for individual nerds, but to track how many total nerd objects there are overall. We’ll do this tracking with a class field that keep the total count (num_nerds_) and use our constructor/destructor to up/down the count.

// nerd.h
class Nerd { 
 public:
  Nerd(const string& name) {
   name_ = name;
   IQ_ = 100;
   ++num_nerds_;
  }
  ~Nerd() {
   --num_nerds_;
  }
  string talk() const { ... }

 private:
  string name_;
  int IQ_;
  static int num_nerds_;

  void study(int hrs) { ... }
};
// nerd.cpp
// initialize class fields in the cpp; initializes value of our class field.
int Nerd::num_nerds_ = 0;

In C++, when we define a member variable as static that means that it’s a class-level field, stored in memory just once for the class. Each object/instance doesn’t store this field within their memory! Instead, the field is stored once in the static data area of the process’s RAM per class and all object methods just refer to this singleton value. So all objects share the same singleton variabl num_nerds_.

Class Methods

A class method is one that can ONLY access class fields - it cannot access ANY instance fields (e.g., name_ or IQ_) inside of an object, because it’s not associated with any particular object. It’s associated instead with the overall class. Here’s an example in C++:

class Foo { 
 public:
  Foo(int v) {
    val_ = v;
    ++num_foos_;
  }
  ~Foo() { --num_foos_; }
  int inc()
   { ++val_; return val_; }

  static int getActiveFooCount()
   { return num_foos_; }

 private:
  int val_;
  static int num_foos_;
};

int main() {
  Foo a(5);

  cout << "Count of foos: " << a.getActiveFooCount() << endl;
  cout << "Count of foos: " << Foo::getActiveFooCount();
}

In the above example, the getActiveFooCount() method is a class-level method, because it’s define as static. It may only access class-level fields or call other class-level methods, since it knows nothing about instance-level fields/methods. As you can see, in C++ we have two ways of calling a class-level method: using an instance variable name, like a followed by a dot, or by using the class name, like Foo, followed by two colons. Other languages vary in the syntax used to call class-level methods.

Here’s an example from python - you can see we use the class name (e.g., Foo) followed by a period to call a class method or access a class variable:

# Python class variables/functions
class Foo:
 num_foos_ = 0  # class variable

 def __init__(self, v):
   val_ = v
   Foo.num_foos_ = Foo.num_foos_ + 1

 def __del__(self):
   Foo.num_foos_ = Foo.num_foos_ - 1

 def inc(self):
   val_ = val_ + 1
   return val_

 @classmethod
 def get_num_foos(cls):
   return cls.num_foos_

a = Foo(5)
b = Foo(6)
print("Number of foos:", Foo.get_num_foos())

And here’s an example from Java. It looks pretty similar to C++ in that it uses static to define class methods/fields:

// Java class variable/method example
public class Nerd {
  private String name;
  private int IQ;
  private static int num_nerds;
  
  static {
    num_nerds = 0;
  }
  public Nerd(String name) {
    this.name = name;
    this.IQ = 100;
    num_nerds++;
  }
  public void finalize() {
    this.IQ = 0;
    num_nerds--;
  }

  public static int getNumNerds() {
    return num_nerds;
  }
}
// Java calling a class method getNumNerds()
public static void main(String args[]) {
  Nerd n = new Nerd("Carey");
  System.out.println("There are " + Nerd.getNumNerds() + " nerds.");
}

Class Field and Class Method Challenge

Q: When would we want to use class-level methods?

A: We use class-level methods when a method only accesses class-level variables and thus doesn’t need access to member variables. We might also use class-level methods when defining a class that contains a bunch of related functions that aren’t associatee with individual objects. For example, imagine we’re defining a Math class with functions like sqrt(), exp(), tan(), cos(), etc. Those functions could all be class functions and placed into a Math class.

class MathLibrary {
   static double sqrt(double x); // doesn't need access to member variables
   static double exp(double x);  // ditto!
}; 

Q: Can a class-level method call a traditional instance-level method? What about visa-versa? Why?

A: No, a class method can’t call an instance method, because an instance method by definition is associated with an instance, but a class method is not. So when the class method runs, it has no idea what object it’s associated with, so it can’t call an intance method - which would be associated with a particular object. On the other hand, an instance method can call a class method just fine.

Classify That Language: Class Functions

Which, if any of the methods in this class are class methods (as opposed to instance methods)?

struct Nerd {
  name: String,  
  iq: i32
}
 
impl Nerd {
  // constructor
  pub fn new(name: String) -> Nerd {
    return Nerd { name: name, iq: 100 }
  }
    
  pub fn study(&mut self, hrs: i32) {
    self.iq += hrs;
    println!("{} studied {} hours.", self.name, hrs);
    println!("My incredible IQ is now {}.", self.iq)
  }
}
 
fn main () {
  let mut n = Nerd::new("Carey".to_string());
  n.study(3);
}

Notice how the study() method has a self parameter (like we see in python, or “this” in C++). That identifies study() as an instance method. But notice that the new() method - a constructor in this language - does not have a self parameter. That is an indication it’s a class method. It has no way of identifying a particular instance/object without a self field. So constructors in this langauge are class-level methods, and they must construct and then return a new object. This language is Rust!

This and Self

When a method is called, it must be told what object it is to operate on (so it can find the object’s member fields). In most languages, this is done transparently when you call a method on an object p, e.g., p.printMyAddress(). The language passes a reference to p as a hidden extra argument, and it may be referenced in the method as self or this. Self/this is an object reference or pointer in most languages.

Here’s an illustration of how this is implemented in c++:

// The code you write:
class Nerd {
 private:
   string name;
   int IQ;

 public:
   Nerd(const string& name) {
     this->name = name;
     IQ = 100; // this->IQ is optional
   }

   string talk() {
     return this->name + " likes PI."; 
   }
};

int main() {
 Nerd n("Carey");
 cout << n.talk();
}
// What's actually happening for the code above:
struct Nerd {
  string name;
  int IQ;
};

void init(Nerd *this, const string& name) {
  this->name = name;
  this->IQ = 100;  
}

string talk(Nerd *this) {
  return this->name + " likes PI.";
}

int main() {
 Nerd n;
 init(&n, "Carey");
 cout << talk(&n);
}

And here’s an example in Python that uses self:

# Python Nerd class
class Nerd:
 def __init__(self, name):
   self.name = name
   self.IQ = 100

 def talk(self):
   return self.name + " likes closures."

In python, using the self. prefix is mandatory, unlike in many other languages where self/this is usually optional (unless we need to bypass shadowing of a local variable).

Here’s an example in go:

// Golang Nerd class
type Nerd struct {
  name string
  IQ   int
}

func New(name string) *Nerd {
 ...
}

func (self *Nerd) Talk() string {
  return self.name + " likes closures."
}

func (self *Nerd) Study(hrs int) {
  self.IQ += hrs
}

...

n := New("Carey")
fmt.Println(n.Talk())

It’s interesting to note that in Go, the self parameter is explicit and it’s placed before the function name, all by itself. Then the usual parameters are placed after the function name.

This and Self Challenges

Q: Would a class method (as opposed to an instance method) also have a this/self parameter? Why or why not? A: Class-level methods operate on the overall class and don’t even know what instance they’re operating on (if any), so no this/self pointer or object reference is passed to them.

Q: In Python, could you make the self. prefix optional? If not, why not? A: In Python as it currently stands the self. prefix is required. While a reference to read a variable could figure out if a variable is a local or a member variable, assigning a variable (without self.) would be problematic. Would a = 5 create a new member variable (self.a) or would it create a new local variable? Without explicit member variable declarations, which Python doesn’t have, assignment without the self prefix would be ambiguous.

Classify That Language: This and Self

package Employee;   # Equivalent to a class def.

sub new_employee {  # constructor function
  # Parameters held in $_[0], $_[1], ... $_[n-1]
  # employee name in $_[0], and income in $_[1]
  my $obj = {};   
  $obj->{"name"} = $_[0];   # param 0 is name
  $obj->{"income"} = $_[1]; # param 1 is income
  bless $obj, "Employee";
  
  return $obj;  # return the object
} 

sub compute_ytd_income { # func. to compute income
  my $emp = $_[0];  # 1st param is employee obj
  my $months_worked = $_[1]; 
  return $emp->{'income'} * $months_worked/12;
} 

...

# Code to create/use an employee
use Employee;

$e = new_employee("Ed", 100000);
print "My name: ", $e->{"name"};
print "My income thru Nov is: ",
      $e->compute_ytd_income(11);

Q: Does this language have a notion of this/self for objects? Explain! A: Answer: Yes! When we call a method, e.g., $e->compute_ytd_income(), this passes the object reference $e as the first argument to the method (as $_[0]). In this language, function parameters are placed in array called $_ and you can index them. This is Perl.

Access Modifiers

As we learned, a key goal of encapsulation is to hide the data/implementation details from users of a class. Languages provide “access modifier” syntax to enable a class to designate what parts of a class are publicly visible and to hide the class’s data/implementation.

There are generally three different access modifiers:

  • public: A public method or data member in class C may be accessed by any part of your program.
  • protected: A protected method or data member in class C may be accessed by C or any subclass derived from C.
  • private: A private method or data member in class C may only be accessed by methods in class C.

Public, protected and private generally have the same semantics across languages, though there are exceptions. For instance, Java’s protected keyword not only lets subclasses access the member/method, but also lets any other class defined in the same source file.

If you omit an explicit access modifier, different languages have different defaults – some default fields/methods to private, some default to public, etc. Python defaults to public, for example, while C++ defaults to private.

Here’s an example from Swift:

// Swift access modifiers
class Person {
  private var name: String

  public init(name: String) {
    self.name = name
  }
  public func talk() {
   print("Hi, my name is "+self.name)
  }
  private func poop() {
    print("Grunt... plop!")
  }
}

class Student: Person {
  ... 
  public func dostuff() {
     self.talk()     // Works – talk is public
     self.poop()     // Error – poop private
  }
}

var p = Person(name:"Irna")
p.talk()     // Works – talk is public
p.poop()     // Error – poop private

Note: Swift doesn’t have a protected keyword! It does have something called internal which is similar but slightly different, however.

Here’s how we do public/protected/private in Python. It uses underscores to indicate protected/private methods:

# Python access modifiers
class Person:
 def __init__(self, name):
   self.__name = name

 def talk(self):         # Public method
   print("Hi, my name is "+ self.__name)

 def _daydream(self):    # Protected method
   print("Rainbow narwhals")
  
 def __poop(self):       # Private method
   print("Grunt... plop!")


# Derived class
class Student(Person):
 ...
 def do_stuff(self):
   self.talk()           # Works fine - public
   self._daydream()      # Works fine - protected
   self.__poop()      

a = Student("Cindi")
a.talk()          # Works fine - public
a._daydream()     # This works!! - Python doesn't enforce protected methods
a.__poop()        # Generates error

Encapsulation Best Practices

Here are some things to think about when using encapsulation.

  • Design your classes such that they hide all implementation details from other classes.
  • Make all member fields, constants, and helper methods private.
  • Avoid providing getters/setters for fields that are specific to your current implementation, to reduce coupling with other classes.
  • Make sure your constructors completely initialize objects, so users don’t have to call other methods to create a fully-valid object.

Classify That Language: Access Modifiers

Our Comedian class has both public and private methods… but no explicit public or private keywords!! Assuming comedians are only serious in private, how does this language hide private members?

package comedian

type Comedian struct {
  name string
  joke string
}



func (c *Comedian) TellJoke(times int) {
  for i := 1; i < times; i++ {
    fmt.Println(c.joke)
  }
}

func (c *Comedian) GetName() string {
  return c.name
}

func (c *Comedian) beSerious() {
  fmt.Println(c.name + " saves for retirement!\n")
}

func main() {
  c1 := Comedian{"Carey", "Knock, knock..."}
  c1.TellJoke(5)
}

Answer: In this language, methods that begin with a capital letter are public, and those that begin with a lower-case letter are private. This is GoLang!

Properties, Accessors and Mutators

While our goal in OOP is encapsulation, sometimes we want to expose a field (aka property) of an object for external use. For example, in a Person object we might want to expose a person’s name externally.

  • A property is some attribute of an object, e.g., a person’s age or name.
  • An accessor is a method that gets the value of such a property.
  • A mutator is a method that changes the value of a property.

Below is how we’d create accessors/mutators in C++ - you have to roll your own. But many programming languages have syntax to define accessors and mutators more easily.

class Person {
public:
  string getName() const;       // accessor
  void setName(string name);    // mutator
};

Why use accessors? Accessors and mutators for a property enable us to hide a field’s implementation and more easily refactor. We can define a function which abstracts the underlying implementation of a given field/property. Some languages actually support built-in syntax to create accessors/mutators for properties. Let’s look at a few examples:

# Python's accessors/mutators
class Person: 
 ...

 @property
 def age(self): 
   return self._age

 @age.setter
 def age(self, age):
   if age >= 0 and age <= 130:
     self._age = age
   else:
     raise ValueError("Invalid age!")

a = Person("Archna", 22)
print(a.age)
a.age = 23

In python, the first age() method is an accessor method. The @property designation is python’s way of indicating that this function is an accessor. Simply referring to the function by name, just like you would a member variable - causes it to be called. Notice how we simply refer to a.age int the call to print(a.age). This looks just like its reading the value of a member variable but it’s really calling a function, whose return value is treated like the value of the member variable. Similarly, @age.setter designates that the second age method (which takes two parameters) is a mutator method. Simply using assignment, as in a.age = 23, causes this method to be called and the value of 23 to be passed in as the age argument.

Here’s another example from Kotlin, which defines accessors/mutators:

# Kotlin's accessors/mutators
class Temperature {
  var celsius: Double = 0.0
    get() { return field }
    set(value) { field = value }
  var fahrenheit: Double  
    get() {
      return ((celsius * 1.8) + 32.0)
    }
    set(value) {
      celsius = (value - 32)/1.8
    }
}

fun main(args: Array<String>) {
  var b = Temperature()
  b.fahrenheit = 98.6
  println(b.celsius)  // 37
}

In Kotlin, we may define one or more properties (celsius, fahrenheit) and then specify a getter and setter for each such property. And in fact, the property might just be synthetic, as fahrenheit is above (in other words, it doesn’t have a concrete member variable). Any reference to fahrenheit really uses the backing celsius field for its computations.

Property Best Practices

So when should you write a property vs. defining a traditional method (in a language with explicit support/syntax for properties)? Here’s the rule of thumb if your language supports syntax for properties: Use properties when you’re exposing the state of a class (even if exposing that state requires minor computation). Use traditional methods when you’re exposing behaviors of a class or exposing a state that requires heavy computation (e.g., a database query that might take tens of milliseconds or more). In general, you don’t want to use properties to expose implementation details of a class that might change if you update your implementation. So exposing a property of a name would be fine, but exposing a property for some internal member variable that is specific to your current implementation would be bad.