© Giuliana Carullo 2020
G. CarulloImplementing Effective Code Reviewshttps://doi.org/10.1007/978-1-4842-6162-0_2

2. Code Structure

Giuliana Carullo1 
(1)
Dublin, Ireland
 

The world is full of obvious things which nobody by any chance ever observes.

—Sherlock Holmes in Sir Arthur Conan Doyle’s “The Hound of the Baskervilles” (1902)

In the early days of programming, being able to create Turing-complete algorithms was so innovative that people were not that much concerned with structured programming.

Few years later, in 1968, Edsger W. Dijkstra wrote his open letter “Go To Statement Considered Harmful,” where structured programming was born.

Today, structured code represents the very basics of clean code and something to really consider not only from a technical perspective but also for its implication to the economics of writing successful code.

Thus, in this chapter, we will take a closer look at the following aspects and what we should look for when doing code reviews:
  • What makes bad code

  • Core principles of clean code and good practices

  • Dos and don’ts of object-oriented (OO) programming

  • Software architectures

What Makes Bad Code?

First, let’s talk about the worst kind of code: spaghetti code. Much like the noodles it is named after, spaghetti code is limp, unstructured, and can turn into a mess if not controlled. Technically, spaghetti code
  • Violates principles of structured code

  • Has arbitrary control flows

  • Has jumps (back-in-the-day GOTOs) here and there

There are other kinds of “badly written code,” but spaghetti code deals with an even more severe problem: bad code structure.

If a codebase is well designed (e.g., modular), bad code can still be spotted and fixed. Spaghetti code—and any other example of badly structured code—requires tedious, often avoided, refactoring. Although it is possible to fix bad code, it is much more difficult to fix spaghetti code. For that reason, in this chapter, we will provide the fundamentals for assembling a nicely layered lasagna instead of ending up with an inextricable tangle of spaghetti.

Note

Unstructured code makes everything so difficult to maintain.

Recipe for Disaster

Even though modern programming languages oftentimes proclaim

I would rather see you begging for mercy rather than giving you that magic GOTO statement

we can still create our best spaghetti version for lunch.

Refresher

GOTO statements are unconditional jumps from one portion of the code to another. They provide a mean, differently from loops and conditional statements (if-then-else), to unconditionally change the execution flow. As a consequence, they are highly discouraged if the language provides them because they make the flow very difficult to understand and many modern programming languages do not provide GOTO at all.

Let me give you the best recipe for spaghetti code in the world:
  • Do not design your solution in advance, that is, no architecture, nothing.

  • Use a bunch of global variables.

  • Use an object-oriented (OO) language, but dismiss all the benefits (inheritance, polymorphism, etc.).

  • Forget that someone told you that design patterns exist. They are your enemy.

  • Write big classes with tons of different responsibilities.

  • Write highly coupled components.

  • Don’t think about APIs and just pick and choose your preferred names.

  • If parallelization is required, just add a bunch of threads when you think it might somehow work.

  • Don’t consider code reusability; someone else needs to reinvent the wheel in case needed.

If this recipe makes you cry and there are no onions in the room, you already understand why spaghetti code is bad. So, don’t do it! Either way, in the next sections, we will start untangling piece by piece the ingredients of the preceding recipe.

Fundamental Principles of Good Code

Now that we’ve discussed what not to do, what are we supposed to do? This section introduces the always green principles—the ground truth of clean coding.

The Zen of Python

The “Zen of Python”1 states the fundamental principles that every programmer should follow. It is as follows:

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren’t special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you’re Dutch.

Now is better than never.

Although never is often better than right now.

If the implementation is hard to explain, it’s a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let’s do more of those!

It is a pleasure to see how beautifully and simply the Zen of Python states its core design principles behind the language. And it also provides a solid base for engineering any code that you will type using it.

Note

Even if the Zen of Python, as the name says, describes the guiding principles behind Python, it is and should be used as a good summary of guiding principles for any code in any language.

KISS Principle

The KISS principle stands for keep it simple stupid. The underlying idea is to keep the code as simple as possible so that it will be easier to work on it later.

