How to Write Great Software

 

This article is about the characteristics that distinguishes great code form good code. Characteristics that distinguish great Software Engineering from mediocre Software Engineers.

This article is about how to make sure the software you write is truly great, amongst the best in the world, and more meaning than just the sum of its parts and an executable piece of binary.

OK, so these are the characteristics of great software:

  • Structure and Dependencies
  • Abstraction and Modularization
  • Ease of Debugging
  • Ease of Maintenance

Before we get talking about the above specifics we'll first list out the characteristics of bad, mediocre, boring and generally shit software. These  are:

  • Scattered Structure
  • Data Structure Driven Development
  • In-line Coding
  • Pattern Driven Coding
  • Code that does not include all of the items in the first list.

 Structure and Dependencies

The structure of great software has to reflect the structure of the requirements. And this almost always means that its structured from the top to the bottom. So the top most layer (such as the user interface - the GUI), defines the structure of the services provided by the layer below (generally the application layer).

This is obvious when you think about it. When you are defining a new applicaiton, a change or extension to an existing appliation, you do so by defining the changes to the user interface or, if you are working in an Agile way, then via the Stories. So for example, the requirment:

As a User I want to search for Customers by their Postcode

Kind of implies that search functionality will be provided in the GUI. This in-terns means that there should be a service interface method which supports this functionality:

/**
* Returns a JSON array of Customers or and empty array if none are found.
*/
public interface CustomerSearch
{
   String findCustomers(final String postcode);
}

So the definition of the search functionality is firstly defined by the requirments, then defined by the GUI and then implemented by some layer below.

Now lets look at the layer below. This we will call the applicaiton layer (just to keep things simple we will defined our appliation as having a 3-tier architecture - a presenatation tier, a logic or appliation tier and a data tier).

So the specification of the customer search service is defined in the GUI tier of our application (uk.co.jeeni.gui.app.definitions). This little package structure means that it belongs in the GUI teir, and the appliation definitions package.

So here is our service definition:

package co.uk.jeeni.gui.app.definitions;

public interface CustomerSearch
{
   String findCustomers(final String postcode);
}

This service definition needs to be implemented in the appliation tier of our system (uk.co.jeeni.application.*), so the package we will choise for the implementation class will be  uk.co.jeeni.application.gui.impl.

So this implementation class needs to do a search on the database and return the results as an array JSON Customer objects. We'll forget about the detail of this and just focus on the interaction between the layers.

This implementation means that the appliation layer needs to invoke a service on the database tier. This service on the data tier will will return domain objects rather than JSON strings as JSON is primarally a web-view technology (a JavaScript object), so our implementation will include converting from the database persistance format of a Customer to our custom domain application/business representation of a Customer object.

Thus the outline of the system at the application level looks like this:

package co.uk.jeeni.appplication.gui.impl;

public class CustomerSearchImpl implements CustomerSearch
{
   String findCustomer(final String postcode)
   {
      final List<Customer> foundCustomers = database.searchCustomer(postcode);
   // Now we conver the domain Customers into a JSON array and...

      return jsonFoundCustomers;
   }
}

So now we have a definition of a database service shich takes in the postcode does the database search and returns a list of Customer objects. So this service we can specify as:

package co.uk.jeeni.appplication.database.definitions;

public interface CustomerSearch
{
   String findCustomers(final String postcode);
}

Notice, just as before, that this service is specified at the application level. This is because the application level is the component that requires this service. It requires this service from the database in order to satify the service it is responsible for implementing in order to satify the demands made on it by the presentation tier.

