The DNA of Good Methods

Photo by Warren Umoh on Unsplash

The DNA of Good Methods

Dennis Groß's photo
Dennis Groß
·Feb 27, 2022·

15 min read

Subscribe to my newsletter and never miss my upcoming articles

Play this article

Table of contents

Methods are the bread and butter of Object-Oriented Programming (OOP). If done right, they turn your code into a language that fellow programmers can understand without the need of extensive documentation.

But what is the magic behind good methods?

Well, it takes years to master Object-Oriented Programming. But I’ll give you three simple rules that will help you to write better code instantly.

In short, here is the D-N-A of good Methods.

  • Don’t Repeat Yourself
  • Name Things Properly
  • Abstraction is Key

Don’t Repeat Yourself

“Don’t Repeat Yourself” or in short “DRY” is a core principle of software development. Keeping your code “DRY” means to eliminate as much code duplication as possible.

This does not only mean duplication of code in the literal sense. Two code passages do not have to be the same word by word. They might just use the same algorithm to achieve a similar goal.

But what do you do with two similar or even identical code snippets?

You extract the algorithm of the code snippet into an extra method. Something we also refer to as “helper methods”. And then you use the helper method in your program instead of copy & pasting the same code snippet all over again.

As a rule of thumb: “If you can reduce your overall amount of code lines by creating helper methods, then do so!”

Bad Coding Example

Enough theory, let us have a look at an example of literal code duplication.

The following Python code snippet performs HTTP requests using standard library tools.

class HttpClient:

def post(params: dict, payload: dict, url: str) -> str:

    encoded_params = urllib.urlencode(params)

    headers = {
        "Content-type": "application/x-www-form-urlencoded",
        "Accept": "text/plain"
    }

    json_payload = json.dumps(payload)

    connection = httplib.HTTPConnection(url)
    connection.request("POST", json_payload, encoded_params, headers)

    response = connection.getresponse()

    if response.status_code != 200:
        raise RequestError("Unexpected http response code {}".format(response.status_code))

    connection.close()

def get(params: dict, payload: str) -> str:
    encoded_params = urllib.urlencode(params)

    headers = {
        "Content-type": "application/x-www-form-urlencoded",
        "Accept": "text/plain"
    }

    json_payload = json.dumps(payload)

    connection = httplib.HTTPConnection(url)
    connection.request("GET", json_payload, encoded_params, headers)

    response = connection.getresponse()

    if response.status_code != 200:
        raise RequestError("Unexpected http response code {}".format(response.status_code))

    response_str = response.read()
    connection.close()

    return response_str

Take a moment to spot the redundancies in the code above.

You may notice that the only differences between the get and post methods are:

  • Post requests expect no response string
  • The request mode string “POST”, “GET” is different.

Let us take a moment and analyse why the code above is so bad.

Code Redundancy is Misleading

I bet any experienced programmer would scrutinise the post and get method carefully.

Why?

No one expects that you just copy & paste code (another term for reusing the same code passage somewhere else again). So, people will try to spot the differences in the two methods, post and get.

I always believe that people have a good reason when they write ugly code passages. So please don’t make me think so hard. Avoid code redundancy when it is avoidable.

Take your time and write clean and simple code.

You will spend most of your time as a professional software developer on reading, debugging and testing code. In fact, only a small fraction of your time gets spent on coding.

Invest the time to write the best code that you can. It will make the work for you and your colleagues easier in the future.

Fixing Bugs in Copy & Paste Code is Tough

I hid a small bug in the code snippet above. Take a moment and try to spot the mistake.

The post and get methods do not close the connection with connection.close() when HTTP request was unsuccessful (status code is not 200).

In programmer speak we say: “An edge case is missing in the code”.

Edge cases refer to problematic input parameters or side effects that are uncommon and can lead to errors in your algorithm. It is your job as a developer to find these edge cases and to add safeguards (countermeasures to prevent an error) in your code.

But that is not always easy. Let’s assume you write the code snippet above and your code goes into the productive system (the system that our customer uses).

