Yellow degree

Principles

Interface Segragation Principle

The Interface Segragation Principle means to separate the description of a component from its implementation. The goal is to have WHAT has to be done separated from HOW it is done.

A good designed interface describes which problem shall be solved by the implementation. It is reduced to the bare minimum of how the component shall be used later. The less the interface exposes, the better.

A simple example could be to check if two instance are equal:

interface IEquatable<T>
{
    bool Equals(T other);
}

That interface does not specify how the equality check is done, but it describes precisely what shall be done.

Dependency Inversion Principle

The Dependency Inversion Principle means that classes should not instantiate their dependencies. The major reason for this is to reduce coupling.

To take this a step further, this principle could be seen like that:

  1. Classes should not instantiate their dependencies.
  2. Classes should not depend on classes.
  3. Classes should depend on interfaces.
  4. Interfaces should depend on interfaces.

The cleanest way to achieve this is, that a class retrieves all of its dependencies via constructor parameters.

An advantage of this is, that when someone reads your class, that person just need to look at the constructor to get an overview of the dependencies.

A disadvantage is, that you have to provide the dependencies every time you instantiate that class, which can become quite a mess when done manually. Because of this, we will have a look at Inversion of Control later.

Liskov Substitution Principle

The Liskov Substitution Principle means, that a subclass of a class should behave the same way, as the base class does. This sounds trivial, but an example will make it clear. If a subclass overrides a method and the new logic of the subclass throws an exception under specific circumstances, but the original logic of the base class doesn't, then the caller of that method might not expect that exception and does not handle it.

This principle faces two problems:

  • The developer of the base class should chose a good design, that consumers of that class know what to expect.
  • The developer of the subclass should consider what the consumer of the class would expect.

As addition to the example above, imagine a parser class:

class MyParser
{
    MyDto Parse(string content)
    {
      // ...
    }

    bool TryParse(string content, out MyDto instance)
    {
      // ...
    }
}

That class makes it obvious to the consumer, that there are two methods to parse some content. One method shall always return an instance of the parsed content, or might throw an exception if the content is invalid. Another method will true if parsed the content and false if the content is invalid.

In case of a subclass overriding those methods, the developer of the subclass should not throw an exception in the TrypParse method and the consumer of that class is prepared to handle exceptions thrown by the Parse method.

Principle of Least Astonishment

Software development - similar to other creative activities - requires focus. Whenever there is disturbance, the focus is lost and it takes time to focus again. There are statistics that say that it takes around 25 minutes to reach the same level of focus again, after being disturbed. Not all disturbances can be avoided. You can work in a quite area, wear headphones, etc. But if the IDE you're working with does not behave as expected, or if an API does not work as documented or is designed in an unusual way, can disturb you and is not avoidable.

If you write software, your application or API should work as the user would expect.

If the user can copy and paste text in your application, then you should use Ctrl+C (copy) and Ctrl+V (paste) as keyboard shortcuts as most applications do. Using Ctrl+P for paste would be confusing for the user since it is often used to show the print dialog.

It's similar with APIs. A method GetValue() should just return the value and not change the internal state. PopValue() or GetAndResetValue() implies that you will get the value and that the state will be reset.

Make it easy to do it the right way. Make it difficult to do it the wrong way.

Information Hiding Principle

An API - interfaces, classes, etc. - should expose the bare minimum that is required by the consumer. Specifying less reduces the potential of coupling between components and facilitates the orchestration of components.

Perfection seems to be achieved not when there is nothing left to add,

but when there is nothing left that can be taken away.

Antoine de Saint-Exupéry

Practices

Automated Unit Tests

We talked about automated integration tests before to test that your application still works as expected with other applications/modules after you did changes. Automated unit tests work on a lower level. Instead of testing your application on an integration level, unit tests focus on the internals of your application, the classes/components. A unit test verifies that a (public) method of a class works as expected, i.e. that a method returns the expected result.

  • Unit tests should be written for each public method of all public classes.
  • Unit tests should cover all reasonable test cases.
  • Unit tests may cover guarding statements too, if they are relevant for the logic. There are usually guarding statements which are relevant for debugging only, during development or bug-fixing.
  • Unit tests should have a short runtime to be tested continuously.
  • All unit tests should run and succeed when you commit your changes to the version control system. Modern VCSs can be configured to do this automatically before accepting any changes.

