Declarative programming
Declarative programming can improve the overall readability of the program (for people, machines), including not limited to declaring types, declaring dependencies, declaring API paths/methods/parameters, and so on. From a machine-oriented perspective, the advantage of declarative is that it can be easily extracted from these meta-information for two processing times. Declarative is also a reflection of the system as a whole, to find the focus of attention, division, and improve re-usability. From imperative to declarative, it is a shift from what to do to what is needed.
This article focuses on the practice of Egg, transformation, emphasis on the system as a whole, in the specific implementation of functions, such as the use of forEach/map
alternative for
loops, such as find/include
the use indexOf
of alternative details do not do in-depth.
Controller
Controller as a system external interface, involving the front-end interaction, the change brought about by the promotion is the most obvious.
In the Java system, Spring MVC provides some standard annotations to support API definitions, and a common notation is:
@RequestMapping(value = "api/foo/{fooId}", method = RequestMethod.POST)@ResponseBodypublic Result<Void> create(HttpServletRequest request) { Boolean xxxx = StringUtils.isBlank(request.getParameter("fooId")); if (无权限) { ... } ...// 日志记录}
This declarative notation makes it easy to see that a POST API is declared here without the need to look for other business logic. But here are some questions, such as the need to read through the code to know the API's entry is fooId
, and when the logic of the Controller is very complicated? And the logic of authority judgment is more difficult to see.
It is obvious that this is unfriendly to people looking at the code. This kind of writing hides the parameter information that we are concerned about, it is naturally difficult to unify the processing of the parameters, such as parameter formatting, validation and other logic can only be written together with the business logic.
The other way to do this is to declare the parameters:
@RequestMapping(value = "api/foo/{fooId}", method = RequestMethod.POST, name = "创建foo")@ResponseBodypublic Result<Void> create(@PathVariable("fooId") String fooId, Optional<boolean> needBar) { ...}
(Java is also constantly improving, such as the type of JDK 8 added Optional<T>
, combined with Spring can be used to identify parameters as optional)
These are the things within the java/spring design, what are the remaining requirements such as permissions, logs, etc.? In fact, the same is true, this system of concern, can be divided into the way the requirements of the cut out, written as independent annotations, rather than with the business logic written in the method, so that the program to the people, the machine can be more readable.
Abstract permission Facets:
/** * 创建foo * @param fooId * @return */@RequestMapping(value = "api/foo/{fooId}", method = RequestMethod.POST, name = '创建Foo')@Permission(Code = PM.CREATE_FOO) // 假设权限拦截注解@ResponseBodypublic Result<Void> create(@PathVariable("fooId") String fooId) { ...}
Machine oriented
The advantage of declarative is not only more readable to people, but also more convenient when using the procedure to analyze. For example, in daily development, there is often a need for the backend staff to provide API interface documentation and other information, the most common way to generate documents is to write perfect comments, and then through the Javadoc can easily write detailed documents, with the Doclet API can also customize the Tag, Implement your custom requirements.
Comments are a complement to the code, the more information that can be extracted from the code, the less redundant information can be in the comments, and the declarative can reduce the cost of the extraction.
Thanks to the reflection mechanism of Java, it is easy to extract information such as the route of the interface based on the code, and it can further simplify the front-end invocation cost by directly generating the SDK from the front-end call.
*asp.net WebAPI also has a good implementation, see Official support: Microsoft.AspNet.WebApi.HelpPage
Egg
With the Java precedent, is it possible to do the corresponding optimization in EGG? Yes, of course, there are TypeScript assists in the type, and compared to Java annotations, the adorner in JavaScript is basically enough.
Before transformation:
// app/controller/home.jsexport class HomeController { async getFoo() { const { size, page } = this.ctx; ... }}// app/router.jsexport (app) => { app.get('/api/foo', app.controller.home.getFoo);}
After transformation:
// app/controller/home.tsexport class HomeController { @route('/api/foo', { name: '获取Foo数据' }) async getFoo(size: number, page: number) { // js的话,去掉类型即可 ... }}
Using the adorner API can be similar to Java, this way also regulate the way and information to register the route, so as to generate API documentation, the front-end SDK, such as the ability of course, can also be achieved, details: Egg-controller plug-in
The problem with JavaScript implementations is the lack of types, which are not written in the code, and are sufficient for simple scenarios. Of course, we can also use TypeScript to provide type information.
TypeScript
In fact, the cost of switching from JavaScript to TypeScript is very low, the simplest way is to change the suffix from JS to ts, only where needed to write the type. The type system brings a lot of convenience, editor smart tips, type checking, and so on. Like the Controller in the API entry type, sooner or later will be written again, whether it is in the code, comments or documents, so why not do it together? And now EGG official also provides a convenient use for TypeScript, you can try it.
Reflection/Meta Data
TypeScript in this respect compared to java/c# is still much weaker, can only support the basic metadata requirements, and because of the JavaScript itself module loading mechanism, TypeScript only for use of decorators Function, Class to add meta data. Information such as generics, complex type fields, etc. is not available. However, there are curves of the solution, TypeScript provides the Compiler API, you can add plug-ins at compile time, and in the compilation period, because it is for the TypeScript code, so you can get rich information, but the processing is relatively difficult.
Dependency Injection
Declarative programming can also be applied at other component levels to improve readability, and dependency injection is a typical approach.
When we split two component classes, A relies on B, the simplest notation:
class A { foo() {}}class B { bar() { const a = new A(); }}
You can see that B directly instantiates object A, and when there are multiple classes that depend on a? This writing can lead to the creation of multiple instances of a, and in an environment where the Service is likely to be needed ctx
, it is const a = new A(this.ctx);
obviously not feasible to do so.
The solution for egg is to load classes through the loader mechanism, ctx
set up multiple getters, unify management instances, initialize instances at first access, and use them in the egg project:
public class FooService extends Service { public foo() { this.ctx.service.barService.bar(); ... }}
In order to realize the management of the instance, all the components are mounted uniformly ctx
on, the benefit is that the mutual exchange of different components is very easy to ask, but in order to achieve the exchange of visits, each component is strongly dependent ctx
, by ctx
looking for components, you should also see, This is actually the service locator pattern in design mode. Type definitions can be problematic under TypeScript, but egg does an auxiliary tool that generates a corresponding type definition based on the component code that conforms to the directory specification and merges the attributes of the TypeScript merge declaration into the egg. This is also the current cost-effective solution.
The advantage of this scheme is the convenience of mutual access, the disadvantage is that there are ctx
many ctx
components that are not related to itself, the resulting ctx
type is distribution definition, more complex, and hides the dependencies between components, need to look at the specific business logic to know the dependencies between components.
How does that work in the java/c#? In java/c#, AOP/IOC are basically standard for each frame, such as in Spring:
@Componentpublic class FooService { @Autowired private BarService barService; public foo() { barService.bar(); ... }}
Of course, in Java is generally the declaration of the injection IFooService
interface, and then implement one IFooServiceImpl
, but the front-end is basically no one to do so, there is no such a complex demand scenario. So what you can do with dependency injection at the front end is to explicitly declare the dependencies and decouple them from ctx
the unrelated components ctx
.
The use of dependency injection in EGG transformation is as follows:
public class FooService extends Service { // 如果不依赖 ctx 数据,也可以不继承 // ts @lazyInject() barService: BarService; // js @lazyInject(BarService) barService; public foo() { this.barService.bar(); ... }}
After changing the wording, it can be intuitively seen that Fooservice relies on barservice, and no longer through CTX to obtain barservice, improve readability. The focus of dependency injection as an instantiated component is that it can easily implement some facets-oriented gameplay, such as dependency graphs, function call tracking, and so on.
Egg-aop,egg under AOP/IOC plug-in
Conclusion
The code is the best document, the readability of the code is very important for subsequent maintainability, and the cost of the subsequent maintenance is related to human readable, while the possibility of machine readable relates to automation. Declarative programming is more about describing what to do or what to do, which can help when describing the relationship between modules/systems, whether it's automating output documentation or automatically generating calling code or mock docking, and so on, which reduces duplication of effort, and in the era of intelligence, data represents another possibility.