Learn the dependency inversion principle.

And stop monkey-patching.

High-level modules should not depend on low-level modules. Both should depend on abstractions. Also, abstractions shouldn’t depend on details. Details should depend on abstractions. These abstract interfaces become the contract on how two modules will communicate. Each module can change internally, but as long as they implement the abstract interface, they will still work together.

No class should derive from a concrete class. If you derive from a concrete class, you become more dependent on specific implementations.

No method should override an implemented method. This is a sure-fire way to find surprises further down the line. People expect one behavior, and then you’ve gone and changed the behavior. Bugs will follow.

Variable instantiation should use factories or a Dependency Injection framework.

Reducing dependencies and relying on abstracts leads to code that is easier to test, simpler to change, and more straightforward to support.

Learn the Interface Segregation Principle.

Only depend on what you use.

As applications grow over time, they get more and more features. Their interfaces become more and more complicated, as they can handle all kinds of different exceptional cases. The problem with these beasts is that making a single change can break so many things, as everything depends on the entirety of the interface.

Instead of this mess, you should prefer that systems become decoupled from each other. Do not force clients to depend on interfaces or methods they don’t use. Breaking classes and interfaces into smaller pieces means that systems can rely only on the small parts instead of everything. It is much better to depend solely on the portions you’re going to use. This way, there’s less impact on you when things change.

Watch out for big fat classes with too many methods. These massive classes can do a ton of things, but they also have a ton of reasons to change. Instead, look to have many smaller classes with much more limited interfaces. If that means you have to depend on several classes with limited interfaces, that’s better than relying on a giant class with a massive interface that can do many more things than you’ll ever need.

Choose fine-grained specific interfaces over a multipurpose interface. Think of it as preferring to use a box of tools over an elaborate swiss army knife. Use the right specialized tool for the job. It’s much easier to repair or replace one tool. If you’re just drilling holes, you don’t need support for tightening bolts and sanding edges.

Learn the Liskov Substitution Principle.

You can use the subtypes.

If S is a subtype of T, then you may replace objects of type T with objects of type S without breaking anything. Let’s say your type T is a Chevy, and the subtypes of Chevy are Corvette, Camaro, and Silverado. Then anywhere you can use a Chevy, you should be able to use a Corvette or a Camaro. Or even a Silverado. They’re all subtypes of Chevy.

Okay, so maybe that all seems like common sense, and perhaps we should call in Captain Obvious. Here’s the problem: adding or changing setter methods in the subtype can cause the subtype to behave differently.

For example, take the type Rectangle. To create a Rectangle, you will need to know its length and width. Fine. Now examine a subtype of Rectangle: the Square. The square behaves differently. You can create a square just by knowing its width; you don’t need to specify its length. Also, the formulas for area and perimeter and simpler for squares than for rectangles.

It comes down to contracts. When you created the Rectangle class, you expected its subclasses also to have width and length. Squares behave differently and have to know that their width and length are the same. With a Rectangle object, one expects to be able to set the width to 2 and the length to 3 and get an area of 6. But with a Square, if you set the width to 2, the object sets the length to 2 as well. Then, if you set the square’s length to 3, the object will set the width to 3. The area comes out as 9, not matching the 6 you would expect when setting the width to 2 and the length to 3.

If you write your subtypes in such a way that they obey all of the contracts of the parent object, then you are on your way to getting the Liskov substitution principle. Mind your interfaces.

Learn the Open/Closed Principle.

Tools are better when you don’t need to rebuild them.

Continuing to discuss the SOLID principles, one can think of the open/closed principle in terms of using tools. The key idea is that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. That is to say, you can use the tool, you can attach things to the tool, but you cannot rebuild the tool.

Modification of software requires code reviews, unit tests, documentation, and work. Despite all that, modifications still manage to break things. On the other hand, if you can build a tool that cannot be modified and doesn’t need to be changed, you avoid those dangers. It just becomes a library you use.

Extending classes is still allowed, which allows you to customize the classes and add additional functionality. Extension of software requires the subclass to follow the original interfaces, so things that use the class will still work.

If you can build applications in this way, they start to resemble Lego building blocks. You’re not concerned with the innards. You don’t try to modify the components; instead, you use them and build more complex machines out of them. Applications built in this way react to change better and have fewer bugs.

Learn the Single Responsibility Principle.

That thing should have one job.

The first amongst the SOLID principles is the single responsibility principle. Programming with the five SOLID principles will lead to software that is easier to understand and maintain, and thus is a worthy goal.

The single responsibility principle says that a class should have only one job. If changes occur to the software’s specifications, only one part of the changes should be able to affect the specifications of the class. If this is not the case, then the class probably has multiple responsibilities, a situation that one should avoid. The class should only have one reason to change.

