Coding Principles

These are the two core principles that guide how we write code:

  • KISS: Keep It Simple, Stupid
  • YAGNI: You Ain't Gonna Need It

KISS

This first principle is a constant reminder to ourselves to avoid the seductive call of cleverness. Most code does not need to be clever. Very occasionally, it is necessary to implement something with real flair, that requires explanation.

The best code, though, requires no explanation. The best code gets its job done in a very boring way, using the same patterns to achieve different ends. The best code is instantly recognizable to those who know the patterns. The best code doesn't raise any questions. The best code doesn't need comments. The best code is obvious and, yet, does amazing things—like fulfill requirements in a stable, predictable, testable, customizable and high-performance manner.

It's kind of obvious: The lower the complexity, the easier it is to reason about systems. The easier it is to reason about a system, the easier it is to prove that either certain things can't happen or will always happen. It should be obvious where to add a customization—because there's only one place that it could logically go. It should be obvious where a bug lies—because there's only one place it could have originated.

The best code is readable and understandable not only by the original programmer, but also by another programmer—even if that's the original programmer, six months later.

YAGNI

We'd be lying if we said that we never write code that we don't need, but we keep this principle in mind whenever we build code. There's a bit more wiggle room when building frameworks vs. products. It's easier to determine whether a feature is appropriate for a product than to do the same for a framework. Who knows how a framework might be used?

Encodo does have a framework named Quino. The point of a framework is to support the development of products that use it. It's not easy to predict what those products might need, even when you're focused only on features that your framework is supposed to provide. However, a framework or library has a purpose and it shouldn't stray from it.

Just as an example: Does Quino provide a remote data driver? Yes, because products have used it and the feature fits into the strategy of metadata-supported data. Is there an XML transport protocol? No, because no-one needed it. Do we support any kind of object? Not out of the box, we don't. You can register your own converters, but it's not a generalized protocol.

At the very least, we stay away from throwing in everything but the kitchen sink—just in case a product that uses Quino might need it. Be prepared for anything, but build only what you need.

Other Principles

We apply the following principles to avoid unneeded complexity.

  • Separate state from logic
  • Use immutable data
  • Use non-nullable references
  • Avoid side-effects
  • Compose functional components
  • Use singletons

Separate state from logic

From the article Why OO Sucks by Joe Armstrong (inventor of Erlang).

State is the root of all evil. In particular, functions with side effects should be avoided.

The sentiment in the title is a bit strong, but its not unfair. OO programming mixes data with operations, leading to more complexity than required by the task.

Most applications need some state. That state should be isolated from most components. State should be stored in dumb objects and passed around.

A component without state is purely functional, drastically simplifying the things that could possibly happen to it. Its output is completely determined by its inputs. It does not introduce any threading issues beyond those inherent in its input.

Use immutable data

A component avoids a whole class of issues if it cannot make changes to the data that flows through it. As with state, restrict mutability to only certain components.

For example, transient objects like DTOs or ORM objects are mutable because it makes the program logic much more understandable

Another example is stateless singletons with configuration settings. instead of using a single component with mutable properties, define the configuration in a settings component. This has several advantages:

  • The settings is a dumb "state" object (single responsibility)
  • The service is a stateless singleton (single responsibility)
  • A product can replace the service independently of the settings (and vice versa)
  • Service implementations don't repeat boilerplate code to manage properties

Use non-nullable references

If references are guaranteed to be non-null, whole swaths of checking code fall away and make the component much simpler. As with immutability, there are far fewer possibilities of what can happen to non-nullable code.

TypeScript supports a null-checking mode. C# supports one as well, starting with C#8. For older versions of C#, use the JetBrains Annotations along with ReSharper to enable real-time/compile-time null-checking.

Avoid side-effects

A method should either change state or it should return data. This is the idea behind CQRS (Command-Query-Separation Principle). That said, we employ a weaker version where only visible state really counts.

Techniques like lazy-initialization and caching retrieved data are generally OK. Technically, those behaviors have non-visible state in the sense that they affect performance, but are still OK if used carefully.

Compose functional components

We use C# and TypeScript—wonderful OO languages with strong functional support—but we're using less and less of what OO has to offer.

Virtual methods are a code smell. Instead, use smaller, testable components with a single purpose. If it's easier to test, it's easier to replace where necessary. Smaller components are more focused and easier to replace without duplicating code.

If logic is separated from data, and services are injected or passed as parameters, then there is less and less need for base classes with many helper functions or virtual/protected methods.

Use singletons

If state just flows through a component, then that component can be a singleton, avoiding needless allocation.

It's a lot easier to reason about an application that comprises a graph of singletons with transient data flowing through it.

Inject factories to create transient services (e.g. a remote-method caller that captures state).

Conclusion

As you can see, we put a lot of thought and care into our development practices and patterns. We try really hard to work in a way that ends up with quality software: stable, maintainable, extensible, testable and, most importantly, does what it's supposed to.

For more information about specific development patterns, please see the architecture section of the Quino conceptual documentation. There are sections on interfaces, base classes and virtual methods, providers, tools & toolkits, task-specific interfaces and much more.

updated on 6/18/2019