There are different test strategies:

  • Test-Driven-Design
    You write the unit tests the same time you develop the component.
    Pro: efficient development
    Con: testing own expectations, edge cases often not covered
  • Test suites
    The interfaces of the components are designed/specified first. Then the unit tests are written against the specified interfaces. Then the component is written to succeed all tests.
    Pro: component is tested against real-world test cases
    Con: requires usually more effort from both sides, business and development
  • Test scenarios
    Similar to TDD, but you specify test scenarios for your component which will be used to test different aspects/members of your component in similar situations.
    Pro: efficient development, various aspects of your component are tested against real-world scenarios
    Con: requires good business knowledge at the developer side

When to start with unit tests in a project? Right from the start! Your first lines of code in a project should be covered by unit tests already.

If you brake something in the future, your unit tests will safe you a lot of time by identifying that early in the process.

Mockups

As mentioned above, you components might have dependencies. If you followed the Dependency Inversion Principle, then you can replace all dependencies with mockups to ensure that you test your class, and your class only.

Mockups are also known as fakes, stubs or dummy.

There are frameworks that can be used in your unit tests to construct/configure mockups at runtime easily. Those mockups are then injected in your component and behave as a replacement for the original dependency.

An advantage of mockups is, that your tested component is the only system under test.

A disadvantage of mockups is, that you test your component against your assumptions of how the dependencies work. If one of your assumptions turns out to be wrong, you have to keep in mind to correct that assumption in the affected unit tests.

A good strategy when to use mockups instead of real dependencies is to design your application in layers and to use mockups for all components of different layers than the component you are testing. Example: you test a component with business logic which has dependencies to other business logic and to the data access layer. In that case you should use mockups for the data access layer.

Code Coverage Analysis

Modern test frameworks provide code coverage analysis for unit tests. Those reports are very helpful to identify uncovered code and to get a code coverage ratio.

A code coverage ratio of 100 % seems reasonable, but is not realistic. As mentioned above, you might have guarding statements deep in your code and those statements act as a safety net for you during development. Usually those internal guarding statements are not reachable using public members. Should you hack your code to run into those statements? My recommendation is: no, don't do that. Readability has a higher value, so keep your code clean. Modern compilers will ignore your guarding statement anyway if the code can't be reached or JIT-compilers will eliminate that statement during runtime if identified as unreachable. There are better ways to keep your code coverage ratio high. The solutions might differ depending on your programming language.

What are reasonable code coverage ratios?

  • Standalone algorithms: 85% to 100%
  • UI glue code: 60% to 85%
    These low values are a result of the nature of UI glue code: there should be as little business logic as possible in the glue code. If you have one guarding statement and then one statement to call the business logic in a UI call, means that you reach 50% by testing the relevant logic only.

Participation in professional events

Step outside your comfort zone and think outside the box! Get in contact with other developers. Gather inspiration by listening to others.

A good start is to participate the (yearly) developer conferences of your favorite programming languages/platforms/tools.

Hackathons, code camps and meetups might be interesting if you're a junior developer, but - because of the nature of those events - the outcome of those events are quick-and-dirty demos produced in a few hours, no long-term enterprise-ready solutions. You might get there some inspiration, but from my experience, listening to someones presentation which is based on solid experience and well-tested solutions gives you a higher value in shorter time.

Complex refactorings

We talked about simple refactorings earlier, such as renaming and method extraction. Simple refactorings don't change the way how the code works, it just rearranges it.

Complex refactorings are often required after a project grew for a while and older code does no longer match the structure of the rest of the project. Then it is time to change the way how code works but to preserve the same outcome.

As you can imagine, this includes the risk of braking existing functionality. This is the moment when your automated unit tests reveal their true power: they provide you a safety net. You can do your refactoring and you will immediately get feedback if something is broken.