Complex or complicated code takes longer to design, write, and test. And it might be harder to modify or maintain in the future.

I would still avoid, however, being “cheap” during design phase. Simple is far better, but missing core requirements and not embedding them into the process is as detrimental as overcomplicating things.

If you don’t understand your code well enough to write it into its most simple version, how can you look back at the code and get what it was supposed to do in the future?

Note

Some programmers might be tempted to write complex code for the sake of showing their mastery of the language. Meanwhile, certain feature of a given language might suit your code, be always aware if any other version would make it simpler.

Reusability

Let me tell you a story. In 1812, Charles Babbage came up with the idea that computation could be performed by a fast reliable machine. He called this machine the Analytical Engine. In the following years, Ada Lovelace described an algorithm for this engine able to compute Bernoulli numbers. Thus, the first program was born.

Even back then, the wise Ada already understood the wisdom of having reusable code. She actually recognized that the machine

Was not a mere steampunk abacus, but a device that can process data of any kind, and perhaps even reason.

Reusability is an innate concept; why do we persist in neglecting it in our code so often?

Code reusability aims at saving time, money, and resources by eliminating unneeded redundancy by designing the code in a way that once created, it can be used in some shape or form by other components during the software development process.

Think about not having reusable code. It’s like constantly asking a machine that is able to reason to think about the exact same issue and solve it over and over. It’s not a proper use of such a capable resource.

If I have not convinced you yet, duplication is definitely tedious to deal with. Do you enjoy books that have the same concept repeated over and over? I bet you do not. And, even if in books repetition can still have the purpose of acting as a reminder (up to an extent), in programming the other way around is a recipe for disaster.

In conclusion, avoid repetition at all costs: it leads to high levels of coupling and makes the code less performant and more difficult to read.

Takeaway

Writing reusable code is a cornerstone of clean code. If the code duplicates logic and/or data, it is breaking this principle.

Readability

Good readability means that it takes little time to understand what a piece of code is doing.

Human brain is so complex, but we all still forget things. Can you remember complex, long, and random passwords? No way. Maybe only one if you repeat it every day.

What chance do you have to quickly remember a random name of a variable and get the context in which it has been used?

Naming and proper commenting are one facet of readability, which will be addressed in the majority of the proposed good practices presented in this book.

Facts from the World

In 1956, the psychologist George Miller suggested that human capacity to remember items in the short term is limited somewhere between five and seven items. How many lines of code (LOCs) do we professionally write every day? How many variables? Definitely more than five every day. Help your memory by embedding context. This would benefit readability too.

Other important aspects of a well-flowing (i.e., readable) code include
  • Indentation

  • Following the guidelines from the given programming language to implicitly express intentions (e.g., declaring a private variable or a global one)

  • Functions, methods, and classes are short and on point

  • Maintaining consistent conventions across the entire codebase (e.g., single quotes vs. double quotes)

All these aspects of readable code will, of course, be analyzed more in depth in the remainder of this book.

Modularity

Modularity is a basic principle of software systems. It means that components in a software system should be cohesive and loosely coupled.

A cohesive component has a clearly defined function. Multiple components are loosely coupled if the dependencies between each other are minimal.

In general terms, modularity is applied every time we get to decide which portion of code goes into which function, module, class, or package.

This principle impacts on several quality attributes including
  • Readability due to the logical split of code.

  • Modular code consists of well-separated components. In such case, modifying one component would have minimal impact on others.

  • By tightening together logically cohesive pieces of code, reusability increases.

Takeaway

Modular code is easier to maintain and refactor. Always look for logic that can be generalized, put in its own component, and reused elsewhere in the code.

Maintainability

Maintainability is generally used to refer to how easy it is to maintain code over time.

It involves
  • How easy will it be to extend the code in the future (extensibility)?

  • How easy will it be to rework or refactor the code?

A lot of principles and quality aspects impact on this very broad definition. As an example, not easily readable code makes the code harder to understand and, hence, to modify later on during the development process. As you saw in the previous section, also modularity contributes to an overall easy-to-maintain code.

Note

