Simplicity criteria

    The lion's share of programmers with a clear conscience will say that they prefer to solve problems simply, guided primarily by common sense. That's just it is "simple" for each his own and usually different from the others. After one long and unconstructive argument with a colleague, I decided to state what exactly I consider to be simple myself and why. This did not lead to immediate agreement, but allowed to understand each other's logic and minimize unnecessary discussions.


    First criterion


    The features of the human brain are such that it is poorly stored and distinguishes more than 7-9 elements in one list with an optimal number of 1-3.


    Hence the recommendation - ideally, have no more than three members in the interface, three parameters in the method, and so on, and not allow an increase in their number over nine.
    This criterion can be implemented by means of static analysis.


    Second criterion


    The most important thing is in the first two lines of any class.


    1. Name, type parameters, implemented interfaces.
    2. Constructor options.

    As a result, we know about the class everything that is necessary for its use (name, parameters, inputs, outputs), without even looking at the remaining code.
    Of course, this works subject to restrictions:


    1. Avoid implementation inheritance
    2. Avoid dependency injection other than through constructor options.

    And this criterion can be implemented by means of static analysis.


    Third criterion


    One type - one task. Contracts to interfaces, implementations to classes, algorithms to methods!
    Multitools are only good if there is nothing else at hand. Thanks to the first two criteria, writing these types is easy. Yes, this is the principle of sole responsibility (and separation of interfaces to the heap)
    And here we will have to entrust the static analysis to a person.


    Fourth criterion


    Constraints are the same legal part of a contract as method signatures.
    And therefore ...


    1. Comments in the contract are almost always useful, comments in the implementation code are almost always harmful.
    2. DTOs are complete objects whose primitive behavior is rewarded with automatic serialization.
    3. Immutable objects - reward the convenience of validation, the absence of races and unnecessary copying.
    4. The static method is a full-fledged stateless class, all the buns from immutable objects plus less memory traffic.
    5. Anonymous delegates and lambdas - replace a bunch of interface-class with one method, allowing you to throw out two extra types and demonstrate the signature of the call directly in the code.
    6. Add the rest of the "fake objects" to your liking.

    Static analysis here can only help with recommendations for using simpler type options if they see them.


    Fifth criterion


    Maximum usability for reuse.
    Use interface inheritance, avoid implementation inheritance and get three sources of code reuse right away


    1. Reusing contracts is very useful, even if no ready-made implementations work.
    2. Reuse of implementations - wherever a contract is accepted, you can use an already written implementation.
    3. Reuse of client code - all code that works with the interface (including tests) can be reused with each new implementation.

    Yes, this is the principle of substituting Lisk in the most simple and practical form.
    A static analyzer can report the use of unwanted implementation inheritance.
    Also, do not hide types or methods "just in case". Everything should be public, except what needs to be hidden as implementation details.


    Sixth criterion


    Your types should be simpler for composition, decoration, adaptation, organization of the facade, etc., than for change. Subject to the previous criteria, this is not difficult to achieve.
    This will allow reducing as many changes as possible to adding new code instead of rewriting the current one, which will reduce the regression and the need to edit old tests as well.
    An ideal example is interface extension methods that add functionality on the one hand and do not change the original contract and implementation on the other.
    Yes, this is the principle of openness-closeness .
    No, static analysis is of little help here.


    Seventh criterion


    The types of constructor parameters should talk as much as possible about how to use their value.
    For instance:


    1. IService - a service required for implementation
    2. Lazy - a service, which may not be at start, to start using it is necessary to read the Value property, a pause is possible at the first call.
    3. Task - a resource that is obtained asynchronously
    4. Func - parameterized resource factory
    5. Usable - the resource that is needed up to a certain point can be reported about the end of use by calling the Dispose method.

    Alas, static analysis will not help much here.


    Eighth criterion


    Small types are better than large types, since even poorly designed or implemented small types are much easier to fix, rewrite, or delete compared to large ones.
    A large number of small types is not in itself a fatal problem, since types are not a list, but a graph. It’s enough to keep in mind the connections of the current type at the same time, and their number is limited by the first criterion.


    Ninth criterion


    Type dependencies among themselves must be limited


    1. Loops in the type graph should be avoided
    2. You also need to avoid direct reference implementations to each other.
    3. It is highly desirable to use patterns of splitting types into layers, adding a restriction on the dependencies between the types of different layers on each other.

    The goal is to simplify the graph of type dependencies as much as possible. This helps both when navigating through the code, and when determining the possible affect of certain changes.
    Yes, this criterion includes the principle of dependency inversion .
    Yes, static analysis can help here.


    Tenth criterion


    The same as the ninth criterion, but for assemblies. It greatly simplifies life and speeds up assembly, especially if you immediately design with it in mind. For assemblies with contracts, it is easier to maintain backward compatibility (published contracts do not change). Assemblies with implementations can be replaced entirely - they are still not directly dependent on each other.
    By means of static analysis, one assembly can be prohibited from referencing others.


    Eleventh criterion


    All previous criteria may be violated if optimization is necessary. But one thing, in my opinion, always works:
    It’s easier to optimize the correct code than to correct the optimized one.


    Summary


    Writing my own ideas clarified a lot for me.
    I would like to see your criteria for simplicity in the comments.
    Mentioning of static analysis means the possibility of implementation, and not its presence at the current moment.
    Extras and criticism are traditionally welcome.


    Also popular now: