
This post is about classes and objects and I will talk about things like keeping classes clean and working with them cleanly. Also, before starting the discussion, we should understand the difference between real objects and class-like data structures. In the next step, I will go to the rules of working with object-oriented programming and we will get to know the rules of SOLID and demeter. Finally, I'll move on to polymorphism, except this time I'll focus specifically on classes.
Naturally, in this post, we are not going to teach object-oriented programming, and you are expected to know how to work with classes. Our goal is to write human-readable code.
Difference between objects and data structures
One of the first fundamental concepts for writing clean code in object-oriented programming is to understand the difference between real objects and data structures.
A real object hides its internal content (such as properties) and only allows us to access them using a public API (multiple methods). We do not mean only getter
and setter
methods by these few methods or public API, but all the methods that do a real job are part of this section. On the other hand, data structures, which are sometimes called data containers ) are also known as simple objects that display their content in a public way and we have almost no public API or methods to work with them.
So if you're using object-oriented programming, real objects are very important and usually contain the core logic of your program. On the other hand, there is no special logic in data structures, but we only use them to maintain and store data temporarily so that we can pass the data we want to different parts of the program. Also, when talking about real objects, there is a concept called abstraction over concretion. This concept means that we prefer abstraction over concreteness. what does it mean? That is, in real objects that are made of classes, we have methods, and these methods perform certain operations, and we do not care how these operations are performed (this is an abstract concept), if there are almost no methods in data structures. they don't have, so the data is available to us in concrete or "explicit" form and we work directly with this data.
For example, consider the following class:
class Database {
private uri: string;
private provider: any;
private connection: any;
constructor(uri: string, provider: any) {
this.uri = uri;
this.provider = provider;
}
connect() {
try {
this.connection = this.provider.establishConnection(this.uri);
} catch (error) {
throw new Error('Could not connect!');
}
}
disconnect() {
this.connection.close();
}
}
This is a class for a database and by using it we can create a real object because the object created from this class has its content hidden inside itself and then by using several methods it allows us to perform operations. gives a special For example, the connect
and disconnect
methods are high-level methods and perform important tasks, but when we work with an object made from this class, we do not know what is going on behind the scenes and how exactly these methods perform their tasks. but we only know that the connect
method connects us to a database. This problem is called abstraction.
On the other hand, we have objects and classes that are basically data containers:
class UserCredentials {
public email: string;
public password: string;
}
As you can see, there is no method at all and to work with it, we have to work directly with its data such as email
and password
.
You may ask why this difference is important? If you consider these two types of objects as one, you will usually write codes that will not be clean and standard. Whether your entire program is written in an object-oriented way or whether it is a combination of procedural and object-oriented programming, you should still pay attention to this issue. For example, consider the Database
class that I showed you above. If we want to use this class, our work will be easy and as follows:
const database = new Database('my-database:8100', sqlEngine);
database.connect();
database.disconnect();
The code is very clean and readable by anyone. Also, whenever there is a need to change the connect and disconnect logic, we simply go back to the class definition and edit its corresponding methods, and we will not need to edit the above code (calling connect
and disconnect
). Now, if we don't want to make a difference between real objects and data structures, we may write the properties of the Database
class as public
:
class Database {
private uri: string;
private provider: any;
public connection: any;
constructor(uri: string, provider: any) {
this.uri = uri;
this.provider = provider;
}
// Other Codes
For example, I have made the connection
property (our connection to the database) public
. In this case, the connection
property is also available outside the class. For example, we may want to close the connection to the database in another part of the program as follows:
const database = new Database('my-database:8100', sqlEngine);
database.connect();
database.connection.close();
As you can see, I have directly accessed the connection
from the database
object and called close
on it. The problem is that in the first mode, which was the standard mode, we performed our operations by calling the methods, and when we needed to edit, we only had to edit the class definition, but what about this case? If we are going to directly access the content of the object like the code above, we will encounter many problems.
For example, if later the name of close
is changed to something else like shutDown
or disconnect
or any other name, all our code will be broken and not only we have to edit the class definition but also we have to go to each part where we have closed the connection and then Edit it. Naturally, this work is not standard at all! In addition, database.connection.close
is not readable. A person who wants to use our code must suddenly learn what connection
is and where it comes from, and why we didn't have it when connecting (the connect
method) and dozens of other questions that will confuse other developers.
This is why it is so important to understand the difference between real objects and data containers. We cannot treat both in the same way.
Polymorphism in classes
Polymorphism means methods or objects that have a fixed shape and name in appearance (for example, they are called in the same way), but in practice, based on how you use them, it behaves completely differently to the point where it seems as if we have used another method or object.
The problem here is that polymorphism is usually active in the field of classes and objects and is less explained in the field of methods. For this reason, in this section, I want to pay special attention to polymorphism in classes. First, I have prepared a simple class for you:
type Purchase = any;
let Logistics: any;
class Delivery {
private purchase: Purchase;
constructor(purchase: Purchase) {
this.purchase = purchase;
}
deliverProduct() {
if (this.purchase.deliveryType === 'express') {
Logistics.issueExpressDelivery(this.purchase.product);
} else if (this.purchase.deliveryType === 'insured') {
Logistics.issueInsuredDelivery(this.purchase.product);
} else {
Logistics.issueStandardDelivery(this.purchase.product);
}
}
trackProduct() {
if (this.purchase.deliveryType === 'express') {
Logistics.trackExpressDelivery(this.purchase.product);
} else if (this.purchase.deliveryType === 'insured') {
Logistics.trackInsuredDelivery(this.purchase.product);
} else {
Logistics.trackStandardDelivery(this.purchase.product);
}
}
}
This is a class called delivery
, which has a property called purchase
, has a constructor
, and finally has two methods, the first one (deliverProduct
) is responsible for sending the product based on the type of delivery, and the second one (trackProduct
) is responsible for tracking the sent product. We have three types of shipping for our products: normal shipping (called in the else
section), express or fast shipping, and insured shipping.
If we want to use polymorphism in this example, instead of one class, we should divide our classes into several different classes so that each class is specifically responsible for doing a part of the sending process. For example, we will have a base class for delivery, which has shared logic between all shipping methods, and then we will have a separate class for each specific type of shipping and tracking products:
class ExpressDelivery {}
class InsuredDelivery {}
class StandardDelivery {}
Why did we do this? If you look at the initial code, you will notice that the method of sending the goods plays an important role in this code and everything is based on the shipping methods, so dividing the code based on the shipping method will be an ideal option. Now, for each class, we must have a product sending method and a product tracking method. For example, for the express
class, we say:
class ExpressDelivery {
deliverProduct() {
Logistics.trackExpressDelivery(this.purchase.product);
}
}
The problem is that currently the ExpressDelivery
class is an independent class and there is no longer a feature called purchase
in it, and of course we do not have access to it. To solve this problem, we must use inheritance. It means that the ExpressDelivery
class is a child of the Delivery
class (basic and main class). If you look at the Delivery
class, you can see that the purchase
attribute is private
, which means it is only available in the Delivery
class itself. I change it to protected
so that it can be accessed in child classes as well:
class Delivery {
protected purchase: Purchase;
constructor(purchase: Purchase) {
this.purchase = purchase;
}
// Other codes
In the next step, we extend
the ExpressDelivery
class:
class ExpressDelivery extends Delivery {
deliverProduct() {
Logistics.issueExpressDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackExpressDelivery(this.purchase.product);
}
}
As you can see, we have two methods in this class, the first one (deliverProduct
) to send the product and the second one (trackProduct
) to track it. No more if conditions. All that's left is to repeat this for the other two classes:
class ExpressDelivery extends Delivery {
deliverProduct() {
Logistics.issueExpressDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackExpressDelivery(this.purchase.product);
}
}
class InsuredDelivery extends Delivery {
deliverProduct() {
Logistics.issueInsuredDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackInsuredDelivery(this.purchase.product);
}
}
class StandardDelivery extends Delivery {
deliverProduct() {
Logistics.issueStandardDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackStandardDelivery(this.purchase.product);
}
}
Now that we have three special classes for sending and tracking all kinds of goods, we have to go to the basic class (Delivery
) and delete both of its methods because we don't need them anymore. With this account, all the codes of this file will be as follows:
type Purchase = any;
let Logistics: any;
class Delivery {
protected purchase: Purchase;
constructor(purchase: Purchase) {
this.purchase = purchase;
}
}
class ExpressDelivery extends Delivery {
deliverProduct() {
Logistics.issueExpressDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackExpressDelivery(this.purchase.product);
}
}
class InsuredDelivery extends Delivery {
deliverProduct() {
Logistics.issueInsuredDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackInsuredDelivery(this.purchase.product);
}
}
class StandardDelivery extends Delivery {
deliverProduct() {
Logistics.issueStandardDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackStandardDelivery(this.purchase.product);
}
}
You are probably asking, so what are the if conditions? How to determine which class we should instantiate and call its methods? You should check the sending method in the part of the program where you want to use these classes. We are defining these classes here, and naturally, in real programs, the place of defining and using the classes are separate.
For example, suppose we want to use these delivery classes in a part of the program, naturally we have to create an example of them and work with that object. Before making the above changes, we used to make changes as follows:
const delivery = new Delivery({});
delivery.deliverProduct();
Currently, it is not possible to write such a code because there is no longer a method named deliverProduct
in the main class of Delivery
. How you use these classes is highly dependent on your program and your files and your personal decisions, but if I want to give you a simple example I'll use this example:
let delivery: Delivery;
if (purchase.deliveryType === 'express') {
delivery = new ExpressDelivery(purchase);
} else if (purchase.deliveryType === 'insured') {
delivery = new InsuredDelivery(purchase);
} else {
delivery = new StandardDelivery(purchase);
}
delivery.deliverProduct();
In this example, based on the type of user's purchase (Express
or insured
or Standard
), we set the value of the delivery
variable. In addition, these codes are written in Typescript language, so don't be surprised by the presence of two dots to determine the type of delivery
variable at the beginning of the codes. These are minor issues and have nothing to do with clean code. Finally, I have called the delivery
method. Although this simple example should show you the generality of the work, but for more practice, let's rewrite all the codes of this file in a clean way, assuming that we want to use these classes in this same file.
In the example above, you will get an error that the deliverProduct
method does not exist at all on the delivery
object, which is correct. My goal in rewriting is to clear these errors. To solve this problem, we can use TypeScript interfaces. Example:
interface Delivery {
deliverProduct();
trackProduct();
}
I have defined the general structure of Delivery
using this interface
. In the next step, I change the name of the Delivery
class to DeliveryImplementation
(meaning the implementation of Delivery, which is our interface) so that the naming is more accurate:
class DeliveryImplementation {
protected purchase: Purchase;
constructor(purchase: Purchase) {
this.purchase = purchase;
}
}
In the next step, we must extend
this new class (we have changed the name of the parent class, so the codes will be confused) and then use the interface for child classes with the keyword implement
:
class ExpressDelivery extends DeliveryImplementation implements Delivery {
deliverProduct() {
Logistics.issueExpressDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackExpressDelivery(this.purchase.product);
}
}
class InsuredDelivery extends DeliveryImplementation implements Delivery {
deliverProduct() {
Logistics.issueInsuredDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackInsuredDelivery(this.purchase.product);
}
}
class StandardDelivery extends DeliveryImplementation implements Delivery {
deliverProduct() {
Logistics.issueStandardDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackStandardDelivery(this.purchase.product);
}
}
At the end, you can use the same if
conditions that I showed, but usually in real programs, these conditions are used several times, so it is better to make it in the form of a separate function (a factory function):
function createDelivery(purchase) {
if (purchase.deliveryType === 'express') {
delivery = new ExpressDelivery(purchase);
} else if (purchase.deliveryType === 'insured') {
delivery = new InsuredDelivery(purchase);
} else {
delivery = new StandardDelivery(purchase);
}
return delivery;
}
let delivery: Delivery = createDelivery({});
delivery.deliverProduct();
This function receives the user's purchase and then, based on the type of purchase, calls the correct delivery class and returns it in the form of the delivery
variable. Next, we can call the deliverProduct
method according to our taste and in the right place. I've passed an empty object to createDelivery
instead of an actual order because our code isn't real and we don't have a real purchase to pass, but you won't have this problem in your real apps. With this account, all our codes are as follows:
type Purchase = any;
let Logistics: any;
interface Delivery {
deliverProduct();
trackProduct();
}
class DeliveryImplementation {
protected purchase: Purchase;
constructor(purchase: Purchase) {
this.purchase = purchase;
}
}
class ExpressDelivery extends DeliveryImplementation implements Delivery {
deliverProduct() {
Logistics.issueExpressDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackExpressDelivery(this.purchase.product);
}
}
class InsuredDelivery extends DeliveryImplementation implements Delivery {
deliverProduct() {
Logistics.issueInsuredDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackInsuredDelivery(this.purchase.product);
}
}
class StandardDelivery extends DeliveryImplementation implements Delivery {
deliverProduct() {
Logistics.issueStandardDelivery(this.purchase.product);
}
trackProduct() {
Logistics.trackStandardDelivery(this.purchase.product);
}
}
function createDelivery(purchase) {
if (purchase.deliveryType === 'express') {
delivery = new ExpressDelivery(purchase);
} else if (purchase.deliveryType === 'insured') {
delivery = new InsuredDelivery(purchase);
} else {
delivery = new StandardDelivery(purchase);
}
return delivery;
}
let delivery: Delivery = createDelivery({});
delivery.deliverProduct();
[hive: @albro]
Comments (3)
https://inleo.io/threads/albro/re-leothreads-2pakvygd1 The rewards earned on this comment will go directly to the people ( albro ) sharing the post on LeoThreads,LikeTu,dBuzz.
Congratulations!
✅ Good job. Your post has been appreciated and has received support from CHESS BROTHERS ♔ 💪
♟ We invite you to use our hashtag #chessbrothers and learn more about us.
♟♟ You can also reach us on our Discord server and promote your posts there.
♟♟♟ Consider joining our curation trail so we work as a team and you get rewards automatically.
♞♟ Check out our @chessbrotherspro account to learn about the curation process carried out daily by our team.
🥇 If you want to earn profits with your HP delegation and support our project, we invite you to join the Master Investor plan. Here you can learn how to do it.
Kindly
The CHESS BROTHERS team
Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!
Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).
You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support.