As tempting as it is to provide quick and hot fixes for bugs in the code, especially if on a strict deadline, always go for looking at the root cause of the issue at hand. This would greatly benefit how easy to maintain code in the long run (and also the likelihood of not having to fix a bug several times).

Testability

Testability refers to how easy it is to ensure correctness of code by means of writing tests for it.

Clean code allows not only for easier bug discovery but also for easier times when writing testing code and procedures.

Readability plays a very important role in testing a piece of software. A generally good practice is to have a developer and a tester to be two different people in order to ensure. The rationale behind is similar to writing a book. Proofreading and editing is normally performed by a person different from the author. Indeed, for the author, it might be easier to neglect mistakes (e.g., typos) since they already know what’s written. This guideline is even more true in industrial settings at scale. In such cases, indeed, often, yet not always, different specialized roles are in place: developer and tester. In such case, having readable code helps the tester to quickly and better understand what to test for.

Note

Testability goes well beyond code readability. It is highly encouraged communication between developers and testers to ensure that requirements and scope are clear to both in order to drive what cases need to be tested. The more the knowledge and understanding of the software under testing, the better the outcome.

Understandably, not all the companies have the same processes in place, and you might find yourself being both the programmer and the test writer of a single feature. It would be not the end of the world, and I am sure if you are in such conditions you’d be great anyway with the results!

However, if more than one person is in the team, an encouraged practice would still be to add a different perspective to who does and/or reviews the tests.

Composition vs. Inheritance

Refresher

Composition and inheritance are two techniques used to establish relationships between components. The first, as the name says, allows relationships to be created by putting and using together multiple components. As an example, a car can be seen as a composition of wheels, engine, windows, sets, and so on. The latter allows for extending the behavior of an object. For example, a dog can be seen as a general extension of the abstraction animal.

The composition vs. inheritance principle means that to achieve polymorphism, composition should be preferred instead of inheritance.

Composition usually presents a more flexible design, thus resulting in being maintainable in the long run. Not only are designs based on composition easier to write; they will also accommodate future requirements and changes without requiring a complete restructuring of the hierarchy.

Truth to be told, composition comes with minor drawbacks including forwarding methods. This happens when the behavior of the composer for a certain method needs to match the behavior of the composed object. In this case, the composer only needs to call the relative method from the composed object.

As a rule of thumb, if too many forwarding methods are in the code, they may signal the—very few—cases where inheritance might be preferred in the current design. In this case, just reevaluate it.

Premature Optimization

The premature optimization principle suggests that you resist the urge of speed up before it is necessary.

Again, being cautious with not writing unneeded code as well as unneeded optimizations does not mean being “cheap” with design. Parallelizing your newborn code might be unneeded, hence totally embrace this principle. However, do not use it as an excuse to write bad code by not considering performance requirements.

As an example, suppose that you are building a new framework and that a functionality running on top of it needs to match a 100ms runtime mark. Sure enough you have to consider it and not only for the algorithm behind the actual functionality. The framework’s design should be designed in such a way to make the goal actually achievable.

These principles are inherently anti-spaghetti code, so following them will help you write better code. Now, let’s look at the bigger picture.

Takeaway

Proper design and premature optimization are not the same thing. Always strive for good design, and leave premature optimization for later during the implementation process.

Sound Software Architectures

Truth to be told, there is no single path to write perfect code. Good-design and sound software architectures are the inceptions to write better software.

Software architectures constitute the very first step to write marvelous code. The kind of code you don’t hate to read. The kind of code you don’t hate to troubleshoot, to extend... Okay, you got the point.

We love to code and to dig deep into the latest cool technology. But the shiniest tech would not help us—by itself—to achieve our goal: make others love the quality of the code we write. We may have unconditional love for our cute code babies—no matter how well (or bad) they are written. But others won’t necessarily love them: the harsh truth. So, we need to raise our code babies right so they can be outstanding members of a strong software architecture.

There are a few things you can do to make sure you have a good software architecture that will get the job done and will make others love the quality of your code.

We will expand on what it means to have a sound software architecture and what to look for during reviews in Chapter 5.

Be People Minded

