📜 ⬆️ ⬇️

Simplicity criteria

The lion's share of programmers with a clear conscience will declare that they prefer to solve problems simply, guided first of all by common sense. That's just this "just" everyone has their own and usually different from others. After one long and unconstructive dispute with a colleague, I decided to state what I consider to be simple myself and why. This did not lead to immediate agreement, but made it possible to understand each other’s logic and minimize unnecessary discussions.


First criterion


The peculiarities of the human brain are such that it badly stores and distinguishes more than 7-9 elements in one list with the optimum number of 1-3.


Hence the recommendation - ideally to have no more than three members in the interface, three parameters in the method, and so on, and not to 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 all that is needed to use it (name, parameters, inputs, outputs), even without looking at the remaining code.
Of course, this works subject to limitations:


  1. Avoid inheriting implementations.
  2. Avoid dependency injection other than through constructor parameters.

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 good only if there is nothing else at hand. Thanks to the first two criteria, writing such types is easy. Yes, this is the principle of sole responsibility (and separation of interfaces to the heap)
And here a static analysis will have to be trusted to a person.


Fourth criterion


Restrictions - the same legal part of the contract, as well 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 full-fledged objects whose primitive behavior is rewarded by automatic serialization.
  3. Immutable objects - reward us with the convenience of validation, lack of racing and unnecessary copying.
  4. The static method is a full-fledged class with no state, all the buns from immutable objects plus less memory traffic.
  5. Anonymous delegates and lambdas replace the bundle of the interface class with one method, allowing you to throw out two extra types and demonstrate the call signature right in the code.
  6. Add the rest of the fake objects to your liking.

Static analysis here can help only with recommendations on the use of simpler variants of types, if they are seen.


Fifth criterion


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


  1. Contract reuse is very useful, even if no ready-made implementation has come up.
  2. Reuse of implementations - wherever a contract is accepted, an already written implementation can be used.
  3. Client code reuse - all code that works with the interface (including tests) can be reused with each new implementation.

Yes, this is the Liskov principle of substitution in the simplest and most practical way possible.
A static analyzer may report unwanted implementation inheritance.
Also, do not hide the types or methods "just in case." Everything should be public, except for the fact that it is necessary to hide them as implementation details.


Sixth criterion


Your types should be simpler for composition, decoration, adaptation, facade organization, etc., than for change. With previous criteria, this is easy to achieve.
This will allow as many changes as possible to add to the addition of new code instead of rewriting the current one, which will reduce the regression and the need to edit old tests as well.
A perfect 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 speak as much as possible about how to use their meaning.
For example:


  1. IService - the service needed to implement
  2. Lazy <IService> is a service that may not be present at the start, you should read the Value property to start using, a pause is possible at the first call.
  3. Task <IResource> - a resource that is obtained asynchronously
  4. Func <IArgument, IResource> - parameterized resource factory
  5. Usable <IResource> - a resource that is needed up to a certain point; you can report the end of use by calling the Dispose method.

Alas, static analysis does little to help.


Eighth criterion


Small types are better than large ones, since even poorly designed or realized 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. In the head at the same time it is enough to keep the connections of the current type, and their number is limited by the first criterion.


Ninth criterion


Type dependencies between themselves must be limited


  1. Cycles in the type column should be avoided.
  2. You should also avoid direct references of implementations to each other.
  3. It is highly desirable to use the pattern-splitting patterns, adding a constraint on the dependencies between the types of different layers on each other.

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


Tenth criterion


Same as the ninth criterion, but for assemblies. Strongly simplifies life and speeds up the assembly, especially if you immediately design with its account. For contract assemblies, it is easier to maintain backward compatibility (published contracts do not change). Builds with implementations can be replaced entirely - they still do not directly depend on each other.
By means of static analysis, it is possible to prohibit some assemblies from referring to others.


Eleventh criterion


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


Results


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


')

Source: https://habr.com/ru/post/312950/


All Articles