
关于lsFusion语言的系列文章的第三部分和最后一部分(链接到
第一部分和
第二部分)
它将着重于物理模型:当数据过多时,与系统功能无关但与开发和性能优化相关的所有事物。
与以前的文章一样,本文不太适合娱乐阅读,但是与其他文章不同,本文将提供更多的技术细节和“热门”主题(例如键入或元编程),此外,本文还将提供部分问题答案,这是怎么回事在里面工作。
在本文中,我们将没有图片(没有这样的堆栈),但是将按照前几篇文章的要求制作目录:
物品识别
如果项目由几个小文件组成,则通常不会出现元素命名问题。 所有名称都在眼前,而且很容易确保它们不会重叠。 相反,如果项目由许多不同的人开发的许多模块组成,并且这些模块从一个域中进行抽象,则名称冲突的可能性就更大。 LsFusion有两种机制可以解决这些问题:
- 命名空间-名称分为全名和简称,并且在访问元素时只能使用简称
- 显式输入(更精确地说,函数重载)-能够以相同的方式命名属性(和动作),然后在访问它们时,根据参数的类别,自动确定调用要访问的属性的能力
命名空间
任何复杂的项目通常都包含大量必须命名的元素。 而且,如果域域相交,则经常需要在不同的上下文中使用相同的名称。 例如,我们有一个类或表单发票(发票)的名称,并且我们希望在各种功能块中使用此名称,例如:购买(购买),销售(销售),购买退货(购买退货),销售退货(销售退货)。 显然,类/表单可以称为PurchaseInvoice,SaleInvoice等。 但是,首先,这样的名称本身将过于庞大。 其次,在一个功能块中,通常会调用同一功能块的元素,这意味着例如在不断重复购买一词来开发例如Purchase功能块时,它只会在您的眼中荡漾。 为了防止这种情况发生,平台具有名称空间的概念。 其工作方式如下:
- 平台中的每个元素都在某个命名空间中创建
- 如果在创建元素的过程中引用了其他元素,则在相同名称空间中创建的元素具有优先权
当前语言版本的命名空间将立即在模块头中为整个模块设置。 默认情况下,如果未指定名称空间,则会使用与模块名称相同的名称隐式创建名称空间。 如果需要从非优先级名称空间访问元素,则可以通过指定元素的全名来完成此操作(例如,Sale.Invoice)。
显式输入
命名空间很重要,但不是使代码更短且更具可读性的唯一方法。 除了它们,在搜索属性(和动作)时,还可以考虑在输入处传递给它们的参数的类别。 因此,例如:
当然,这里可能会出现问题:如果所需属性的命名空间不是优先级,但更适合于类,将会发生什么? 实际上,一般的搜索算法非常复杂(其完整描述在
此处 ),并且存在很多此类“模棱两可”的情况,因此,在不确定的情况下,建议要么明确指定所需属性的名称空间/类,要么在IDE中进行仔细检查(使用转到声明- CTRL + B),找到的属性正是这个意思。
另外,值得注意的是,通常不需要在lsFusion中进行显式键入。 可以省略参数类,并且如果平台具有足够的信息来查找所需的属性,则它将这样做。 另一方面,在非常复杂的项目中,不仅从代码简洁的角度来看,还是从各种附加功能的角度来看,仍然建议显式设置参数类,例如:早期错误诊断,IDE的智能自动完成等等。 我们在使用隐式打字(最初的5年)和显式打字(剩余时间)方面都有丰富的经验,我必须说,现在用颤抖的方式来记住隐式打字的时代(尽管它可能只是“我们不知道如何烹饪”)。
模块化
模块化是系统最重要的特性之一,它可以确保其可扩展性,代码重用以及开发团队的有效交互。
LsFusion通过以下两种机制提供模块化:
- 扩展-在创建系统元素后对其进行扩展(更改)的能力。
- 模块-将某些功能组合在一起以进一步重用的能力。
扩展名
lsFusion通过第一篇文章中描述的多态性机制支持扩展类和形式以及属性和动作的能力。
另外,我们注意到几乎所有其他平台设计(例如,导航器,表单设计)都可以通过定义进行扩展,因此没有单独的扩展逻辑。
模组
模块是项目中功能上完整的一部分。 在当前版本的lsFusion中,模块是一个单独的文件,由模块的头和主体组成。 反过来,模块的标题包括:模块的名称,以及(如有必要)使用的模块列表和该模块的名称空间的名称。 模块的主体由系统元素的声明和/或扩展组成:系统属性,操作,限制,形式,元代码等。
通常,模块使用其他模块中的元素来声明自己的元素/扩展现有元素。 因此,如果模块B使用模块A中的元素,则有必要在模块B中指出它依赖于A。
基于它们的依赖关系,项目中的所有模块都以一定的顺序排列,并按照这些顺序进行初始化(在使用上述扩展机制时,此顺序起着重要的作用)。 如果模块B依赖于模块A,则可以保证模块A的初始化要早于模块B的初始化。不允许项目中各个模块之间存在循环依赖性。
模块之间的依赖关系是可传递的。 即,如果模块C依赖于模块B,而模块B依赖于模块A,则认为模块C也依赖于模块A。
任何模块始终自动依赖于系统模块System,无论是否明确指示该模块。
元编程
元编程是一种与编写程序代码关联的程序设计,因此会生成另一个程序代码。 LsFusion使用所谓的元代码进行元编程。
元代码包含:
- 元代码名称
- 元代码参数
- 元代码的主体-由系统元素的声明和/或扩展(属性,动作,事件,其他元代码等)组成的代码块
因此,在开始进行代码的主要处理之前,平台会进行准备-用这些元代码的主体替换元代码的所有用法。 在这种情况下,标识符/字符串文字中使用的所有元代码参数都将替换为传递给该元代码的参数:
公告:
用法:
结果代码:
除了简单地替换元代码参数外,该平台还允许您将这些参数与现有的标识符/字符串文字(或彼此)结合起来,例如:
公告:
用法:
结果代码:
元代码与C中的宏非常相似,但与后者不同,它们在文本级别不起作用(例如,它们不能在参数中传递关键字),而仅在标识符/字符串文字级别(特别是这种限制允许)在IDE中解析元代码主体)。
在lsFusion中,元代码解决的问题类似于Java中的泛型(将类作为参数传递)和FP中的lambda(将函数作为参数传递),但是,它们做起来并不十分完美。 但是,另一方面,它们是在更通用的情况下执行此操作的(例如,可以组合标识符,在任何语法构造中使用-表单,设计,导航器等)。
请注意,不仅平台本身也支持IDE的元代码“部署”。 因此,在IDE中,有一个特殊的模式Enable meta,它可以直接在源代码中生成结果代码,从而允许此生成的代码参与对用途,自动完成等的搜索。 在这种情况下,如果元代码的主体发生更改,IDE将自动更新此元代码的所有用法。