A few weeks later, the customer opens a support ticket (a formal request for change from the customer, often reporting persisting issues in the software). The post method caused a ConnectionNotClosedError. The problem happens because we sent an HTTP post request to the HTTP server. The server responds with an HTTP status code 401 (the code 401 indicates that you made a request to an HTTP server without authentication).

You are currently on vacation and your colleague Matilda gets assigned to fix this bug (an issue in a program) and to create a bugfix (a code change that solves a bug).

Matilda uses a code debugger (a tool that lets you analyse the source code of your application in an interactive session) and finds the missing edge case shortly afterwards. She solves the bug by moving the close statement in front of the status code check.

... connection.close()

if response.status_code != 200: raise RequestError("Unexpected http response code {}".format(response.status_code))

But Matilda does not expect that you use the same code snippet from your post method also in the get method.

What happens now is unfortunately a very common issue in the world of software development.

Matilda creates a bugfix that fixes only the post but not the get method.

This is one of the reasons why redundant code is difficult to maintain (servicing existing code that used in productive systems through bugfixes and further refinements).

Bloating your Code Base

But there is more evil to code redundancy.

Using the same code snippet repeatedly leads to an increase in the total amount of code lines in your program.

And that is bad.

More code potentially yields more bugs. More code is also more difficult to read and understand.

Thus, your team will take more time to develop new features (functionality that you add to a program to satisfy a customer need) because it takes more time to read and understand the code base.

In fact, it will be even more difficult for you to understand your code. Your brain functions like a RAM (an ephemeral storage medium in your computer) stick. Most information doesn’t make it onto the hard disk and gets only stored to your brain's RAM for a limited amount of time.

And this is especially true for code. You write it today, you still understand it tomorrow. Four weeks later, you ask yourself what you did there, promise.😗

So make your own and the life of your esteemed colleagues easier and write code that is short and simple.

Cleaning the Example Up

Our refactored snippet utilises the OOP principles to improve the code. Please keep in mind that there is more than one way to clean up the code example from above.

The programming community created over the years solid principles for good code, but much remain subject to personal preference.

class HttpStatusCodes: SUCCESS = 200

class HttpRequestMethods: POST = "POST" GET = "GET"

class HTTPRequest:

REQUEST_HEADERS = {
    "Content-type": "application/x-www-form-urlencoded",
    "Accept": "text/plain"
}

def __init__(self, payload: dict, params: dict, url: str, method: str):
    self._params = params
    self._payload = payload
    self._url = url
    self._method = method

def execute(self) -> str:
    self._prepare_request()
    self._send_request()

def extract_response(self) -> str:
    return self._response.data

def _prepare_request(self):
    self._encode_payload()
    self._encode_params()

def _send_request(self):

    self._open_connection()
    self._response  = self._connection.request(
        self._method, 
        self._json_payload, 
        self._encoded_params, 
        HTTPRequest.REQUEST_HEADERS
    )

    if self._response.status != HttpStatusCodes.SUCCESS:
        raise RequestError("Unexpected http response code {}".format(self._response.status))

def _encode_payload(self):
    assert self._payload is not None
    self._json_payload = json.dumps(self._payload)

def _encode_request_params(self):
    assert self._params is not None
    self._encoded_params = urllib.urlencode(self._params)

def _open_connection(self):
    assert self._url is not None
    self._connection = httplib.HTTPConnection(self._url)

    yield

    self._connection.close()
    self._connection = None

class HttpClient:

def post(self, params: dict, payload: dict, url: str) -> str:

    HTTPRequest(
        params: params,
        payload: payload,
        url: url,
        method: HttpRequestMethods.POST,
    ).execute()

def get(self, params: dict, payload: dict, url: str) -> str:

    request = HTTPRequest(
        params: params,
        payload: payload,
        url: url,
        method: HttpRequestMethods.GET,
    )

    request.execute()

    return request.extract_response()

At first, you might think that this is not so much less code than before.

But what if I tell you that the HTTP protocol does not only support the request modes “POST” and “GET”?

In fact, HTTP supports 9 request methods (if we count the common methods).

Can you imagine the mess that we create by copy & pasting our original code example 9 times?

