Lecture01 - nus-cs2030/2324-s2 GitHub Wiki

OOP Principles of Abstraction and Encapsulation

This lecture focuses on the first two principles of object oriented programming, namely abstraction and encapsulation. Moreover, we shall start introducing the concept of effect-free programming using immutable objects, as well as instilling good coding practices.

Java: a Statically-Typed Language

Students who have been programming with Python or Javascript will be facing their first hurdle when crossing over to strongly-typed languages like C or Java. Every variable and value is associated with a type.

A value is an evaluation of an expression. The validity of an expression depends on the rules of the programming languages. In the case of Java, an expression comprises of literal values (e.g. 1), operations (e.g. infix 1 + 2, prefix -1), parantheses (..), method invocations, constructor invocation using new, variables, and even the assignment (to allow say, 1 + (y = 1)).

Passing a value around when the program runs can be viewed as data flow:

  • Since a value can be assigned to a variable, data flows from the RHS evaluation of an expression to the LHS variable;
  • Since a value can be passed to a method, data flows from the arguments of a method call, to the parameters of the method;
  • Since a value can be returned from a method, data flows from the return value of the method to the caller of the method.

Function as an Abstraction

You will realize from the start of the lecture that the course focuses very heavily on writing functions (or methods). Our first refresher example already exemplifies this. To solve the problem of finding the Euclidean distance between two points, you would have to first think of how to abstract out (or abstract away) the technicalities of distance computation and provide this abstraction as a service to the caller of the function.

You will need to consider the following:

  • What is the name of the function;
  • What should the caller provide as input (together with the associated types) to the function;
  • What is the return value (as well as type of this value) output from the function.

Our very first solution utilizes the data-process model to devise a solution where the function is seen as a process that takes in data to perform the desired computation and return the result.

This could either be in the form of

double distanceBetween(double px, double py, double qx, double qy) { ... }

or

double distanceBetween(Point p, Point q) { ... }

The advantage of taking in two Point objects as input instead of four double values is that Point provides the necessary data abstraction to package the x and y coordinate values into each Point, so that the method can take in two points , rather than four seemingly unassociated random looking double values.

Object-oriented Modeling

From using the data-process model to formulate the solution for distance computation, we moved on to object-oriented modeling.

We say that an object is an abstraction of closely-related data and behaviour.

In terms of data abstraction, a Point abstracts away the details of how the x and y values are stored.

In terms of behavioural abstraction, a Point abstracts away the details of how it computes the distance between itself and another point.

Q4: Besides Point and Circle, give another example of a class, together with the abstractions of data and behaviour. Think of another class with a has-a relationship to this first class as its data abstraction, and provide relevant abstractions of behaviour.

Abstraction and the Abstraction Barrier

An abstraction barrier sits between a class (called the implementer) and the user of this class (called the client). The implementer abstracts away the internal data representation, as well the detailed implementation of each function (e.g. distance computation) from the client. The only way that a client can communicate with the implementer is through services that the latter provides. These include the way an object of the implementer can be constructed (i.e. the constructor) and other instance methods the implementer provides.

By providing a distanceTo abstraction as a service to the client, the implementer could also have the freedom to modify the internal representation, say using a List instead.

class Point {
    private List<Double> coord;

    Point(double x, double y) {
        this.coord = List.of(x, y);
    }

    private double getX() {
        return this.coord.get(0);
    }

    private double getY() {
        return this.coord.get(1);
    }

    double distanceTo(Point otherPoint) {
        double dx = this.getX() - otherPoint.getX();
        double dy = this.getY() - otherPoint.getY();
        return Math.sqrt(dx * dx + dy * dy);
    }
    ...
}

Encapsulation

While abstraction is about hiding implementation details, encapsulation is the principle that restricts access. In terms of data, the implementer should always restrict client access to these data. In terms of functionality, the implementer can restrict access to certain methods, thereby not allowing the client to call these methods.

In line with encapsulation, restricted data should not be made accessible using getter methods, e.g. getX() and getY() in the Point class. Doing this violates the guiding principle of Tell, Don't Ask.

Notice in the above definition of the Point class that the getX() and getY() methods are declared private. These methods serve as useful helper methods within the Point class itself. Since they are not accessible by clients, "Tell, Don't Ask" is not violated.

Effect-Free Programming

Before we start talking about effect-free programming, consider a Point object defined below:

Point p = new Point(1.0, 1.0);
Point q = p;

Note that p and q actually refer to the same object. How do we know this? Consider the following Point class with a moveTo method.

class Point {
    private double x;
    private double y;

	Point(double x, double y) {
		this.x = x;
		this.y = y;
	}

	void moveTo(double x, double y) {
		this.x = x; 
		this.y = y;
	}

	public String toString() {
		return "(" + x + ", " + y + ")";
	}
}

Notice that moveTo will modify the properties directly. Now we write some code to move a point.

jshell> Point p = new Point(1.0, 1.0)
p ==> (1.0, 1.0)

jshell> Point q = p
q ==> (1.0, 1.0)

jshell> p.moveTo(2.0, 2.0)

jshell> p
p ==> (2.0, 2.0)

jshell> q
q ==> (2.0, 2.0)

Notice that as point p moves, point q also moves?

You will also come to appreciat that effect-free programming is the over-arching theme of CS2030. Our journey towards effect-free programming begins with the definition of immutable objects, and we achieve this by making object properties private final. As such, the moveTo method has to return a new point instead.

class Point {
    private fial double x;
    private final double y;

	Point(double x, double y) {
		this.x = x;
		this.y = y;
	}

	Point moveTo(double x, double y) {
        return new Point(x, y);
	}

	public String toString() {
		return "(" + x + ", " + y + ")";
	}
}

Q8: In the context of effect-free programming, does it matter if q and p point to the same point or different points?

Top-Down Design vs Bottom-Up Implementation

Top-down design involves breaking a large-scale problem into smaller, manageable sub-problems, which are in turn broken down into yet smaller sub-problems, until we attain the smallest constituents.

Bottom-up implementation and testing starts with the smallest constituents, having them individually implemented and tested, before going up to the larger problems that depends on the solutions to these constituents. This is also known as incremental development and testing.

Here is an interesting analogy of incremental development [credit: @ZHD1987E]

Think of a bus. The bus has multiple systems, for example, the drivetrain. Even the drivetrain or other systems has sub-systems, for example the engine itself, the gearbox, the axles and all that stuff. All that integration needs to be planned so that the unit designs fit the agreed specifications (you don’t want an engine that is too big to fit the gearbox!) From here, you develop these sub-systems (develop the engine, drivetrain and other basic parts) and test them to make sure they work as a unit (the engine starts and runs as planned, the transmission has no fatal flaws), and then integrate them slowly (with good testing to make sure they integrate well with no strange side effects, you don’t want to install an air conditioner and cause the engine to fail for some odd reason) until you have an entire bus that works well.

There, a full working bus.

Java API:

You have been exposed to the sqrt method of the Math class and the add method from the Math class. You have also dabbled with methods from the ArrayList class.

For the Math class, we do not have to instantiate a Math object before calling its methods. The methods in the Math class are static, and we call them directly on the class, such as Math.sqrt(9.0).

On the other hand, for the ArrayList class, we have to instantiate an ArrayList object before calling its methods. We use new to create an instance, for example,

ArrayList<String> list = new ArrayList<String>()

Once the object is created, we can call methods on the ArrayList instance, such as adding elements with list.add("one").

[credit: @zenlxy]

⚠️ **GitHub.com Fallback** ⚠️