3R Principles of Programming
A review of the 3 R's of programming for cleaner, friendlier Java code.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
Programming is an Art. When we program, often we think that programs are a set of instructions fed to a computer for solving business use cases. Though it is true, programs also serve more than that in the real world.
Let us start with a code snippet that runs without any problems.
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class Shipment {
@Inject
CustomerDAO customerDAO; // @Inject will inject the implementation
@Inject
ShipmentDAO shipmentDAO;
@Inject
ShoppingCart shoppingCart;
@Inject
Inventory inventory;
public void ship(final Long customerId, final Long orderid)
throws CustomerException, OrderException, ShipmentException {
if (customerId == null)
throw new CustomerException("Customer id cannot be null");
Customer customer = customerDAO.getCustomer(customerId);
if (customer == null)
throw new CustomerException("Invalid Customer");
if (customer.type == 3 || customer.payment == 4
|| !customer.shipAddressProvided()) // ??
throw new CustomerException("Unauthorized Customer");
Order order = shoppingCart.getOrder(orderid);
if (order.associatedId != customerId) // ??
throw new OrderException("Unauthorized Order");
Address address = customer.getShippingAddress();
if (!Pattern.matches(ADDRESS_LINE1_PATTERN, address.getLine1())
|| !Pattern.matches(ADDRESS_LINE2_PATTERN, address.getLine2())
|| !Pattern.matches(ADDRESS_CITY_PATTERN, address.getCity())
|| !Pattern.matches(ADDRESS_COUNTRY_PATTERN,
address.getCountry())
|| !Pattern.matches(ADDRESS_ZIP_PATTERN, address.getZip())
|| !Pattern.matches(ADDRESS_PHONE_PATTERN, address.getPhone())) // ??
throw new CustomerException("Invalid Address");
final List<Error> errors = new ArrayList<Error>();
List<Product> products = shoppingCart.getProducts(orderid);
for (final Product product : products) {
Product invProduct = inventory.getProduct(product.getId());
if (invProduct.availabilty == false || invProduct.type == 1
|| invProduct.type == 3) { // ??
errors.add(new Error(customerId, orderid, product, OUT_OF_STOCK));
} else {
shipmentDAO.update(orderid, product.getId(),
new ShipmentCallback() {
public void statusUpdated(boolean status) {
// private method call
raiseNotification(READY_TO_SHIP, product,
status);
if (!status) {
errors.add(new Error(customerId, orderid,
product, UNABLE_TO_SHIP));
}
}
});
}
}
if (!errors.isEmpty()) {
throw new ShipmentException("Shipment Errors", errors);
}
}
}
Most of the time, developers get code written by someone else. We become accountable for adding new features, fixing bugs, or maintaining the code. The first step before starting any change is to understand the code at hand. Then we create unit tests to make sure new changes don’t break existing functionality. The next step will be the changes we want to incorporate.
In my opinion, if it takes more than a minute or two to understand a piece of code, then the code is complex. If you take even more time to understand, then the complexity of the code is high. The code snippet shown above is small and solves a simple shipment problem. I guarantee this code runs in production without any issues. The problem is not about the execution; rather how it is written. The style of writing code is equally important as running code in a machine. Interestingly most of the compilers or interpreters do code optimization. The complexity we saw in the code above is not due to business logic, but the time taken to understand it. It also makes it difficult to write unit tests. We may end up writing more, unnecessary, or duplicate tests.
The rest of this article takes you through the 3R principle that could be used to write better software and easier understanding. The 3R principle will try to avoid the complexity mentioned above.
What is the 3R Principle?
3R principle stands for Responsibility, Reusability, and Readability of any program we write. These principles are universal and can be applied irrespective of the programming paradigms in use, be it object-oriented or functional programming.
Let us see how these principles help us write good code.
Responsibility
Let me ask a very simple question, if we were planning to model a car “where would the “drive” method be?” Whenever I ask this question, most of the answers are “you can put the drive method in class Car”, because we can drive using a Car.
My answer to this question is drive should be in a class (like Person) who invokes the car and the car should have methods like accelerate, applyBrake, etc. Let us look into some simple pseudo code with drive methods in both approach.
|
|
The above question can be rephrased as “Who should have the responsibility of driving?”: the car or the person who is driving the car?
Responsibilities are related to the obligations of a method or class. For example, it is the responsibility of a person to drive safe and not car’s (don’t think about Self Driven Cars). Car’s responsibility is to control the car when accelerating, braking, etc.
The responsibility is one of the key principles we should follow. If the responsibilities are not clear in methods or classes, the code becomes complex.
Having proper responsibilities helps in writing unit test cases. For example, the test classes for class Car in new style will be testAccelerate(), testApplyBrake(). Like the test case name defines you are testing a single unit of work. This is always better than writing a single test case like testDriving() for a car. When you test driving, there are chances you could miss some test cases( like applyBrakes!!).
Let us now talk about a real world example to see how responsibility helps in designing and the problems that might arise by overloading responsibility.
Most of us are hired at a company for a specific role. Let us assume we are hired as a developer for a software company. During our initial contract, we are briefed about our roles and responsibilities and compensation corresponding to that. After some time, if we were asked to do something totally different from the role we agreed upon (with no extra benefits), what will be our take on that?
To illustrate, say our management thrust on us to a lead role in addition to development. Think about the issues we will have to face when we take dual or more responsibilities. We will be overloaded for sure. Other folks will have difficulties in understanding our actual role. We will need to maintain the state and status for both the roles.
The same holds true when we mix responsibilities of a class or a method.
Another geeky analogy, as a Java developer can we imagine a read method in Outputstream class? Can we imagine Outputstream.write(bytes), to validate the contents writes to the output stream?
The first place to start defining responsibility is to provide a proper name for the Class or Method.
Now let us look back to our example. The class name says "Shipment" and the method name is "ship()". Like the name explains this class is meant to do Shipment via ship method, but the class is handling Null Check, Customer Validation, Order Validation, Address Validation, Retrieves Products for that Order, Checks for Inventory, Updates shipment, and Notifies shipment.
This class is handling 8 functionalities. Do these functionalities belong to this class? This code works well but violates responsibility. Out of these 8 things the method did, only "Updates shipment" and "Notifies shipment" functionality actually handled shipment; others were either pre-process or protective code for shipment.
The goal is to remove such pre-process or protective code outside. Shipment should only handle shipment related processes. Note that sometimes we cannot remove these functionalities from our module due to business requirements. But you can always move such code outside the “Shipment” class.
The basic problem here is, we are trying to send raw data to the Shipment class and handling all the pre-processing and validating there. Instead, the best practice is to do all the preprocessing and validations outside and provide Shipment only the data it needs.
The new code will look like:
public class Shipment {
@Inject
ShipmentDAO shipmentDAO;
@Inject
Notification notification;
public void ship(final Long customerId, final Long orderid,
List<Product> availableProducts) throws ShipmentException {
final List<Error> errors = new ArrayList<Error>();
for (final Product product : availableProducts) {
shipmentDAO.update(orderid, product.getId(),
new ShipmentCallback() {
public void statusUpdated(boolean status) {
notification.notify(READY_TO_SHIP, product, status);
if (!status) {
errors.add(new Error(customerId, orderid,
product, UNABLE_TO_SHIP));
}
}
});
}
if (!errors.isEmpty()) {
throw new ShipmentException("Shipment Errors", errors);
}
}
}
Now you will notice the Shipment.ship() is handling only shipment. Few things to notice here are:
- We removed all pre-process code
- The actual database operation is done by the DAO class
- The actual notification is handled by “Notification” class.
- It throws only shipping exceptions
The Shipment class acts as an orchestrator for updating and notifying. That is what the class is meant for. Adding anything else to this class will overload its responsibilities.
Now the test for this becomes easier to write. We will write test cases only for the Shipping requirement. You can always mock the Database and Notification framework while unit testing and make sure the "ship" method works as expected.
Note this is not the final code. We can have many variations like changing the "ship" method to take only available products and not pass customer or order IDs.
The goal behind this principle is to make sure the class or the method should only perform its own responsibility.
Refer to the “S” in SOLID principle by Robert C Martin for more details.
Developing the code using such a pattern introduces the next principle, “Reusability”.
Reusability
Reusability is another principle that needs to be followed as part of programming. A process of identifying and designing components that can be reused in your application or across applications. This can be within your team or company or outside your company. Developing a reusable component is not trivial and we shouldn’t spend time just to make it reusable.
Consider the following line from our example: if(customerId == null) throw new CustomerException ("Customer id cannot be null");
You'll notice that there are lines in same methods which have the same functionality for different variables. These lines can be made simpler by moving the logic to a private method that can be reused wherever necessary.
This code can be re-written as “assertNull(customerId, "Customer id cannot be null");” and the assertNull method can be like:
void checkNull(Object object, String msg) throws CustomerException {
if (object == null) {
throw new CustomerException(msg);
}
}
This is the first step in identifying a reusable component. We should start small and gradually expand the horizon. In this case, the code looks reusable and generic. Which means we can write a utility class with static method to validate null and throw an IllegalArgumentException. We can make such classes a part of our application or make them public.
Note: For Java Developers, they already have libraries from Apache Spring which support such reusable validation methods.
Reusability is not just in the method level, it can be at the component level too. Let us take another use case in our example. If you have noticed the original code, we wrote “raiseNotification()" for sending the notification regarding the shipment. Later, it was changed to "notification.notify();". The notification itself is a big module, so instead of writing the entire code inside a private method or utility class, the notification module is converted into a reusable component, which can be used for different types of notification.
This principle is in conjunction with Responsibility principle. Developing such reusable components also help in testing better, both unit and functional. Each reusable components can be tested separately.
One of the key points while writing reusable component was to make sure the "Readability" of the class name, methods, and arguments are meaningful. More than that, if we are planning to open-source (either public or within your organization), the code should have clear "Readability". This introduces the third "R" principle, "Readability".
Readability
Most of the time, we see code that runs without issues on production, but takes a long time to add any new features to it or fix any bugs that might arise. One main reason for that is the lack of readability. Readability is one of the key ingredients in the art of programming. I believe in "Writing Code for Others to Understand", because most of the time somebody else will maintain our code.
This can be achieved in many ways. One of the common approaches we see is writing “inline comments”. This helps others know what the code is supposed to do. Another approach is providing meaningful names for files, classes, methods and variables. Another approach is providing spaces and indentation.
All these have to be applied for good readable code, However, I prefer a self-explanatory code over comments explaining the code's intent.
Readability is yet another principle, I should say this is a behavioral principle we need to use while programming.
Let us take few examples to understand how readability helps in programming. First, let us consider a real world example.
Readability is not only part of a code or logic that you write; it can be applied to the structure. It is very important to know the folders (aka packages or namespaces in languages) where each file belongs. If you are writing an API for weather report, it can be at <base_folder> /com/mycompany/weather/reports/api.
Once you decide the folder structure, the next step will be to give meaningful file, variable, and method names.
Consider the following snippet of code:
class Utils {
Employee process(String id) {
Response myresponse = WebUtil.get ("/employee/" + id);
Employee e = null;
if (myresponse.getStatus() == 200) {
e = myresponse.getEntity();
}
return e;
}
}
The above code is simple and easy to understand, but the problem in the code is Readability. Think about the call to get an Employee object, it will look like Utils.process("1234"). In terms of readability, this line doesn't say what the call or method will do. Now if we look more into the method we can notice it has two variables like "myresponse" and "e". Since it is just a few lines of code it is easy to understand what these variables are meant for. But if it is a long piece of code it will be very difficult as we read down the code. Each time we may need to visit the declaration part to understand. The last one is the usage of "200" constant. We may assume it is HTTP Response code 200 because the line above that says "WebUtil.get()", but this will cause problems for future readers. Say during the course of this project, if someone refactored "WebUtil" to "AppUtil", we can't relate 200 to anything. The following code makes more readable.
public class APIClient {
public Employee fetchEmployee(String id) {
Response webResponse = WebUtil.get("/employee/" + id);
Employee employee;
if (isResponseValid(webResponse)) {
employee = webResponse.getEntity();
}
return employee;
}
private boolean isResponseValid(Response webResponse) {
return webResponse.getStatus() == HttpStatus.OK;
}
}
Let us see the points we discussed above. The call to this method will be APIClient.fetchEmployee ("1234") and see the variables used within the method. They are "webResposne" and "employee" which is more meaningful than "myresponse". Finally the value "200", the new code not only makes the constant readable by introducing HttpStatus.OK, but also made it more readable by moving the condition check to "isResponseValid()".
Readability makes your code more maintainable and easy to understand.
Putting 3R Principles Together
The following section shows a sample code to read a file, convert to uppercase and print all the words.
The code without using 3R principle
BufferedReader reader = new BufferedReader(new FileReader(
"/tmp/mydata.txt"));
String line;
while ((line = reader.readLine()) != null) {
line = line.toUpperCase();
String[] tokens = line.split(" ");
System.out.println(Arrays.toString(tokens));
}
reader.close();
The code with 3R principles:
WordReader wordReader = new WordReader(new UppercaseReader(
new FileReader("/tmp/mydata.txt")));
String word;
while((word = wordReader.readWord()) != null){
System.out.println(word);
}
wordReader.close();
import java.io.IOException;
import java.io.Reader;
public class WordReader extends Reader {
private static final int SPACE = ' ';
private static final int NEW_LINE = '\n';
Reader reader;
public WordReader(Reader reader) {
this.reader = reader;
}
public String readWord() throws IOException {
StringBuilder builder = new StringBuilder();
int ch;
while ((ch = this.reader.read()) != -1) {
if (ch == SPACE || ch == NEW_LINE) {
break;
}
builder.append((char) ch);
}
return builder.length() == 0 ? null : builder.toString();
}
@Override
public void close() throws IOException {
this.reader.close();
}
}
public class UppercaseReader extends Reader {
Reader reader;
public UppercaseReader(Reader reader) {
this.reader = reader;
}
@Override
public int read() throws IOException {
return Character.toUpperCase((char) this.reader.read());
}
@Override
public void close() throws IOException {
this.reader.close();
}
}
By introducing WordReader and UpperCaseReader provides single Responsibility, Reusability, and Readability in all three classes.
Opinions expressed by DZone contributors are their own.
Comments