The building blocks of an OOP application are classes. Think about it this way: The OOP application is very much like the house you live in. The strength of the house depends on the strength of the foundation on which it is built.
The same is true for Object-Oriented Programming. If you don’t build a strong foundation, you’ll soon find yourself with the structure fallen out of place. Poorly written classes can one-day lead to a difficult situation when an application is running.
On the other hand, well-designed and well-written classes can speed up the coding process and reduce the number of errors.
Here are three basic rules your code should follow:
- Maintainability
- Extensibility
- Modularity
You may find yourself in a difficult situation when you ask a question about whether any particular code depicts the above qualities or not. So in this article, I’m going to list ten basic principles of class design that a programmer should consider when writing code in an object-oriented manner. Most of them are part of SOLID, but some are just stand-alone rules to follow when writing high-quality code.
DRY (Don’t Repeat Yourself)
Quite a simple principle, the essence of which is clear from the title: “Do not repeat” in order to use abstraction in your work. If the code has two repeating sections, they must be combined into one method. If the same unchanged value is used more than once, it must be converted to a public constant.
We get a few benefits using the DRY principle: We write less code, which takes less time and effort, makes the code easier to maintain as well as reduces the chances of bugs.
Open/Closed principle
This principle can be easily remembered by reading the following statement: “Program entities (classes, modules, functions, etc.) should be open for expansion, but closed for modification.” In practice, this means that their behavior can change without modifying the source code. Once you RELEASE the code, you should take steps to not touch it. If you add functionality to a class and change the code, there is a high chance that you will introduce a new bug into existing functionality.
This principle is important when changes to the source code require additional revision, unit testing, and other procedures. By the way, the open-closed principle is one of the SOLID principles.
Single Responsibility Principle
Yet another principle from the SOLID set. It states that “there is only one reason leading to a change in a class“. One class solves only one problem. It may have several methods, but each of them is used to solve a common problem. All methods and properties should only serve this goal.
The value of this principle is that it weakens the connection between the individual software component and the code. If you add more than one functionality to a class, this introduces a link between the two functions. Thus, if you change one of them, there is a great chance to spoil the second one. This means an increase in testing cycles in order to identify all the problems in advance.
Encapsulation
The purpose of encapsulation is to help you (and others) safely make changes to one part of your program without damaging other parts. Programs that use encapsulation are also easier to understand.
In Java, one of the main ways to achieve encapsulation is to create classes with private member variables. Keep the member variables/methods private by default, and this will provide Encapsulation. If you will need access from other classes, keep increasing accessibility step by step.
DIP (Dependency Inversion Principle)
Let’s look at the definition of the principle of dependency inversion from Wikipedia:
“In object-oriented design, the dependency inversion principle is a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.”
To make things clearer, consider the following example:
[code language=”java” firstline=”0″]
public class Bird {
private Flier flier;
public Bird() {
flier = new Flier();
}
}
[/code]
The problem here is that the class Bird
depends on the concrete class Flier
. For the sake of extensibility, reusability or testability – there may be a task to separate them. As stated by the principle of dependency inversion, an intermediate abstraction should be introduced between the Bird
and Flier
class.
[code language=”java” firstline=”0″]
public class Bird {
private IFlier flier;
public Foo() {
flier = new Flier();
}
}
[/code]
Where’s the inversion? The basic idea, without the understanding of which it is impossible to answer this question, is that interfaces belong not to their implementations, but to the clients using them. The name of the interface IFlier
is misleading and makes you consider the IFlier
+ Flier
combination as a whole. At the same time, the true owner of IFlier
is the Bird
class, and if you consider this, the direction of the connection between Bird
and Flier
will really be reversed.
Interface Segregation Principle
This one says that you should only make the user implement whichever methods they intend to use.
A problem commonly found in interfaces – a bundle of functions having a name and defined inputs/outputs but no code – is that the user needs to implement them, or else it won’t compile. A bad interface is one that has too many functions for what the user wants. If you only intend to use a handful of relevant functions but there are 100 others, that’s 100 “do-nothing-code” snippets that need to be added to your code. Instead, you should split this up into its most relevant groups.
If you are a mechanic that only offered two services – a complete overhaul of the whole car top bottom, or nothing at all – nobody would go to your shop. Instead, you should offer brake packages, exhaust packages, oil changes, etc.
When designing interfaces you should always ask yourself this question: “Do I really need all the methods on this interface? If not, how can I break them into smaller interfaces?”
Composition over Inheritance
Let’s say we have multiple types of objects that must have some common functionality.
The stupidest way to deal with sharing functionality across those types is to copy and paste the code. Make one mistake in pasting, and you break it. What’s worse, every time you want to make a change to that functionality, you have to make it in multiple places.
A much better way is to use inheritance. You have a parent class containing the shared functionality, and everyone who wants to share it inherits that class. The issue with this is that inheritance is pretty clumsy when you have more than one set of shared functions. All flying beings must be able to fly()
, and all beings should also be able to breathe()
. So is the bird a flying object or a breather? In some programming languages, it’s not even possible to have multiple parents, and even if it is possible, it gets super messy and confusing.
Composition is a much better way. Instead of having “beings”, you just have an object called breather
. If something needs to breathe, you give it a breather
(instance of a Breather
class) and if it also needs to be able to fly, you give it a flier
.
[code language=”java” firstline=”0″]
class Flyer {
public void fly() {
System.out.println("Look Ma, I’m flying!");
}
}
class Breather {
public void breathe() {
System.out.println("In and out…");
}
}
[/code]
[code language=”java” firstline=”0″ title=”Inheritance:”]
class Bird extends Flyer, Breather { //not possible in JAVA
// can fly(), because it inherits Flyer
// can breathe(), because it inherits Breather
}
[/code]
[code language=”java” firstline=”0″ title=”Composition:”]
class Bird {
private Flyer flyer;
private Breather breather;
public Bird(Flyer fl, Breather br) {
flyer = fl;
breather = br;
}
public void fly() {
flyer.fly();
}
public void breathe() {
breather.breath();
}
}
[/code]
Liskov Substitution Principle
If we have a rectangle class:
[code language=”java” firstline=”0″]
class Rectangle {
private int height;
private int width;
public int getHeight() {
return height
}
public void setHeight(int h) {
height = h;
}
public int getWidth() {
return width;
}
public void setWidth(w) {
width = w;
}
public int getArea() {
return height * width;
}
}
[/code]
Following statements should describe the behaviour of a rectangle: if we change the height, the width should remain unchanged, and the other way round, if we double the width/height – its area will be doubled as well, and setting the width then the height (or the other way round) it will give you an area equal to height times width.
Because a square is a special case of Rectangle
, we should be able to subclass Rectangle
to make a Square
, it seems like a perfectly reasonable subclass to define. Except that width and height can’t be set independently, they both change the side of the square, and the area is height * width.
Now let’s revisit our earlier premise about a Rectangle
and how our Square
fits in: whenever we change Square
‘s height or width the other dimension changes as well, doubling the height or the width increases fourfold the area. If we change the width then the height, the area will be equal to height times height, and vice versa, setting height then the width will result in an area equal to width times width.
Even though a Square
is a special case of a Rectangle
, what the Liskov Substitution Principle says is that since the observable behavior of a Square
is wildly different from that of a Rectangle
, the Square
class mustn’t be treated as a subtype of a Rectangle
.
Program to an Interface, Not an Implementation
Here the name speaks for itself. The application of this principle leads to the creation of a flexible code that can work with any new interface implementation.
You should use the interface for:
- the type of variables;
- the return type;
- the type of method arguments.
An example is using SuperClass, not SubClass.
The most important point comes from a program design perspective: “programming to an interface” means focusing your design on what the code is doing, not how it does it.
In programming, the classic example is the database. You can write a simple database interface that does a few operations (create, read, update and delete data). Then you can write a class for MSSQL, Oracle, MySql, Postgres, etc. When I write my program, I’ll just use the IDatabase
interface. This way I can easily change the class without changing any of my code. This improves flexibility and maintenance. Try to use it whenever you can.
KISS – Keep It Simple, Stupid
The KISS principle is quite common in other fields besides programming. Necessity is the mother of invention, and simplicity is the level of quality. Avoiding complexity is quite an important factor to follow when it comes to programming. A simple code will take less effort to write and will prevent from running into bugs. Along with that, the code will also be quite easy to modify and maintain.
In order to achieve KISS, try to write simple code. Think of many solutions for your problem, then choose the simplest one and transform that into code. Whenever you find some lengthy code, divide it into multiple methods — right-click and refactor in the editor. Try to write small blocks of code responsible for one thing only.