Note: This series is for my revision of content and learning consolidation. Posting it here because I like DEV's code highlighting. Join me if you want to refresh your memory on OOP & Java.
INTRO (Some Java basics)
- Different from Python, Java needs to be compiled before running
- Acquiring type awareness is a must when writing Java programs
- Java focuses on object-oriented modeling where everything is within some classes
- Java Memory Model: Stack for function executions, Heap for object storage and garbage collection, Non-heap(Metaspace) for static fields etc.
OOP
I think there are a few key points about OOP that is different from a standard process-oriented way of writing code:
- Model your code based on real-life objects
- Everything else is an interaction between objects
Supposed we want to find out if a point is within a circle, what do we do? Basic programming knowledge tells us that we need to write an algorithm, or more precisely a function that describes the procedures that a computer will follow. Let's say we go with the following:
boolean contains(double pointX, double pointY, double centreX, double centreY, double radius){
// by distance between two points on a graph formulae
double dx = pointX - centreX;
double dy = pointY - centreY;
double distance = math.sqrt(dx*dx + dy*dy);
return distance < radius;
}
It will work, but can we do better?
The first observation is that there are five parameters. Do you want to use a function, possibly written by others, that requires you to input 5 different parameters in a specific order? It is definitely error-prone and the user has to figure out which double value goes to where. So, Data Abstraction to the rescue.
Instead of having multiple parameters, many of them are mere numbers, we can start thinking of the problem by modeling two objects. The aim here is to reduce the parameters down to two. Here's one way to do it:
class Point {
double x;
double y;
}
class Circle {
Point centre;
double radius;
}
boolean contains(Point p, Circle c){
double dx = p.x- c.x;
double dy = p.y- c.y;
double distance = math.sqrt(dx*dx + dy*dy);
return distance < c.radius;
}
Now, if we give somebody the above function, he is less likely to make mistakes. Here, I equate data abstraction to:
Reduce and make meaningless parameters meaningful
Instead of passing around simple pieces of data, now intelligent things that know thyself are flowing through our program. Also, notice that our program in fact grew in size. Abstraction is more of a better organization of code than simple deletion.
The next logical leap is that the function "contains" is too wordy. How might we be able to break it down further? Notice that the word "contains" tells one whether something is within another thing. What we are doing in the function is more than that. We calculate the distance between two points, then we ask if the distance is less than the radius. The calculation of distance is not "contains". We might be able to take it out like this:
boolean contains(Point p, Circle c){
double d = distance(p,c.centre);
return d<c.radius;
}
double distance(Point a, Point b){
double dx = a.x- b.x;
double dy = a.y- b.y;
return math.sqrt(dx*dx + dy*dy);
}
Now both functions do exactly like what they are named after:
- distance() returns the distance
- contains() returns true or false
This is functional abstraction, to which I say:
Reduce and make meaningless lines of code meaningful
The idea of encapsulation comes about because our code is like a naked man. We might want to put on some clothes for two reasons, preventing people from seeing and touching.
In the above example, we use two numbers to represent coordinates. But, someone could change the representation to using an array. This is the knowledge that is too low level and dangerous because it is subjected to changes.
Much like we have pants for our lower body and shirts for our upper body, we package data into classes in Java. One crucial question is where do we package the functions that work on these data. If we remember that we model things like real-life objects, then we can answer the question by saying that the function can be stored with the one that does the action or the one that receives the action, or both.
The reason why we avoid having it in both places is obvious:
- unnecessary code
- prevent inter-dependence
Hence, most of the time we should put relevant functions in reasonable classes. If you coded a function call "contains", it might make more sense to put it within the Circle class since a circle contains a point.
class Point{
double x;
double y;
double distance(Point b){
double dx = this.x- b.x;
double dy = this.y- b.y;
return math.sqrt(dx*dx + dy*dy);
}
}
class Circle {
Point centre;
double radius;
boolean contains(Point p, Circle c){
double d = p.distance(c.centre);
return d<c.radius;
}
}
The second part of the story is preventing contact. In Java, we have access modifiers such as public and private to disallow the modification of values of instance variables in one class from another class.
class Impt {
private int x;
void changeSelf(){ // acceptable
x=1;
}
void changeOther(Impt p){ // acceptable
p.x = 1;
}
}
class Client {
void change(Impt p){ // illegal access
p.x =1;
}
}
One note on the private access:
It means class-level access, not object-level access. Hence, a function in a class can access the internals of an instance of the same class, even when marked as private. You can also think of it as objects of the same type are both implementer. This is not a client implementer relationship. Hence, there is no abstraction barrier within the two implementer. See the stack overflow post for more:
class Person { private BankAccount account; Person(BankAccount account) { this.account = account; } public Person someMethod(Person person) { //Why accessing private field is possible? BankAccount a = person.account; } }
Please forget about the design. I know that OOP specifies that private objects are private to the class. My question…
The last two concepts are somewhat new to me and hence I would like to pen down my thoughts on each of them.
Tell-Don't-Ask
Tell an object what to do, rather than asking an object for data and acting on it.
boolean contains(Point p, Circle c){
// tell a point to give me distance
double d = p.distance(c.centre);
return d<c.radius;
}
// vs
boolean contains(Point p, Circle c){
// ask a point to give me its x and y values so that I can
// calculate the distance
double dx = p.x- c.x;
double dy = p.y- c.y;
double distance = math.sqrt(dx*dx + dy*dy);
return distance < c.radius;
}
It is important to understand this concept in the context of object interaction. Following the above example, the method "contains" in the Circle class will not ask for coordinates from the Point and calculate it. It will much prefer the distance to be calculated by the Point class and returned for comparison.
Immutability
void methods that mutate states should be avoided
Point setX(double x){
// does not mutate the original point
return new Point(x, this,y);
}
// vs
void setX(double x){
// mutates the original point
this.x = x;
}
This is for the sake of consistency in testing.
To me, the two principles basically discourage the use of getters and setters. I think there is probably some situation where setters and getters are useful.
Summary
Abstraction
- Data abstraction
- Function abstraction
Encapsulation
- Packaging
- Information hiding
Principle of Good OOP Design
- Tell-Don't-Ask
- Immutability
Top comments (5)
I like the way you have been able to talk about each point in detail and bring out the gist of it.
Keep going indeed Liu.
Nice series, I'm going to read all of them :)
Personally, I'm not a fan of that concept, take a look at this part of the video from Robert C. Martin, just 35 seconds (51:10 - 51:45)
Hey,
I watched the talk and found some interesting ideas that you are pointing at, and I agree with them.
And
In a sense, I feel that OO does model the code based on real-life objects, just that those objects are now more independent that they really are. It is definitely not a 100% copy-paste from the real world.
Also, I like the part on the open-close principle.
Like what was mentioned in the video, we are hardly able to predict what clients/consumers may want now and in the future. Even if we can safeguard ourselves from possible changes that we are able to predict at the point of coding, (e.g. that clients may want to add an Oval in our classes of shapes), there are other aspects which will eventually force us to change our design. In this case, open-close principle is simply our desire and our hopes and dreams. He mentioned that we can try to release early and get a sense of consumer thinking behind a product and therefore implement accordingly. That might work to an certain extent.
Like most things in life, human beings are the source of failure. 😂
Thanks for sharing!
Yea! I completely agree :)
Your series are really good, hands down, are design patterns in your list of things to write about?
Thank you for your kind words 😄
I did plan to write about design patterns and but maybe not anytime soon due to my other commitments. However, my primary source of information on design patterns is this website(Do check it out if you haven't).
Cheers!