Back to Home

Application Pyramid

OOP · application architecture · solid · kiss · dry · yagni · inversion of control

Application Pyramid

    Programming is a fairly young field of knowledge, however, the basic principles of "good code" already exist in it, which are considered by most developers as axioms. Everyone has heard of SOLID, KISS, YAGNI, and other three- or four-letter abbreviations that make your code cleaner. These principles affect the architecture of your application, but besides them, there are architectural styles, methodologies, frameworks, and much more.

    Dealing with all this separately, I was interested in the question - how are they interconnected? Trying to build a hierarchy and inspired by the notorious Maslow pyramid , I built my own pyramid of “application architecture”.

    About what came of it - read under the cut.

    About the pyramid

    Architectural pyramid
    A pyramid is just a convenient visualization for visualizing the hierarchy of various principles, styles and methodologies. A pyramid consists of levels, and levels of elements. Each level additionally has its own generic name. The pyramid should be read from bottom to top, from the most basic and general concepts below, to the more particular and specific above. The order of the elements located at the same level does not matter. Elements of the same level are considered as “equivalent” or “equal”.

    Pyramid levels


    Consider each of the levels of the pyramid, successively rising from its base to the top. Each of the principles mentioned in the article is described in detail many times in books and articles. Therefore, I will not describe them in detail, giving only a brief quote and link. Instead, I’ll try to explain why the levels are located exactly this way and how all this can be used.

    Unconditional restrictions


    The basis of the pyramid are objective (physical) limitations for the application. It can be anything: team size, budget, deadline, country legislation, and even the current level of technology development. The main distinguishing feature of these kinds of restrictions is that you cannot influence them.

    Obviously, such restrictions affect the entire project - its architecture, the choice of technology and the way the team is managed.

    Business requirements


    Above the unconditional restrictions are business requirements: functional and / or technical specifications, customer wishes, explicit and implicit functions that the end user expects to receive.

    Business requirements introduce additional (and often basic) restrictions on the application architecture. But they cannot contradict common sense unconditional restrictions. They only further restrict our freedom of choice and therefore are located above the unconditional requirements.

    Difficulty: KISS, YAGNI


    It is unlikely that anyone will argue that the complexity of the system is one of the most important factors affecting all other aspects and, ultimately, the success of the project.
    The principles aimed at combating complexity in application development are KISS ( Keep it simple, stupid ) and YAGNI ( You aren't gonna need it ).

    The KISS Principle calls for simplification:
    most systems work best if they remain simple rather than complicated
    And YAGNI does not design ahead beyond measure:
    You don't need it
    Both of them are very abstract and suitable for any application, which makes them fundamental.

    These principles are very important, but at the same time no one will pay you if, in the pursuit of simplicity, you have ignored half of the client's requirements. Therefore, the principles relating to simplicity have taken pride of place directly above the business requirements of the client.

    Connectivity: DRY, SRP, ISP, high cohesion


    An old joke about an elephant that you need to eat in parts is fully suitable for any complex task. Including, and for the development of large applications. Two principles are responsible for the correct separation of the task elephant into small, isolated and precisely formulated subtasks: DRY ( Don't repeat yourself ) and SRP ( The Single Responsibility Principle ).

    The DRY principle states:
    Each piece of knowledge should have a single, consistent and authoritative representation within the system.
    And the principle of SRP:
    each object must have one responsibility

    It may seem that both principles are the same thing, only in different words, but this is not so. In fact, they complement each other. For example, guided only by DRY, you can create one object sending out mail and calculating tax. If there are no other objects anywhere else in the code with similar functionality, the condition is satisfied. SRP will force you to share responsibilities by assigning them to different objects.

    The ISP ( Interface segregation principle ) states that:
    Clients should not depend on methods that they do not use.
    Suddenly, with this principle I had the biggest problems. It is somewhat similar to YAGNI - "the client may not need these interface methods." On the other hand, he has a lot from SRP - “one interface for one task”. It could be attributed to the generalization of “Complexity” and put on a par with YAGNI and KISS, but these two principles are more abstract.

    SRP and ISP are part of another well-known principle - SOLID, and at first I was worried that in my pyramid these principles were separated from the rest. However, in the end, I decided that not all yogurts are equally useful principles are equal. This does not mean that other SOLID principles can be neglected. Just SRP and ISP, in my opinion, are a little more generalized than the rest.

    High cohesion is a metric that shows how well the code is grouped by functionality. Implementation of the principles of DRY and SRP leads to a code with strong connectivity, in which parts that perform the same task are located "close" to each other, and the different ones are isolated. Implementing ISPs also leads to strong connectivity, although this is not so obvious. Dividing one interface into smaller parts you group them according to functionality, leaving only the most related methods in each interface.

    GRASP: high cohesion, loose / low coupling
    The strong connection above is called the "metric", although it can be fully called a principle. High cohesion, along with Low coupling, are parts of GRASP ( General responsibility assignment software patterns ), which is not covered in this article.

    Dependencies: IoC, DIP, loose coupling


    After we divided the system into fairly simple components, we can move on to the relationships between these components - to dependencies. The complexity and number of connections between the various components of the application also greatly affects its architecture. There are also principles for managing dependencies between components.

    IoC ( Inversion of control ) assumes the presence of some framework that will transfer control to the components of our program at the right time. In this case, the components may not know anything about each other.

    DIP ( Dependency inversion principle ) states:
    Upper level modules should not depend on lower level modules. Both types of modules should depend on abstractions. Abstractions should not depend on the details. Details should depend on abstractions.
    Loose coupling is not a principle, but a metric that shows how independent the system components are from each other. Loosely coupled components are independent of external changes and can easily be reused. IoC and DIP are means to achieve poor connectivity of the components in the system.

    Extension: OCP, LSP


    The requirements for the developed system can change and supplement over time, so it is necessary to lay the possibility of expanding the functionality from the very beginning. But you should not get too carried away, since we need to fulfill the basic principle of YAGNI. That is, it is necessary to find some balance between the possibility of future expansion and the current complexity of implementation.

    The principles related to functional expansion are OCP ( Open / closed principle ) and LSP ( Liskov substitution principle ).

    OCP determines that
    software entities (classes, modules, functions, etc.) must be open for extension, but closed for change
    A LSP:
    Functions that use the base type should be able to use subtypes of the base type without knowing it.
    A competent implementation of these principles will allow in the future to change (expand) the functionality of the application not changing the already written code, but creating a new one.

    Methodologies: TDD, DDD, BDD


    In addition to the principles, there is a rather large set of methodologies, for example, BDD ( Behavior-driven development ) and TDD ( Test-driven development ). In the general case, a methodology, in contrast to a principle, defines a certain process that is applied to application development.
    Methodologies are very diverse, solve different problems, and often, when developing an application, a combination of them is used. But whichever of them you choose, you will have serious problems if you try to apply them to code that violates the previous principles. For example: will it be easy to apply TDD and write tests for code that violates DIP and contains links to specific implementations?

    Architectural styles and frameworks


    To make it clear what this is about, a good, but far from exhaustive list of architectural styles can be found in the Microsoft documentation . Often they are quite orthogonal to each other and can be used together. Each of them usually represents a time-tested and many times successfully implemented approach to building an architecture.

    The frameworks are mentioned here because they force you to use a specific architectural style, whether you want it or not. Actually, this distinguishes the framework from the application library. Many good frameworks are built on the principles of IoC, simplify testing and contain the ability to expand their own functionality. That is, in fact, they are located "on top" of all previous levels and must support and correspond to them.

    Libraries and Tools


    At the very top of the pyramid are specific application libraries and tools that you use every day for logging, operations on matrices and displaying beautiful pop-ups. These libraries solve specific subtasks and should not affect the architecture as a whole. The choice of a library should be based on previous levels of the pyramid, and not vice versa.

    Geometrically, this level is the smallest in size, but ironically, it is this level that often causes the most disputes, discussions and articles.

    Why is all this necessary?


    In my opinion, a pyramid can help when designing a system from scratch or making changes to an existing one. For myself, I present this in the form of a checklist with questions for each level of the pyramid, which need to be answered sequentially.

    If changes are made to the existing system, then it is approximately as follows:

    1. Are my changes feasible taking into account the budget allocated to me, time and other objective restrictions?
    2. Do they contradict business requirements?
    3. Is what I'm going to do just enough? Are there any easier ways to do this?
    4. How can I divide my task into components (subtasks) so that each of the components performs only one action? Am I duplicating existing functionality?
    5. How will my components relate to the rest of the program? How to reduce the number of links? Will I be able to reuse my components or replace one component with another in the future?
    6. Will I be able to expand the functionality of my components without changing them? Have I laid the possibility of expansion in those components whose probability of change in the future is especially high?
    7. Do my changes contradict my chosen methodology?
    8. Are my changes consistent with the best practices of the framework I use? Do they violate the overall architectural style of my code?
    9. Can the libraries used by me solve the given subtask?

    Each item in the list corresponds to a certain level of the pyramid. Moreover, the choice made at each level should not contradict the choice made at previous levels. This is especially important when designing a system from scratch, when the "uncertainty" in terms of future architecture is much higher and wider than the possible choice.

    So, for example, your chosen library should not conflict with the framework used. If this happens, you are changing the library, not the framework. The framework should support (or at least make it possible) to use the methodology chosen at the previous stage and so on.

    In general, an understanding of hierarchy focuses you on more basic principles (lower steps). In addition, the study of the relationships between the elements of the pyramid allows you to better understand the picture "in general", to generalize and systematize your knowledge. For a deeper understanding of any new principle, it will be useful to try to include it in this system, to understand at what level it should be located.

    Conclusion


    This article is subjective enough and does not claim to be an exhaustive description of all existing principles and methodologies. Moreover, already at the time of writing the first draft, a couple of principles moved from their homes to other levels. Perhaps, over time, the pyramid will be supplemented with new elements or even levels. If you think that something is out of place in it or you want to supplement it, I will be glad to constructive criticism and suggestions in the comments.

    Read Next