One of the best things about writing quality code is that it will make people happy: colleagues, stakeholders, and customers, you name it. Everyone will be happier; even your mom will pat you on your shoulder, believe me. And this is one of the most critical points for engineers in today’s tech jobs: how well you serve your audience. Writing good quality code is—for sure—rewarding, but our main job is to serve others, to which extent we improve their lives.

Software architectures help in driving this major success goal. Good architectures are not only linked to applying design patterns, designing APIs, and optimizing performances (which would make your colleagues happy). It is also about finding the right trade-offs between maintainability, usability, security, and any other requirements both internal (the company you work for) and external (stakeholders’ interests).

Note

As engineers, we like programming and we are with high likelihood industrious people: we like to build and create things. However the human element cannot be removed from our creations: anything we do is meant to serve someone else, not writing beautiful code alone.

The biggest recommendation that I can give you to this regard is to always approach any piece of code from three different angles:
  • Customer driven

  • Data driven

  • Engineering driven

Neglecting any of them will eventually provide some results, but not the best the code can achieve.

We already started thinking about how important it is to implement something that is really needed and solves the right problem. However, any problem which, for example, lacks of the data-driven perspective might limit the opportunities to add even more value on top of the current customers’ needs. At the same time, having a customer-driven approach without proper engineering supporting the code (and hence clean code) causes bugs and errors to pave their way up to the customers and, possibly, missing deadlines because of improper processes and development effort.

Au contraire, the best tools and data exploration will serve no purpose if the result is not what the customer wanted, or if the proposal is not perceived as valued.

Be SMART

Oftentimes, the counting lines of code (LOCs) are used to gauge project “quality.” But we really need to go a step further. Nope, 100k LOCs of code do not necessarily mean the project is more complex—or better—than 50k LOCs. I think that the quality of the code reflects more on the architecture. And it serves us better to be SMART, which stands for being specific, measurable, achievable/realistic, and time-bound. By following this guide, our architecture will be specific to requirements; measurable in terms of usability, maintainability, performances, and so on; achievable/realistic (simple is often better), time-bound (which would make others also happy).

Once again, a well-designed software architecture is the primary need for a good start. Bad code happens; bad architectures are the enemy. Architectures provide needed boundaries for the code to be developed. And they help—a lot—in avoiding modern spaghetti code.

If you are dealing with spaghetti code:
  • Set standards that the code will follow

  • Embrace good practices

  • Design the API and stick to it

  • Refactor the code

And do it as soon as possible; a small short-term investment will work wonder in the long run. Don’t strive for perfection—less than optimal code might also work depending on business needs. But don’t neglect architectures. Writing good code or improving it as we go seems not worthy initially, but it will definitely pay off at the end.

Note

Always leave the code in a better shape than the one you found.

APIs

Let’s look at another recipe for disaster: API (application programming interface) spaghetti omelet. For this recipe, you will need
  • Spaghetti code

  • Beaten API eggs

  • A grain of salt

The preparation is fairly simple: Get the spaghetti you already have. Grab your best engineer and make him/her beat eggs for a couple of minutes. Eggs’ volume will increase, and the texture starts to get thicker and foamy (spaghetti already starts to appear tastier). Mix all together and use a grain of salt for flavor. Deep fry, and serve while still hot.

You might think that spaghetti code can be reasonable because at least APIs are clearly defined and “only” the code is messed up.

Note

Any layer of complexity you add is one more layer of potentially new weaknesses popping into the products you are building. A famous security principle states that the security of a chain is as strong as the security of its weakest link. The same can easily be ported to clean code: a code is as much clean as its least clean piece of software.

On the other end, building a solid API requires as much care as any other piece of software. As an example, error handling is very critical for APIs too. A common mistake is to think about error handling later in the development process. And it can easily become too late, potentially leaving who calls the API confused about the response they get (if human calling up).

Error handling, as well as the format of the response, should be clear, documented, and uniform across the code. In such way, any automation process on top of the API will be easier, quicker, and cleaner.

