Java is the oldest programming language that continues to grow in applicability. However you spin it, Java still offers flexibility, scalability and continues to power some of the most complex computing systems in the world.
Some try to build upon Java, attempting to create an alternative (like Scala and Kotlin) for certain use cases, such as android development. However, the ancient giant is not showing signs of retreat, and fortunately, Java is a rather gentle giant if you befriend it. How? By understanding that proficiency in the syntax and a nerdy shirt are not the only things you need to be a Java great developer.
There’s quite a difference between learning the core OOP principles of Java and being a good Java developer. The first is something anyone with access to the Internet can have in quite a short time. The second is a result of gained experience combined with a firm grasp of the design principles and conventions that make for efficient applications and code that is easier to maintain.
Note that some of the below concepts are relevant not just for Java but for most object oriented languages.
If you’re kinda new to Java, this short introduction to the most important core concepts of Java (and OOP in general) can serve as a great cheat-sheet on your path to more efficient and effective coding. If you’re a seasoned senior developer, this collection of the top 10 core Java software design concepts will be a wonderful reminder of what is important to remember.
1. DRY & Abstraction
I will only write this once: repetition is ineffective. Valid not only for Java, OOP or even programming, the DRY principle means simply “don’t repeat yourself”. To elaborate, it is stated as “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”
As you probably know, one of Java’s main strengths is its efficiency in reusing code and resources, and this principle is just one of several to reflect that. You can avoid repetition by using abstraction, one of the four core principles of OOP.
In Java this means using interfaces and abstract classes that do more than save you keystrokes. In addition to helping avoid repetition, abstraction aims to “hide” internal implementation details. Such consolidation makes for code that is easier to read and maintain.
Another OOP principle for reuse and efficiency is polymorphism that favors the use of a common interface instead of concrete implementation. As polymorphism literally means having one thing take many forms, it allows for a single common interface to be used for a general class of actions. The use of a common interface allows for easier and faster integration of new requirements or features without manual changes across the codebase.
In Java, polymorphism is implemented with method overloading and method overriding. When various methods with the same name are present in a single class, method overloading takes place. The various methods, though identical in name, are differentiated by the number, order, and types of their parameters. When a child class overrides the method of its parent, method overriding occurs.
No less important than the efficiency of your coding is the protection of data stored in fields within classes from system-wide access. This is achieved through encapsulation – a virtual capsule for data and code within the class itself.
In Java, encapsulation can be implemented by keeping class variables and methods private by default and providing access to them via public getter and setter methods. An example of fully encapsulated classes are Java Beans.
In addition, encapsulation also has the added benefit of easing the testing and maintenance of code and several Java design patterns make effective use of it.
If you’ve ever had a group of friends help you move house you fully understand the importance of delegation. You simply can’t do everything by yourself, so when you can – delegate.
We’ll not get into the debate between delegation and inheritance or decide which is better. That being said, Delegation is an abstraction mechanism centralizing method behavior, which is similar to inheritance done manually through object composition.
In essence, the delegation design pattern lets a delegator object unload tasks to a helper object. A common implementation of this principle is in event delegation where events are sent to handlers.
The key advantages to delegation as a design principle is the reduction in code duplication, and the ability to modify behavior with greater ease than with inheritance.
5. Program to an interface, not an implementation
This simple sentence, coined by Erich Gamma in the early 90s in Design Patterns: Elements of Reusable Object-Oriented Software (also known as the GoF – Gang of Four), is one of the most important design concepts in OOP in general and in Java in particular. And it is one you should apply in your code.
Programming to interfaces is a way to write classes based on an interface that defines the behavior of the object. Initially, it may seem like more work as you would first be creating an interface, then defining its methods and only then writing the actual class with the implementation.
However, using interface type on variables, return types of methods or argument type of methods in Java makes for flexible code that is also easier to test.
The last five in our list are the SOLID principles. They were first conceptualized by Robert C. Martin in his 2000 paper, Design Principles and Design Patterns, and were later enhanced by Michael Feathers, who also introduced us to the S.O.L.I.D acronym.
The main goal behind the SOLID principles is creating code that is easy to understand and maintain, with complexity reduced even as your application grows in size and scope.
6. Single Responsibility Principle
The single responsibility principle (SRP) states that a class should only have one responsibility and only one cause to change.
This makes perfect sense (and so requires little explanation) as having multiple reasons for change in a single class will affect output and functionality, and cause your code to become an unmaintainable mess.
7. Open Closed Principle
Though the name sounds very self-contradicting (how can something be open and closed at the same time?) it’s actually very simple and logical. The principle states that entities or objects should remain open for extension, but remain closed for modification.
In Java, this means that classes, methods or functions should be open to the addition of functionality, but closed to the alteration of previously tested and functional code. Except, of course, if one discovers it is not as tested and functional, and bugs need fixing.
8. Liskov Substitution Principle
The Liskov Substitution Principle (LSP), introduced by Barbara Liskov back in the late 1980s is perhaps the hardest one to explain in our list. Probably because its definition is very mathematical and somewhat confusing.
Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.
When translated to plain English the principle suggests that any class that is the child of a parent class (subclass) should be usable in place of its parent (superclass) without any unexpected behaviour. So an overridden method of a subclass must be able to accept the same input parameter values as the method of the superclass.
In my personal opinion, it is actually best explained by this meme:
9. Interface Segregation Principle
According to the Interface Segregation Principle (ISP) a client should not be forced to implement an interface which it won’t be able to use. In other words, larger interfaces should be split into smaller ones to ensure that implementing classes only “care” about the relevant methods.
Though this initially can sound like more coding work, it’s actually efficient because it reduces dependency complexity because it prevents the client from depending on a certain thing that it doesn’t actually need.
10. Dependency Inversion Principle
The principle of Dependency Inversion (DIP) states that high-level modules should not depend on low-level modules and instead both should depend on abstractions. In addition, abstractions should not depend on details but rather it’s the details that should depend on abstractions.
When following this principle, high-level modules remain independent of the low-level module implementation details.
Clean, efficient and maintainable code is not that hard to write, and is usually worth the effort. Not only because applying battle-tested software design patterns will make it easier for you to enhance and develop your application, but also because one day someone else might have to. And you never know just how far frustration with your coding practices may take them.