Now to keep things simple, because reading this sort of stuff can get prity dull at the best of time (hence all those patronising books with jokes in that don't sell anymore), we'll skip through the implementation of this service except to say that it would be implmented within the data access teir jusing JPA or JDBC, or whatever.

So to a diagram. Diagram a great because they give a fantatic overview of what is really going on.

WritingGreatSoftware4

The Importance of Structuring Applications this Way

Now here is the true secrete why structuring applications and dependencies in this way is a vital charactoristic of writing great code.

Where and who changes the requirements? Generally changes will come from two areas. Either the users or the Product Managers, and both these groups of people work from the top down. They will update requirements by respecifing or wanting changes/additions to the GUI.

Such a change will lead to a change to the services required by the presentation tier of the application tier, and thus those services will need to be redesigned, which in turn results in changes to the services specified in the appliation tier, and so on down the stack. Consiquently, we have a structure which is in line with the forces of changed demanded by our customers, business and users. In otherwords we have an architecture that can respond repadly and even keep in line to market demands and changes. So to put is more plainly, this artichtecture enables you to stuff it to the competition quicker than the competition can keep up.

Changes Driven by Technical Developments

What about technical driven changes. What if the user base suddnly expands so fast that you decide to ditch Microsoft Access and go with something a bit more funky like MongoDB? So again this architecture comes to the rescue.

Because the dependencies of lower tiers (in this case the data tier), is dependent on the services specified from above (the application tier), then all we have to do is re-implement our co.uk.jeeni.appplication.database.definitions.CustomerSearch service as a MongoDB implementation and everything will work fine. Let me make this clear; So long as the service below implements the contract specified by the service above then we have a plug-n-play architecture where any implementation can be swapped out and replaced with a different technical implementation.

Abstraction and Modulization

Here we are talking about how our code is stuctured within each tier and within each class.

It is key charactoristic of great code that the code's abstractions are strcutrued at the same level of functionality. Lets clear this up with an example. Far to many people write methods that do more than a single thing. So they will write a method that says creates a  Customer, and that method will first check that the data is OK, then go on to log the timestamp of when and which user created the customer object, then it may send of an email to inform some manager that a customer has been added and then write the data to the database and log a sucessful insert to the system log.

So I wont bore you by writing out a complete example because you could find lots of them all over the place.

How such a method should look is like this:

public class CreateCustomerService
{
  public createCustomer(final Customer customer, final User user)
  {
    validate(customer);
    validate(user);

    createAttempLogEntry(customer, user);

    addCustomerToDatabase(customer);

    createSuccessLogEntry(customer, user);

    postEmail(customer);
  }

... implementation of methods above...
}

The important thing to take home form this example is that all the methods operate on the same level of abstraction. Even if the validateUser(user) method is only a couple of lines of code it should still be brought out into it's own method because this maintain sinergy with the level of abstraction we are looking for.

This make the code more readable and more modular because we can obviously reuse many of the method in different circumstances.

To Pass Parameters or to use Object State.

One of the questions the always come up from time to time is, is it better to pass parameters around method-to-method within an class or should you use object state/instance variables and invoke the methods without parameters (such that they operate on the object state).

The most meaningful answer I can give you for this is that if you pass them around then there is no need to rely on state, and thus it is easier to make these methods final static methods which lean themselve better to optimisation. Also passing parameters around clearly shows when reading the code what data the method is working with. This, to my mind, is a good enought reason in itself. The easier code is to read and understand, then the better the code is. It is as simple a test as that.

Ease of Debugging

This is one of the most important measurements of great code verses crap code, and generally under this heading, crap code comes from old C++ developers.

Let me give you two examples of code that does the same thing, and see if you an work out which one is easier to debug:

  LatLog location = LocationUtil.getLatLong(
      db.getCustomer("ABC").getAddress().getGetPostcode());

  int zoom = 0;
  if(location == null)
  {
    location = UK_LAT_LONG;
    zoom = 15;
  }

 

  final Customer customer = db.getCustomer("ABC");
  final Address homeAddress = customer.getAddress();
  final Postcode postcode = homeAddress.getPostcode();

  LatLog mapCenter = LocationUtil.getLatLong(postcode);
  int mapZoomLevel = 0;

  if(location == null)
  {
    mapCenter = UK_LAT_LONG;
    mapZoomLevel = 15;
  }

 So egnoring all the obvious bug errors such as if db.getCustomer("ABC") returns null, lets look at the major differences.

The most obvious and most anoying thing about the first code snippet is that the entire getting of the customer's location is done in a single line. If there are any faults with this line (such as a NullPointerException), the debugger or stack trace will only identify the line and not the partcular method. However on the second snippet, because the stacktrace will tell you the line number you will know at which point the problem occured.

Secondly the second snippit is easier to read simply because it's layed out neat and tidy. Thirdly, the layout of the second snippet gives us the oppertunity to give our variables proper names which indicate what they are for (Address homeAddress).

Our fourth and final point here, is the use of the final keyword. This is used to indicate that the variable will not change its value. So by looking at the second snippet you can instantly see that the only to modafiable variables are mapCenter and mapZoomLevel. This is great for debugging because once you are satified that the final variables have been assined correctly you do not have to worry about them getting reassined to some erronious value latter on. This becomes more obvious and important when you are working with large code bases that someone else wrote. At a minimum, if the coder used the finally keyword consistantly you know that they know what they are doing. When you meet coders who refuse to use the final keyword even after you've explained what the benefits are you can pritty much mark them down as bozos.

There are other benefits to using the final keyword which the code optimizers will pickup on and use them to optimise the code (for example final methods can be repeated and in-lined everywhere by the optimiser during profiling and compile time because they can never be polymorphic).

Throughts on Logging

Asyncroniously written logs during multi-user environments.

Ease of Maintanance

So the final of our great code tips in this artical (but by no means the final measurement per se), is code maintainability. Code maintainability is the most holies of holies when it comes to how good a developer is. A great developer will leave behind code that is as easy as it can be to maintain.

If you follow the rules above then your code will be fairly maintainable. The other issue we should look at is package naming and variable naming.

Variable Naming

We should neve use variable names such as Object o = ...; or List l = ...;. This sort of behaviour is the stamp of a real bozo. If you dont know what type of object your going to get back the say so: Object unknownType = ...; The only place where using variables which are not named by their purpose is in for(int i = 0; ...) loops. And that's only because it's become an ingrained bad tradition.

So remember to name your variables by the purpose or descrption of the data they hold (String name rather than String s, or int sum rather than int num). The same goes for class names, method names, and everything else that has a name. Including package names.

Package Naming

The technical purpose of package naming is to organise code into name spaces, modules and components. So we could quite happly have a package structure that reflects our architecture:

package uk.co.jeeni.presentation;
package uk.co.jeeni.application;
package uk.co.jeeni.data;

And there's nothing wrong with that. However, we also want our package names to be as meaningful as our variable names. So just as in our variable names we want our package names to read meaning into what they are for and what they contain. Firstly, lets look at some bad examples, then some good.

Please keep in mind that these are 'pacakges' which contain classes. The sub-packages do not.

package uk.co.jeeni.application;
package uk.co.jeeni.application.utils;
package uk.co.jeeni.application.report;
package uk.co.jeeni.application.functions;

So in this example we don't really know what each package contains. We know that these packages relate to the applicaiton tier, and that there are some untilities but we dont know what scope of for what these untilities are for. We can also see there is a report namespace and a function namespace but, what sort of reports are we talking about.

We can also guess by looking at this structure that each package will contain a fairly large number of classes, which will make maintainace more difficult. This is true bozo coding because whoever came up with this package structure is trying to be a pithy as possible and writing greate code is not about being pithy, its about writting code that is maintainable, debuggable and most of all, understandable.

So now lets look at some good package names:

package uk.co.jeeni.application.domain.objects;
package uk.co.jeeni.application.domain.objects.json.translators;
package uk.co.jeeni.application.external_services;
package uk.co.jeeni.application.reporting.definitions;
package uk.co.jeeni.application.reporting.pdf_impl;
package uk.co.jeeni.application.reporting.html_impl;
package uk.co.jeeni.application.reporting.excel_impl;
package uk.co.jeeni.application.functions.graphing;
package uk.co.jeeni.application.functions.financial;
package uk.co.jeeni.application.functions.geographical;

The above package names are very descriptive about what they contain and it is clear that sub-packages will probably not contain classes. In other words we have devided our architecture along logical and structural lines. Also note the use of the underscore '_' to give clarity to what that package contains and to restrict the temptation to insert classes at levels that are not intended. Also note that the underscore is the only way we can introduce type of camal casing in our package naming structure. A technique which is useful in class names, method names, variable names, and is also useful in package names.

Also we have broken down the functions and reporting into individual areas. Consiquently there is much less room for vagueness and confusion than in the first example. This is great stuff that will help with maintaining code long after the person that wrote the code has moved on the bigger and greater things.

And just as with a well named method, class or variable, because they are specific they will only contain a few specific classes. Great coding.

Also, if we remain consistant about our naming convensions then we will know that uk.co.jeeni.application.reporting.definitions will contain the interfaces which are implemented in the various uk.co.jeeni.application.reporting.definitions.*.impl packages. Again, making maintiance easier.

Summary

All the above rules and tips help make a great coder, but a great coder must always change with the times and be willing to be flexible.

Now for a bit of carear advice. Only about 5% of coders are great coders so if you insist on writting great code in your company and you not in a sceniour position you will probably find that your head is above the parapit quite often so be prepaired to be evangelical about your organisation writting great code, and also, if your superiors are bozos, then be prepaired to change jobs. You will be amazed at how many managers are bozos.

As Guy kawasaki said, "In most organisations, the higher you go the thinner the air. And the thinner the air the more difficult it is to support intelligent life." And you'll be supprised how quickly the air gets thin once you leave the keyboard and the codebase.

 

 

 

 

 

Add comment