The API-first approach has become fairly popular in recent years to help with the design of consistent and clean API. As the name suggests, such approach consists of prioritizing the design of APIs establishing rules and agreements on how the given application is supposed to behave. This approach responds generally well to scenarios where there is a need for customer to interact with the application (e.g., by means of mobile, tablets, and websites).

Even if this approach might not suit every development process, it still has some beneficial concepts and implication that come with it that you should include when thinking of APIs:
  • It provides a very useful perspective on what the software is meant to do.

  • It helps to consider edge cases.

  • It helps in isolating logical problems, hence providing a possibly more structured approach to their resolution.

We will further explore the need for thinking about the broader problem in Chapter 6. However, the main takeaway for this section is as follows:
  • Do not hide dirty code under the API carpet.

  • Do not underestimate the importance of the APIs and their consistency.

  • When writing code, especially if the mind starts to wonder, call it back and think about what the problem you are really trying to solve is.

Be Mindful of Control Structures

GOTOs are not the only way of altering control flows. Some attention needs to be put also in more innocuous if-then-else statements.

This construct surely will not harm readability as GOTOs. However, having completely defined constructs is needed to both improve readability and have the certainty of a well-defined code behavior. Completely defined means having an else-for-each-if statement.

Pay Attention to Health Status

Looking at the code quality goes beyond defects. Here are five health indicators to consider in order to avoid spaghetti architectures:
  1. 1.

    Problem definition: Everything starts with the problem definition. An unclear or not completely defined problem will surely lead to cluttered and bad smelling architectures and even worst code. Ensure that the overall problem that needs to be solved is clearly defined, what is in scope and what is not, and—when applicable—that a minimum viable product (MVP) and a road map to achieve it are defined and communicated.

     
  2. 2.

    Validate the architecture: The solution might be relatively fancy but won’t solve the problem. Take some time to go through requirements (functional and nonfunctional) to validate the proposed design.

     
  3. 3.

    Rethink technologies: Languages, tools, platforms, and frameworks need to be considered based on overall requirements. Don’t follow the approach “if all you have is a hammer, everything looks like a nail.”

     
  4. 4.

    Knowledge: As part of reaching the best possible product, human aspects also need to be considered. Does the team have proper domain knowledge? If not, training in all shapes and forms is the way to go. But once again, don’t look at everything as a nail.

     
  5. 5.

    Processes: Even the best team, with a well-rounded architecture, can’t work in the most productive way if there are no systematic processes to follow in order to obtain a manageable development.

     

Summary

Ensuring code quality is broader than what it seems. It is not just having good code, nor is it only about having good architectures.

As explained in this chapter
  1. 1.

    Evaluating status and potential risks around the highlighted indicators should be part of our continuous review process.

     
  2. 2.

    Pairing code and architecture reviews with an overall health status will help products to be on track and achieve their best shapes.

     
  3. 3.

    Hiding unclean code under the carpet (APIs or any form of wrappers) does not solve the issue.

     

In the next chapter, we will start digging deeper at how we can add reviews at design phase and what we need to look at in order to maintain sound structure, behavior, and dependencies across the entire codebase.

Code Review Checklist

The following are potential structure issues that should be checked during code review:
  1. 1.

    Does the actual implementation reflect the architecture?

     
  2. 2.

    Is the code easy to understand?

     
  3. 3.

    Is the code too long?

     
  4. 4.

    Is cohesion in place?

     
  5. 5.

    Is the code modular?

     
  6. 6.

    Are components cohesive?

     
  7. 7.

    Is the code loosely coupled?

     
  8. 8.

    Is the code reusable?

     
  9. 9.

    Is the code readable?

     
  10. 10.

    Is the code easy to maintain and test?

     
  11. 11.

    Are premature optimizations in place?

     
  12. 12.

    Is composition preferred?

     
  13. 13.

    Is inheritance properly used?

     
  14. 14.

    Is the flow easy to understand?

     
  15. 15.

    Are interactions between different components easy to catch?

     
  16. 16.

    Are conditional flows completely defined?

     
  17. 17.

    Is there any undefined behavior?

     
  18. 18.

    Are APIs consistent and as clean as the overall code?

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.40.47