Good code is always an investment into the future.

Don’t let tight deadlines and pressure force you to write bad code. Empower yourself, decide to write good code and defend this position. Explain people why bad code will cost them more time and money further along the line.

Your superiors might not be programmers. They have only a rudimentary understanding of programming. It is your job to explain to them why code quality is paramount.

Name Things Properly

Being able to identify things in our environment and communicating them to others using language is a vital part of the human success story.

The same is true for programming. In fact, we programmers do learn programming languages after all, aren’t we?

What point do I want to make here?

Good code works, it does what we ask it to do. Great code explains its intent to the reader.

Using good names for your code variables, classes, and methods makes it easier for readers to understand its intent.

Compare these two versions of our refactored post method.

def post(self, p: dict, pd: dict, u: str) -> str:

    Request(
        pd: pd,
        p: p,
        u: u,
        m: HRMethods.POST,
    ).do()

def post(self, params: dict, payload: dict, url: str) -> str:

    HTTPRequest(
        params: params,
        payload: payload,
        url: url,
        method: HttpRequestMethods.POST,
    ).execute()

Both convey the same functionality but the intent of the first snippet is much more difficult to understand.

Yet, the code above does not really solve a complicated problem.

Abbreviations such as the letter p are a poor name choices for variables or parameters. The letter p does not explain any intend, it could refer in the context of the post method to the payload or the parameters of our http requests.

Using short names and single letter names leads to ambiguity.

Use short names only if they explain the intent of a parameter, class, variable, or method without any room for interpretation.

Use long names where short names are not sufficient.

Finding the proper Method Name

Methods are algorithms packaged in OOP code.

The name of a method should describe what it does.

The _encode_payload method in our refactored code snippet takes our payload dictionary and encodes it to a json string.

You may be temped to name this method _encode_payload_to_json following my advice from the last section. And certainly, this is a good name.

But I decided against it because of the context.

The _encode_payload method is located in the HTTPRequest class. So, we “encode a payload” in the context of an “HTTP request”.

Ask yourself: “In how many ways can you encode a payload for an HTTP request?”.

JSON is the standard format that we use to send information through an HTTP Request to a server. Developer expect that an _encode_payload uses the JSON format in when the class is called HTTPRequest.

Good method names match expectation with functionality.

Let’s assume that our HTTP request sends the payload in the form of an XML string instead of JSON. That’s quite unconventional but not impossible.

It is better to name the encoding method _encode_payload_to_xml in this case. So you avoid a conflict of expectation and functionality.


Document Where Proper Naming Fails You

Many young professionals in the field of software development believe that every method and class should have a doc string (a comment in your source code that explains the intention of a method or class).

And I sympathise with you. After all, you write source code documentation to make it easier for others to understand the intention of your code. That’s a good motive but unfortunately not the best way to make your code easier to understand.

Source code documentation only treats the symptoms of bad code practice but never the root cause for it.

The purpose and functionality of your methods should explain itself through proper method names and proper code structure.

Ask yourself those two question before you write the next doc string.

1) Do others understand my code also without the doc string?

2) Can I improve the code structure or method & variable naming of the code to simplify it?

Do not write a doc string if you can answer at least one of the questions with “yes”. Refactor your code instead.

Why Are Doc Strings Evil?

The last section may be quite a revelation for some of you. You might not be completely sold on abandoning doc strings yet.

Most of us got told through senior developers to write “proper source code documentation” at some point of our career.

But Doc Strings have a fundamental issue. They are difficult to maintain.

A bug in your source code must be fixed. The customer will open a support ticket at some point, forcing you to correct mistakes. But the same is not true for doc strings.

And what is worse than a piece of source code that is difficult to understand? A piece of source code that is difficult to understand and has a misleading doc string attached to it.

Unmaintained source code doc strings are like broken navigation systems. They lead the user into the wrong direction and make it even harder to find the destination.

When Doc Strings Become Necessary

Working as a software developer for a couple of years taught me that there is always a reason to make an exception.

The same is true for source code documentation.

