Design Principles

Key Elements for Pattern

Before we proceed to study a pattern, we must understand some certain key elements for us to quantify a pattern.


Keys

These are the keys elements:

  • Name - for communications and design vocabulary purposes
  • Problem - understanding the context of the issue for applying a pattern
  • Solution - design elements and relationship with specialized abstracts
  • Consequences - trade-offs and benefits applying the pattern


Aspects of Consequences

For stating the consequences, we has a list of known aspects for one to consider:

  • Flexibility
  • Extensibility
  • Readability
  • Encapsulation

Principle #1 - Information Hiding

The idea is that one should expose as little information details as necessary to preserve internal flexibility. This gives 2 benefits:

  • Interface remain stable
  • Hidden implementation can be changed easily
  • Decide information mutability and abstraction
  • Decouple user interface from implementation
  • Promotes reusability
  • Simplify function / software usage


Example of Use

  • The use of private / local keyword in the programming language
  • The use of getter / setter functions over a specific data (not list).

Principle #2 - Functional Correctness

To ensure all functions are working properly, it is always the best practices to perform unit testing against all the public interfaces in a test-driven manner. There are a few stages of testing and validations:


Manual Testing

Usually involves labor testing where automation is too costly to implement. Example tests would be:

  • Live system testing / runtime system testing
  • User interface testing
  • Destruction testing
  • Field testing


Automated Testing

Implements repeatable testing where automation is feasible and cheap to implement. Usually, these testing runs as frequent as a new patch is given and nightly/daily build using a continuous integration approach like Travis CI or GitLab CI. Example tests would be:

  • infrastructure testing
  • dependency testing
  • functional input/output testing
  • functional testing
  • nightly build
  • smoke testing (smoke testing)


Test Strategies

This is usual testing approach for developers:

  1. Read specification
  2. Write for valid test cases
  3. Write for invalid test cases / boundary values conditions
  4. Write for error handling
  5. Write for difficult cases (e.g. complex algorithms)
  6. Think like an attacker (Goal: to find loopholes and bugs)
  7. Check specification coverage
  8. Feel confident? If there are time/money left, repeat #1 for other specifications.


Test Convention

Usually unit testing is by expectation assertion approach. Example:

  1. when return value is equal/not equal to the expected values
  2. when expecting an error, the error is raised from the testing
  3. when expecting a crash, the unit testing is able to recover from a said system crash.


Test Organization

Test codes can be organized based on the programming language facilitation. There are 2 general approaches:

  1. All test codes are inside a single unified test directory
  2. All test codes are next to the associated source codes

Example, in Go, #2 is preferred. In Java, #1 is preferred when using JUnit.


Test Coverage

This is to test whether a test suite covers all/most of the source codes. It provides a map (usually heat map) over the source codes. Test coverage studies the test results by depth and the test suite robustness.


Test Static Analysis

This is source codes scanning for possible errors caused by programming linguistic defects. Usually, in a programming language, linters facilitate such scanning. Example: for Go, it is golang-lint.


Test Scopes

There are different types of test scopes. Among them are:

  • Manual testing
  • Security / Penetration testing
  • Security (stability) / fuzz / reliability testing
  • Usability testing
  • GUI/User Interface testing
  • Regression testing
  • Differential testing
  • Stress/soak testing

Principle #3 - Small Interfaces

This is to keep public facing interface as small, concise, and business needs-to-have basis. Once a public interface is exposed, it is hard to alter since many users are already interfacing with this IO.


Interfaces Itself is a Software Lifetime

Interfaces themselves follows the software lifetime on its own where you schedules a deprecation period for users migration to the new API, then scrap it. This is unlike the hidden implementations under the public interface where developers can refactor them without worrying about user adoption.


Therefore, it is always the best practice to keep public facing interfaces well planned, small and lives as long as possible.

Principle #4 - Low Coupling

Keep Coupling Low. The higher the order of hierarchy, the less it needs to know everything (comply to delegation). The most ideal coupling interaction is 1.

  • It supports design of more independent classes and reducing the impact of changes and should be context dependent.
  • The most preferred way of coupling is via interface instead of implementation.
  • Comply to Law of Demeter
    • each module should have limited knowledge about other unit.
      • Don't talk to stranger. Example: no a.getB().getC().foo()
  • Subclass/superclass coupling is particularly strong
    • protects fields and methods
    • prefers composition over inheritance to reduce coupling.
  • High coupling to very stable elements/interface is usually not problematic.
    • interface is unlikely to change over long period of time.
    • prefer coupling interfaces over coupling to implementations.


High coupling is undesirable. The higher the coupling between classes, the worst the effects:

  1. The higher the dependencies, causing an overall solution rigidity.
  2. Harder to understand in isolation.
  3. Harder to reuse.
  4. Difficult to extend.


Example of Low Coupling

Here is an example of low coupling.

Principle #5 - High Cohesion

Cohesion simply means small and modular implementation. In ideal condition, we strife for high cohesion. However, the higher the cohesion, the higher the coupling. Hence, one must find the balance between low coupling and high cohesion.


Rules of Thumbs

a class with cohesion has relatively few methods of highly related functionalities and does not do too much of work.


Degree of Cohesion

  • Very low - a class solely responsible for everything in the system
  • Low - a class has sole responsibility for a complex task in 1 functional area
  • High - a class has moderate responsibilities in one functional area and collaborate with other classes to fulfill tasks.
  • Very High - every code statements is separated.

Principle #6 - High Robustness

Robustness is similar to system resiliency and capability to operate under compromised conditions such as errors or runtime hardware faultiness. This includes defensive checking, error handling, and system recoverability.

Ideally, a system should be highly robust where it knows how to handle every dips and errors it obtained without the attention from the stakeholders.


Be Strict with What You Send; Be Liberal with What You Accept

A robust system is able to strictly control what to send out including error and failure handling. However, user can provide any sort of input. Be liberal to accept all kinds inputs and validate them vigorously internally.


Modular Protection

  • All errors and runtime panics should not leak outside the module/function.
  • Explicit interfaces with clear in/out conditions
  • Explicitly document and check exceptions when they are propagated outside of modules.

Example, a printer GUI program crashes should not corrupt the entire operating system. Instead, it is handled locally, logged, informed user, and probably restarted itself automatically.


Mock-able / Stub-able Modules/Functions

To scale all the modules, the interface of the module and its functions should be flexible enough to replace its own functional abstract for different degrees of testing (as a client / as an internal testing). Such facility reduces duplication and complexity when it comes to the software testing domain.


Independently Testable

Modules should be independently testable instead of relying on its dependencies for testing. This way, the module achieves its robustness in terms of portability and reduces integration complexity when an issue (bug/crashes) arises.

Principle #7 - High Reliability

This design principle focuses on data recoverability and restoration speed. A highly reliable design can achieve the following attributes.


Be Redundant

Design is able to backup itself without compromising data integrity and have trust-able and reliable ways to verify data correctness.


Quick Recovery

System has a quick self-restoration mechanism to bring back a failed system into operational mode.

That's all about software design principles.