我们如何为广告系统构建UI

图片

而不是加入


在我们博客的前面,我们写了IPONWEB做什么 -我们使Internet上的广告显示自动化。 我们的系统不仅基于历史数据做出决策,而且还积极使用实时获得的信息。 在使用DSP(需求方平台-广告客户的广告平台)的情况下,广告客户(或其代表)必须以一种格式(图片,视频,交互式横幅,图片+文字等)创建并上传广告横幅(创意)。 ,选择将向其显示此横幅广告的用户的受众群体,确定可以向一个用户展示多少次,在哪个国家/地区,在哪个网站,在哪个设备上展示广告,并在广告系列的定位设置中反映(以及更多),以及分发广告预算 s。 对于SSP(供应方平台-广告平台所有者的广告平台),站点所有者(移动应用程序,广告牌,电视频道)必须确定其站点上的广告位并指出例如准备在其上显示的广告类别。 所有这些设置都是使用用户界面预先手动设置的(不是在广告发布时)。 在本文中,我将讨论我们构建此类接口的方法,前提是存在许多彼此相似且同时具有各自特征的接口。

一切如何开始


图片

我们从2007年开始做广告业务,但我们并没有立即进行界面制作,只是在2014年。 传统上,我们从事定制平台的开发,这些定制平台是完全根据每个客户的业务需求而设计的-在我们构建的数十个平台中,没有两个完全相同。 而且由于我们的广告平台在设计时没有对定制可能性的限制,因此用户界面必须满足相同的要求。

五年前,当我们收到第一个针对DSP广告接口的请求时,我们的选择落在了流行且便捷的技术栈上:前端是JavaScript和AngularJS,而后端是Python,Django和Django Rest Framework(DRF)。 由此,创建了最普通的项目,其主要任务是提供CRUD功能。 他的工作成果是XML格式的广告系统设置文件。 现在,这种交互协议可能看起来很奇怪,但是,正如我们已经讨论的那样,我们开始以“零”的形式构建第一个广告系统(即使没有UI),这种格式一直保存到今天。

成功启动第一个项目后,很快就完成了。 这些也是DSP的UI,对它们的要求与第一个项目相同。 差不多了 尽管事实非常相似,但魔鬼隐藏在细节中-对象的层次结构略有不同,在其中添加了两个字段...获得第二个项目的最明显方法与第一个项目非常相似,但有改进之处是复制方法,我们使用了复制方法。 它带来了许多人所熟悉的问题-连同“好的”代码,错误也被复制了,必须手动分发补丁。 在所有活动项目中推出的所有新功能都发生了同样的事情。

在这种模式下,可以在项目很少的情况下工作,但是当项目数量超过20个时,熟悉的方法便无法扩展。 因此,我们决定将项目的公共部分转移到库中,项目将从该库中连接所需的组件。 如果检测到错误,则在库中对其进行一次修复,并在更新库版本时自动将其分发给项目,并且在重复使用新功能时也会发生相同的情况。

配置和术语


在实现此方法时,我们进行了多次迭代,并且从我们通常的纯DRF项目开始,它们都彼此进化。 在最新的实现中,我们的项目是使用基于JSON的DSL来描述的(参见图片)。 该JSON描述了项目组件的结构及其互连,并且前端和后端均可读取它。

初始化Angular应用程序后,前端会从后端请求JSON配置。 后端不仅释放静态配置文件,而且还对其进行处理,用各种元数据补充它或删除配置中负责用户无法访问系统部分的部分。 这允许用户以不同的方式向不同的用户显示界面,包括交互形式,整个应用程序的CSS样式以及特定的设计元素。 后者对于由具有不同角色和访问级别的不同类型的客户端使用的平台的用户界面尤其如此。

图片

后端与前端不同,后端在Django应用程序的初始化阶段读取一次配置。 因此,全部功能都记录在后端,并且动态检查对系统各个部分的访问。

在继续讨论最有趣的部分-数据库结构之前,我想介绍几个我们在讨论项目结构时使用的概念,以便与读者保持一致。

这些概念-实体和特征-在数据输入表单中得到了很好的说明(参见图片)。 整个表单为实体,其上的各个字段为要素。 图片还显示了端点(以防万一)。 因此,实体是可以在其上执行CRUD操作的系统中的独立对象,而功能只是“更多”的一部分,即实体的一部分。 使用功能部件时,如果不绑定任何实体,就无法执行CRUD操作。 例如:没有参考广告系列本身的广告系列的预算就是一个数字,没有上级广告系列的信息就无法使用。

图片

在项目的JSON配置中可以找到相同的概念(参见图片)。

图片

数据库结构


我们项目中最有趣的部分是数据库结构和支持它的机制。 在我们的项目的第一个版本中开始使用PostgreSQL后,我们今天仍然使用这项技术。 同时,我们正在积极使用Django ORM。 在早期的实现中,我们使用外键上的对象(实体)之间关系的标准模型,但是,当需要更改关系层次时,此方法会带来困难。 因此,例如,在DSP业务部门->广告商->广告活动的标准层次结构中,一些客户需要进入代理商级别(业务部门->代理商->广告商-> ...)。 因此,我们逐渐放弃使用外键,并通过一个单独的表使用“多对多”链接来组织对象之间的链接,我们将其称为“ LinkRegistry”。