While you should avoid them whenever you can, there are scenarios when they become a lesser evil. Remember the two points we made in the last section?

What if you cannot think of a way to improve the structure and naming of your code that makes it more explicit?

Perhaps you develop a method that causes numerous side effects (actions or outcomes that a method causes that are not anticipated by the caller). Or the underlying algorithm is very complex.

Add brief doc strings to your methods in such cases. Focus on providing only the missing information through the doc string. Do not document things that are already clear through the method names.

Abstraction is Key

Programming is the “art of abstraction”. We take a specific problem and engineer a solution that can be applied to a wider problem domain.

Such broad solutions are also called “generic solutions” in programming jargon.

But abstraction is not only important to make algorithms reusable. They also give us hierarchy.

Take a moment and scroll up to our refactored code snippet. Try to find the “abstraction layers” in our code. How did we achieve a hierarchy in our code example through methods?

Find the Abstraction Hierarchy

Every method in our refactored example focuses on one abstraction level.

The “highest abstraction” level are the post and get methods. Methods that belong to a higher abstraction level hide contain the least amount of implementation details. Methods in the lowest abstraction layer perform low-level actions.

The post and get methods in our example yield no implementation detail. The entire logic that sends HTTP requests encapsulates in the HTTPRequest class.

Separating the different request types from the actual request logic makes our code reusable. It also gives the reader a sense that the post and get methods are similar in logic, since both use the HTTPRequest class.

Going lower in the abstraction level, we find the HTTPRequest.execute method, which invokes _prepare_request and _send_request. Separating the preparation logic of the HTTP request into two methods makes it obvious that some form of request parameter encoding must be done before sending an HTTP request.

Finally, our lowest abstraction layer deals with the concrete implementation details. The methods _encode_payload and _encode_params perform specific actions to prepare request parameters, while the _open_connection method creates a communication channel to the server.

Hierarchy creates Flow

But why is it necessary to have several abstraction layers? Doesn’t it make the code more complicated, after all?

Take a moment and compare our first code snippet with its refactored counterpart. You will notice that the methods of the refactored code snippet have only a few lines each. Both methods of the original code snippet on the other hand have dozens of code lines.

The reason for it is the missing separation of abstraction layers in the first code snippet and the resulting from it a missing hierarchy in the code.

The post and get methods in our first code snippet encompass all implementation details. That makes the code more complex.

The refactored example has a clear hierarchy. It takes the reader by the hand, starting with high-level methods and goes down into the nitty-gritty details.

Code hierarchy through abstraction layers creates a flow.

Have a final look at the refactored code snippet. You notice that even the order in which we wrote the methods follow the aforementioned top-to-bottom flow.

The result is a piece of source code that reads almost like a well-written user manual for a kitchen appliance. It provides the user with high-level information at first and offers more details further along the lines.

Colleagues that read this code piece choose if they are interested in reading all the details of the code or stop at a higher level.

Perhaps they only want to find out how an HTTP request gets prepared?

No problem, they just jump from the post method to the HTTPRequest.execute method, which points them to _prepare_request. Immediately, they find out that both the params and the payload must be encoded (_encode_payload & _encode_params).

Summary

The most important rule of any proper source code is the “Don’t Repeat Yourself” principle. Any code base with redundant code passages is a nightmare to debug, maintain and bugfix.

Try to avoid code duplication. Code snippets that are literally the same or solve the same problem should be extracted into helper methods.

Your aim should be to create simple source code that anyone can understand. Put an effort into refactoring your code if you think that you can improve its structure or code style.

Use descriptive names for variables, classes, and methods. Your colleagues should not have to rely on any form of code documentation to understand the intent of your methods. Use longer, more descriptive, names where short names fail you.

Ensure that your methods solve exactly one thing and let the method name reflect this action.

Let your code create an abstraction hierarchy. Allow users to be guided by the flow of your code.

And that’s it. Three rules that will help you level up your object-oriented coding skills. Some rules might be difficult to understand at first, but they are packed with concepts that you will use throughout your career as a professional software developer.

So take the time and revisit.

Did you find this article valuable?

Support Dennis Groß by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this