同样,元代码不仅可以用于自动代码,还可以用于手动代码生成(作为模板)。 为此,只需编写@@而不是一个@,然后在完全输入元代码用法字符串(直到分号)后,IDE就会用此元代码生成的代码替换此元代码用法:

整合性
集成包括与lsFusion系统与其他系统的交互有关的所有内容。 从这种交互的方向来看,集成可以分为:
- 从另一个系统访问lsFusion系统。
- 从lsFusion系统访问另一个系统。
从物理模型的角度来看,集成可以分为:
- 与在与lsFusion系统“相同的环境”中运行的系统(即,在lsFusion服务器的Java虚拟机(JVM)中和/或使用与lsFusion系统相同的SQL服务器)中的系统进行交互。
- 通过网络协议与远程系统进行交互。
因此,第一个系统称为内部系统,第二个称为外部系统。
因此,平台中有四种不同类型的集成:
- 呼吁外部系统
- 来自外部系统的上诉
- 呼吁内部系统
- 来自内部系统的上诉
呼吁外部系统
在大多数情况下,使用特殊的EXTERNAL运算符可以访问lsFusion中的外部系统。 该运算符以给定外部系统的语言/范例执行给定代码。 另外,该运算符允许您将原始类型的对象作为此类调用的参数进行传输,并将调用结果写入指定的属性(不带参数)。
当前,该平台支持以下类型的交互/外部系统:
HTTP-执行来自Web服务器的http请求。对于这种类型的交互,必须指定查询字符串(URL),该字符串同时确定服务器地址和需要执行的请求。 既可以在查询行(也可以使用特殊字符$和该参数的编号,从1开始)中在查询行及其主体(BODY)中传输参数。 假定所有未在查询字符串中使用的参数都传递给BODY。 如果BODY中有多个参数,则传输期间的BODY内容类型将设置为multipart / mixed,并将参数作为该BODY的组成部分进行传输。
在BODY中处理文件类(FILE,PDFFILE等)的参数时,取决于文件扩展名(根据下
表 )确定参数的内容类型。 如果文件扩展名不在此表中,则内容类型设置为application / <文件扩展名>。
如有必要,可以使用特殊选项(HEADERS)设置已执行请求的标题。 为此,您需要指定一个属性,该属性的确切位置是将要存储标题的字符串类的一个参数,以及要存储此标头的值的字符串类的值。
http-request的结果以与其参数相同的方式进行处理,只是方向相反:例如,如果下
表中存在结果的内容类型或等于application / *,则认为获得的结果是文件,应将其写入具有FILE值的属性。 通过类似于此请求本身的标头来处理http请求结果的标头(唯一的区别是该选项称为HEADERSTO,而不是HEADERS)。
SQL-执行SQL Server命令。对于这种类型的交互,将指定连接字符串和要执行的SQL命令。 可以在连接字符串和SQL命令中传递参数。 要访问该参数,将使用特殊字符$和该参数的编号(从1开始)。
文件类的参数(FILE,PDFFILE等)只能在SQL命令中使用。 此外,如果执行期间的任何参数是TABLE文件(TABLEFILE或具有表扩展名的FILE),那么在这种情况下,该参数也被视为表:
- 在执行SQL命令之前,每个这样的参数的值都会在临时表中加载到服务器上
- 替换参数时,替换的不是参数值本身,而是创建的临时表的名称
执行结果为:对于DML查询-数量等于已处理记录的数量,对于SELECT查询-包含这些查询结果的TABLE格式文件(带有表扩展名的FILE)。 这些结果的顺序与SQL命令中相应查询的执行顺序一致。
LSF-另一个lsFusion服务器的动作调用。对于这种类型的交互,设置了与lsFusion服务器(或其Web服务器,如果有的话)的连接字符串,将要执行的动作以及属性列表(无参数),这些属性的值将被写入调用结果。 要传输的参数在数量和类别上必须与要执行的操作的参数一致。
在这种类型的交互中设置操作的方法与从外部系统进行访问时设置操作的方法完全一致(有关下一节中的这种类型的访问)。
默认情况下,这种类型的交互是通过HTTP协议使用适当的接口来实现的,该接口用于访问外部系统。
如果您需要使用与上述协议不同的协议来访问系统,则始终可以通过在Java中创建一个动作并在其中实现此调用来实现此目的(但是稍后在“访问内部系统”一节中将对此进行更多介绍)
来自外部系统的上诉
该平台允许外部系统使用HTTP网络协议访问在lsFusion上开发的系统。 交互的界面是使用给定的参数调用某些操作,并在必要时返回某些属性(不带参数)的值作为结果。 假定所有参数和结果对象都是原始类型的对象。
可以通过以下三种方式之一设置被叫动作:
- / exec?action = <动作名称>-设置被调用动作的名称。
- / eval?script = <code>-在lsFusion中设置代码。 假定在此代码中有一个名为run的动作的声明,将调用此动作。 如果未指定script参数,则假定代码作为第一个BODY参数传递。
- / eval / action?脚本= <action code>-在lsFusion中设置动作代码。 要访问参数,可以使用特殊字符$和参数编号(从1开始)。
在第二种和第三种情况下,如果未指定script参数,则假定代码由第一个BODY参数传递。
参数和结果的处理与使用HTTP协议访问外部系统对称(唯一的区别是将参数作为结果处理,相反,将结果作为参数处理),因此我们不会重复太多。
例如,如果我们有一个动作:
然后,您可以使用POST请求访问它:
- URL-http:// server_address / exec?Action = importOrder&p = 123&p = 2019-01-01
- BODY-具有查询字符串的json文件
Python调用示例 import json import requests from requests_toolbelt.multipart import decoder lsfCode = ("run(INTEGER no, DATE date, FILE detail) {\n" " NEW o = FOrder {\n" " no(o) <- no;\n" " date(o) <- date;\n" " LOCAL detailId = INTEGER (INTEGER);\n" " LOCAL detailQuantity = INTEGER (INTEGER);\n" " IMPORT JSON FROM detail TO detailId, detailQuantity;\n" " FOR imported(INTEGER i) DO {\n" " NEW od = FOrderDetail {\n" " id(od) <- detailId(i);\n" " quantity(od) <- detailQuantity(i);\n" " price(od) <- 5;\n" " order(od) <- o;\n" " }\n" " }\n" " APPLY;\n" " EXPORT JSON FROM price = price(FOrderDetail od), id = id(od) WHERE order(od) == o;\n" " EXPORT FROM orderPrice(o), exportFile();\n" " }\n" "}") order_no = 354 order_date = '10.10.2017' order_details = [dict(id=1, quantity=10), dict(id=2, quantity=15), dict(id=5, quantity=4), dict(id=10, quantity=18), dict(id=11, quantity=1), dict(id=12, quantity=3)] order_json = json.dumps(order_details) url = 'http://localhost:7651/eval' payload = {'script': lsfCode, 'no': str(order_no), 'date': order_date, 'detail': ('order.json', order_json, 'text/json')} response = requests.post(url, files=payload) multipart_data = decoder.MultipartDecoder.from_response(response) sum_part, json_part = multipart_data.parts sum = int(sum_part.text) data = json.loads(json_part.text)
呼吁内部系统
内部交互有两种类型:
Java互操作性这种交互类型使您可以在JVM lsFusion服务器内部调用Java代码。 为此,您必须:
- 确保在应用程序服务器的类路径中可访问编译的Java类。 此类也必须继承lsfusion.server.physics.dev.integration.internal.to.InternalAction。
Java类示例 import lsfusion.server.data.sql.exception.SQLHandledException; import lsfusion.server.language.ScriptingErrorLog; import lsfusion.server.language.ScriptingLogicsModule; import lsfusion.server.logics.action.controller.context.ExecutionContext; import lsfusion.server.logics.classes.ValueClass; import lsfusion.server.logics.property.classes.ClassPropertyInterface; import lsfusion.server.physics.dev.integration.internal.to.InternalAction; import java.math.BigInteger; import java.sql.SQLException; public class CalculateGCD extends InternalAction { public CalculateGCD(ScriptingLogicsModule LM, ValueClass... classes) { super(LM, classes); } @Override protected void executeInternal(ExecutionContext<ClassPropertyInterface> context) throws SQLException, SQLHandledException { BigInteger b1 = BigInteger.valueOf((Integer)getParam(0, context)); BigInteger b2 = BigInteger.valueOf((Integer)getParam(1, context)); BigInteger gcd = b1.gcd(b2); try { findProperty("gcd[]").change(gcd.intValue(), context); } catch (ScriptingErrorLog.SemanticErrorException ignored) { } } }
- 使用特殊内部调用运算符(INTERNAL)注册操作
- 可以像调用其他操作一样调用已注册的操作。 在这种情况下,将执行指定的Java类的executeInternal方法(lsfusion.server.logics.action.controller.context.ExecutionContext上下文)。
SQL交互通过这种类型的交互,您可以访问已开发的lsFusion系统使用的SQL Server的对象/语法构造。 为了在平台中实现这种类型的交互,使用了一个特殊的运算符-FORMULA。 使用此运算符,您可以创建一个属性,该属性对SQL语言中的某些公式进行求值。 该公式以字符串形式设置,在字符串中使用特殊字符$和该参数的编号来访问参数(从1开始)。 因此,所获得的属性的参数数量将等于所使用的参数数量的最大值。
建议仅在以下情况下使用此运算符:在无法借助其他运算符来解决任务的情况下,以及在保证可以使用哪些特定SQL服务器或使用的语法构造符合最新SQL标准之一的情况下。
来自内部系统的上诉
一切都是对称的,以吸引内部系统。 交互有两种类型:
Java互操作性在这种类型的交互的框架内,内部系统可以直接访问lsFusion系统的Java元素(如普通的Java对象)。 因此,您可以执行与使用网络协议相同的所有操作,但同时避免此类交互的大量开销(例如,参数序列化/结果反序列化等)。 此外,如果交互非常紧密(也就是说,在执行一个操作期间,需要双向保持恒定接触-从lsFusion系统到另一个系统,反之亦然)和/或需要访问特定的平台节点,则这种通信方法更加方便和高效。
为了直接访问lsFusion系统的Java元素,您首先需要获得指向某个对象的链接,该对象将具有用于查找这些Java元素的接口。通常通过以下两种方式之一完成此操作:- 如果最初的调用来自lsFusion系统(通过上述机制),那么作为“搜索对象”,您可以使用该调用“通过”的操作对象(此操作的类应从lsfusion.server.physics.dev.integration继承。 internal.to.InternalAction,后者具有所有必需的接口)。
- 如果必须从其方法访问lsFusion系统的对象是Spring Bean,则可以使用依赖项注入获得到业务逻辑对象的链接(该bean分别称为businessLogics)。
Java类示例 import lsfusion.server.data.sql.exception.SQLHandledException; import lsfusion.server.data.value.DataObject; import lsfusion.server.language.ScriptingErrorLog; import lsfusion.server.language.ScriptingLogicsModule; import lsfusion.server.logics.action.controller.context.ExecutionContext; import lsfusion.server.logics.classes.ValueClass; import lsfusion.server.logics.property.classes.ClassPropertyInterface; import lsfusion.server.physics.dev.integration.internal.to.InternalAction; import java.math.BigInteger; import java.sql.SQLException; public class CalculateGCDObject extends InternalAction { public CalculateGCDObject(ScriptingLogicsModule LM, ValueClass... classes) { super(LM, classes); } @Override protected void executeInternal(ExecutionContext<ClassPropertyInterface> context) throws SQLException, SQLHandledException { try { DataObject calculation = (DataObject)getParamValue(0, context); BigInteger a = BigInteger.valueOf((Integer)findProperty("a").read(context, calculation)); BigInteger b = BigInteger.valueOf((Integer)findProperty("b").read(context, calculation)); BigInteger gcd = a.gcd(b); findProperty("gcd[Calculation]").change(gcd.intValue(), context, calculation); } catch (ScriptingErrorLog.SemanticErrorException ignored) { } } }
SQL交互系统可以访问lsFusion系统的SQL Server(例如,其中之一就是SQL Server本身)的SQL交互系统可以使用SQL Server工具直接访问由lsFusion系统创建的表和字段。应该牢记的是,如果读取数据相对安全(除了可能的表及其字段的删除/修改之外),则在写入数据时不会触发任何事件(因此,使用它们的所有元素-限制,汇总等)。 n。),并且也不会重新实现。因此,强烈建议不要将数据直接写到lsFusion系统的表中,如果仍然有必要,则必须考虑所有上述功能。请注意,这种直接交互(但仅用于阅读)对于与各种OLAP系统集成特别方便,在该系统中,整个过程应该以最小的开销进行。迁移
实际上,由于各种原因而必须更改系统现有元素的名称时,通常会出现这种情况。如果要重命名的元素不与任何主要数据相关联,则可以完成此操作而无需任何不必要的手势。但是,如果此元素是主要属性或类,则这种“安静”的重命名将导致以下事实:该主要属性或类的数据将完全消失。为了防止这种情况,开发人员可以创建一个特殊的迁移文件migration.script,将其放在服务器类路径中,并在其中指示旧元素名称与新名称的对应方式。全部工作如下:迁移由块组成,这些块描述在数据库结构的指定版本中所做的更改。启动服务器时,将应用迁移文件中所有版本高于数据库中存储版本的更改。更改是根据版本进行的,从较小的版本到较大的版本。如果数据库结构成功更改,则所有已应用块的最大版本将作为当前块的最大版本写入数据库。每个块的描述语法如下: V< > { 1 ... N }
反过来,更改具有以下类型:对于用户数据的迁移,仅涉及前三种更改(主要属性,类,静态对象的更改)。其余四种类型的更改是必需的:- 用于元数据迁移(安全策略,表设置等)
- 以优化用户数据的迁移(以免重新计算聚合,并且不再在表之间再次传输数据)。
因此,如果不需要元数据迁移或数据不多,则可以省略迁移脚本中的此类更改。值得注意的是,通常在生成迁移脚本方面的大部分工作都是使用IDE执行的。因此,重命名大多数元素时,可以指定特殊复选框“更改迁移文件”(默认情况下启用),IDE会自动生成所有必需的脚本。国际化
实际上,有时会出现一种情况,那就是必须能够使用不同语言的一个应用程序。此任务通常归结为本地化用户看到的所有字符串数据,即:文本消息,属性标题,操作,表单等。lsFusion中的所有这些数据都是分别使用字符串文字(用单引号引起的字符串,例如“ abc”)设置的,它们的本地化操作如下:- 字符串而不是要本地化的文本,而是用大括号括起来的字符串标识字符串数据(例如,“ {button.cancel}”)。
- 当将此字符串发送到服务器上的客户端时,它将搜索在字符串中找到的所有标识符,然后在所需语言环境(即客户端语言环境)的所有ResourceBundle项目文件中搜索每个标识符,并且当找到正确的选项时,括号中的标识符将替换为相应的文本。
ServerResourceBundle.properties: scheduler.script.scheduled.task.detail=Script scheduler.constraint.script.and.action=In the scheduler task property and script cannot be selected at the same time scheduler.form.scheduled.task=Tasks
ServerResourceBundle_ru.properties scheduler.script.scheduled.task.detail= scheduler.constraint.script.and.action= scheduler.form.scheduled.task=
优化大型数据项目的性能
如果系统很小,并且其中的数据相对较少,那么通常无需任何其他优化即可非常有效地工作。如果逻辑变得非常复杂,并且数据量显着增加,则有时有必要告诉平台如何最好地存储和处理所有这些数据。该平台有两种主要的数据处理机制:属性和操作。第一个负责存储和计算数据,第二个负责将系统从一种状态转移到另一种状态。而且,如果可以非常有限地优化操作的工作(包括由于后效应),那么对于属性而言,有一套完整的功能可以减少特定操作的响应时间并提高整体系统性能:- . ( , ), , , .
- . , , .
- . / , . «» .
平台中几乎所有聚合的属性都可以实现。在这种情况下,该属性将不断存储在数据库中,并且当该属性所依赖的数据发生更改时,该属性会自动更新。此外,当读取这种物化属性的值时,将直接从数据库中读取这些值,就像该属性是主要属性一样(并非每次都计算)。相应地,所有主要性能都通过定义得以实现。只有当存在有限数量的对象集且该属性的值不为NULL时,才能实现该属性。通常,最近的文章中对实现的主题进行了足够详细的研究。 关于数据库中读写之间的平衡,因此我认为在这里详细介绍它没有多大意义。指标
通过属性构建索引允许您以有序的方式在数据库中存储此属性的所有值。因此,每当索引属性的值更改时,索引都会更新。借助索引,例如,如果正在进行按索引属性进行的过滤,则可以非常快速地找到必要的对象,而不用查看系统中所有现有的对象。只能索引物化属性(来自上一节)。索引也可以一次建立在多个属性上(例如,如果立即对这几个属性进行过滤,这将很有效)。另外,属性参数可以包括在这样的复合索引中。如果指定的属性存储在不同的表中,则尝试建立索引将导致相应的错误。桌子
LsFusion使用关系数据库来存储和计算属性值。所有主要属性以及标记为实现的所有聚合属性都存储在数据库表的字段中。对于每个表,都有一组名称为key0,key1,...,keyN的键字段,其中存储了对象的值(例如,对于自定义类,这些对象的标识符)。所有其他字段都以这样的方式存储属性值,即在每一行的相应字段中都是键字段中对象的属性值。创建表时,必须指定一个对象类别列表,这些对象将成为此表中的键。对于每个属性,您可以指定将其存储在哪个表中。在这种情况下,表键的数量必须与属性参数的数量匹配,并且参数类必须与该表的键类匹配。如果未为该属性显式设置要存储该表的表,则该属性将自动置于系统中现有的“最近”表中(即,其键数与该属性的参数数一致,并且其键类与参数类最接近) )在DBMS中存储属性的表和字段的名称是根据指定的命名策略形成的。当前,该平台支持三种标准命名策略。如有必要,对于每个属性,开发人员可以显式指定将存储此属性的字段的名称。此外,如果由于某些原因上述内容不合适,则可以创建自己的用于命名属性字段的策略。选择命名策略时,请牢记重要的一点是:如果物化属性的数量足够大,则使用太短的属性命名策略会使这些属性的命名大大复杂化(以使其具有唯一性),或者相应地,导致经常需要显式命名这些属性将存储在其中的字段。结论
正如他们在一部著名的动画电影中所说:“我们建造,建造并最终建造。”当然,它可能有点肤浅,但是我认为您可以为这三篇文章弄清楚lsFusion语言/平台的基本功能。现在是进入最有趣的部分的时候了-与其他技术进行比较。正如Habr的经验和格式所显示的那样,要更有效地做到这一点,不是靠自己,而是在国外。也就是说,不是从机遇中来,而是从问题中解决,而不是谈论它们的优点,而是谈论替代技术的缺点,以及它们自己的术语。在保守的市场中,通常更容易理解这种方法,并且我认为,无论如何,在后苏联时期,这样的市场都是信息系统发展的市场。因此很快就会有几篇类似的文章:“为什么不……?”,而且我相信它们会比这些非常无聊的教程更加有趣。