此外,我们逐渐放弃了用于填充实体的硬编码,并开始将大多数字段存储在单独的表中,也通过“ LinkRegistry”将它们链接起来(参见图片)。 为什么需要这个? 对于每个客户端,实体的内容可能会有所不同-将添加或删除某些字段。 事实证明,我们将必须在每个实体中为所有客户存储字段的超集。 同时,它们都必须是可选的,以使“ alien”必填字段不会干扰工作。

图片

考虑图中的示例:这里描述了带有一个附加字段的广告素材的数据库结构-“ image_url”。 只有其ID存储在广告素材表中,而image_url存储在单独的表中,它们的关系由表LinkRegistry中的另一个条目描述。 因此,将通过三个条目(每个表中的一个条目)来描述此创意。 因此,为了保存这样的创意,您需要在每个创意中都进行输入,并以相同的方式进行读取,请访问3个表格。 每次从头开始编写这样的处理都是非常不便的,因此我们的库从程序员那里提取了所有这些细节。 为了处理数据,Django和DRF使用代码描述的模型和序列化器。 在我们的项目中,模型和序列化器中的字段集是在运行时通过JSON配置确定的,模型类是动态创建的(使用类型函数),并存储在特殊的寄存器中,在应用程序运行期间可以从那里使用它们。 对于这些模型和序列化器,我们还使用特殊的基类,这有助于使用非标准基结构。

保存新对象(或更新现有对象)时,从前端接收的数据将进入序列化器并在其中进行验证-标准DRF机制没有任何异常。 但是保存和更新在这里重新定义。 序列化程序始终知道它使用的模型,并且根据动态模型的内部表示,它可以了解下一个字段的数据应放入哪个表。 我们将这些信息编码在自定义模型字段中(请记住Django如何描述“ ForeignKey”-在字段内传递相关模型,我们做同样的事情)。 在这些特殊字段中,我们还抽象了使用描述符机制将第三条绑定记录添加到LinkRegistry的需要-在您编写“ creative.image_url ='http:// foo.bar''的代码中,以及在重写的方法__set__中, `LinkRegistry`。
这适用于写入数据库。 现在让我们来处理阅读。 从数据库中拉出的元组如何转换为Django模型实例? 在基本的Django模型中,有一个from_db方法,当在queryset中执行查询时,对于收到的每个元组都会调用该方法。 在输入处,它接收一个元组并返回Django模型的实例。 我们在基本模型中重新定义了该方法,根据主模型的元组(其中只有“ id”进来),我们从其他相关表中获取数据,并具有完整的集合,实例化了该模型。 当然,我们还致力于针对非标准用例优化Django预取机制。

测试中


我们的框架非常复杂,因此我们编写了大量测试。 我们对前端和后端都有测试。 我将详细介绍后端测试。

要运行测试,我们使用pytest。 在后端,我们有两大类测试:框架测试(我们也称为“核心”)和项目测试。

在内核中,我们使用pytest-django插件编写了隔离的单元测试和功能测试,用于测试端点。 通常,与数据库有关的所有工作都主要通过API请求进行测试-就像它在生产中一样。

功能测试可以指定JSON配置。 为了不附加项目术语,我们在实体中使用“虚拟”名称来测试我们的功能(“ Emma”,“ Alla”,“ Karl”,“ Maria”等)。 由于我们不希望通过编写image_url功能来限制开发人员的意识,即它只能与Creative实体一起使用-这些功能和实体是通用的,并且它们可以以与特定客户相关的任何组合相互连接。

对于测试项目,所有测试用例都在生产配置下运行,没有虚拟实体,因为对我们而言,准确验证客户将使用的对象非常重要。 在项目中,您可以编写任何涵盖项目业务逻辑功能的测试。 同时,基本的CRUD测试可以从内核连接到项目。 它们以通用方式编写,并且可以连接到任何项目:功能测试可以读取项目的JSON配置,确定此功能连接到哪个实体,并仅检查必要的实体。 为了方便准备测试数据,我们开发了一个帮助程序系统,该系统也可以基于JSON配置准备测试数据集。 在项目测试中,特殊的地方是量角器上的E2E测试,该测试可以测试项目的所有基本功能。 这些测试也使用JSON进行描述,它们由前端开发人员编写和支持。

后记


在本文中,我们检查了UIP部门由IPONWEB开发的模块化设计方法。 该解决方案已经在生产中成功运行了三年。 但是,该解决方案仍然存在许多局限性,不能使我们固步自封。 首先,我们的代码库仍然很复杂。 其次,支持动态模型的基本代码与诸如搜索,对象的大量加载,访问权限等关键组件相关联。 因此,其中一个组件的更改会严重影响其他组件。 为了摆脱这些限制,我们继续积极地处理我们的库,将其分为许多独立的部分,并降低了代码的复杂性。 在以下文章中,我们将告诉您有关结果的信息。

本文是我在MoscowPythonConf ++ 2019上的演讲的扩展抄本,因此我还分享了视频幻灯片的链接。

Source: https://habr.com/ru/post/zh-CN455720/


All Articles