Examples of responsibilities include persistence, validation, notification, error handling, logging, class selection, formatting, parsing, and mapping. While it may seem extreme to have a separate class for each of these, it does lead to better code.

Software written this way is less likely to break things that are not related to the requested change. Often, in immature software applications, a single change in requirements can have adverse effects all over the codebase.

It would be best if you encapsulated the responsibility handled by the class within the class. The responsibility is this class’s job, and it should not appear in any other class.

Following this technique leads to more robust code that is resistant to unwanted side effects due to a change.

Develop your soft skills.

There’s more to being awesome than programming talent.

In life, your soft skills can pay you huge dividends. Strong soft skills do more than help you relate to others. It also builds trust. When people trust you, they are more likely to give you more business, impacting your bottom line.

These skills include:

  • Communication (written and verbal). Being able to get your message across is vitally important, as is appropriately receiving and documenting requirements.
  • User empathy. You need to be able to see things from the client’s point of view. How does your application affect them, and how can you make their life easier?
  • Being a team player. You won’t get far working alone. It’s essential to have strong working relationships with others. You need to be able to leverage the power of the group.
  • Teaching and sharing. To make your team more robust, you need to be able to help them thrive and grow.
  • Resolving gray area issues. Sometimes problems simply are not black or white. How you handle things when the issues are fuzzy will significantly impact your level of success.
  • Discourse. Can you have a friendly discussion, support an argument, and reach a satisfactory resolution with opposed parties? Negotiation is another skill that is useful here.

The better you get with your soft skills, the greater your perceived value will be. In business, perception is everything.

Plan your demo early.

Make it a part of your development process.

Don’t put off planning your demo. Some developers make the mistake of not thinking about their demo until they have finished with the code. Instead, consider the demo right from the beginning. Being able to easily demonstrate the change’s functionality could help guide how you will implement the story.

You may be able to write test scripts that double as demo scripts. After all, what you are demonstrating should be covered by automated tests, so there is no good reason to duplicate efforts.

It’s best to plan your demo for a story while it’s fresh in your mind before you move on to the next story. It would be best to envision how the demo will work while you are reviewing the acceptance criteria. This way, you will not have to rebuild your code or tests to accommodating something you found while creating your demo.

The demo should be the most exciting part of the effort. It should prove you have completed the story and easily display all of the requested functionality. That way, everyone involved will have a high confidence level in the new features you will deliver to the end-users.

Performance tuning is dangerous.

Don’t attempt it unless it’s really going to be necessary.

Computers are fast. Really, really fast. They can do billions of operations per second. Shuffle gigabytes of memory in a flash. Stream high-resolution video without breaking a sweat. It’s quite impressive what computers can do today.

As a result, one should generally resist the urge to spend a lot of time on optimization and performance tuning. In most applications, no one will notice whether an operation takes three milliseconds or 300 milliseconds. So if you spend 100 man-hours improving speed and no one notices, then you’ve wasted valuable time that you could have spent fixing other bugs or adding new features.

Tuning for performance can also break things in unexpected ways and take a lot longer than expected to fix. Often a system that works perfectly fine will suddenly develop new bugs as programmers make changes as part of some unneeded performance tuning. So before you re-engineer a system to be massively scalable and ten times faster, make sure the gains will be worth the effort.

Build to last.

Will your code stand the test of time?

If you write your code well, people will still be using it twenty years from now. If you write it poorly, other developers will look to refactor and replace it at their first opportunity. To save them that work, it’s much better to write reliable code that future developers won’t need to rewrite anytime soon.

One reason programmers replace code is needless complexity. They find the existing software to be confusing, hard to understand, and difficult to change. Instead of extending it or improving it, they find it easier to replace it entirely. Software that lasts tends to be elegant in its simplicity. Future developers can understand the long-lasting code at a glance. It follows popular conventions and doesn’t try to do anything weird or clever.

Be prepared.

The world is full of random events that will not always go your way.

Every day is full of unpredictable occurrences. Some of them may be fortunate, others less so. You never know what surprises may greet you at any given time. These random events may range from horrible to disasters to incredible opportunities. What matters most is how you will be able to respond to these events.

Good luck is when preparedness meets opportunity. Want to be lucky? Then you must be prepared. Obviously, one can’t prepare for everything, but that should only drive you harder in your efforts to prepare for most things. Try to imagine various possible scenarios and establish a mental (or even physical) playbook on how you would respond to each scenario.

This way, when strange events occur, you are better prepared. You know what you want to do, and you don’t have to worry about it. Problems change from horrible puzzles that need solving to simple circumstances that warrant planned changes in behavior. In this way, one can generate your own luck.

Make it a habit, and surprises will not turn ugly. Instead, events will start to swing in your favor because you are ready.