本文介绍了机器学习应用中的一些软件工程基础,快速浏览了最流行的一些架构模式、设计模式,以及面向对象设计的 SOLID 原则,目的是让读者尽可能多地了解构建可扩展软件的主要贡献因素。应用程序设计是否能够适应变化,是构建成功解决方案的关键,如果设计过程很仓促,项目结束时,一定会为犯下的错误交学费。
正文
本专题将通过两篇系列文章,介绍与体系结构和设计有关的软件工程基础知识,以及如何在机器学习管道(ML Pipeline)的每个步骤中应用这些基础知识:
第1部分:问题陈述 |架构风格 |设计模式 |SOLID
第2部分:构建机器学习管道
介绍
就像以前在 Steven Geringer 著名的维恩图中看到的,数据科学是 3 个学科的交集:计算机科学、数学/统计学和特定的领域知识。
数据科学维恩图[©Steven Geringer]
拥有基本(甚至是高级)的编程技能,是将端到端的经验结合在一起的关键,但还不足以创建一个可以发布的应用程序。除非从 IT 背景进入数据科学和机器学习(ML),并且在构建企业级的、分布式的、可靠的系统方面拥有实际经验,否则您的 Jupyter 笔记本(译者: 可创建和共享自己的文学化程序文档)没资格成为优秀的软件,遗憾的是,您也不能成为软件工程师!
虽然您打算构建的是一个很好的预测产品的原型,但仍然需要通过工程技术的路线图来推动它。所需要的是一个由专业软件工程师组成的团队,可以将(一次性)概念验证转化为高性能、可靠、松耦合和可扩展的系统!
一切都是设计出来的; 但设计得好的东西很少!
在本系列中,我们将展示如何实现良好设计的一些想法。第一部分从基础知识开始,第二部分逐步设计整体架构。这里建议的架构是技术无关的,机器学习管道将被分解成责任明确的不同层,可以从许多技术堆栈中选择如何来实现每一层。
接下来看看成功的解决方案应该是如何实现的。
问题陈述
我们的主要目标是建立一个系统,具备以下特性:
▸减少延迟;
▸集成系统的其他部分(例如数据存储,报告,图形用户界面),但松耦合;
▸可以水平和垂直伸缩;
▸消息驱动,即系统通过异步、非阻塞的消息传递进行通信;
▸提供工作负载管理相关的高效计算;
▸容错和自我修复,即故障管理;
▸支持批量和实时处理。
架构风格
我们将首先介绍一个反应式系统,并快速浏览最流行的架构模式。
反应式系统
反应式系统设计范式采用前后一致的方法来建立更好的系统,这些系统是根据“反应式宣言”的原则进行设计的。每个反应式原则对应一个可扩展性的重要系统维度:
• 易响应的 → 时间
• 易扩展的 → 负载
• 可恢复的→ 错误
• 消息驱动的 → 通信。
反应系统的特性
面向服务架构(SOA)
SOA 将业务问题分解为服务,这些服务通过网络来共享信息。它们还共享代码(即公共组件),以保持反应式系统的一致性和特性,从而减少开发工作。
服务提供者发布合约,规定服务的特征以及如何使用服务。服务使用者可以在注册中心中找到服务元数据,开发所需的客户端组件来绑定和使用它。
协调器(Orchestrator)是一个综合服务,负责调用和组合其他服务。此外,编排(Choreography)采用去中心化的方法组合服务,服务通过消息/事件的交换进行交互。
SOA
流式(Streaming)架构
流式架构包括以下组件:
生产者:生成和发送消息的应用程序
消费者:订阅和使用消息的应用程序
主题:类别特定的记录流,存储成有序和不可变的记录序列,通过分布式集群进行分区和复制
流处理器:以特定方式处理消息的应用程序(例如数据转换、ML 模型等)。
流式架构
Lambda 架构
Lambda(λ)架构旨在以集成的方式处理实时和过去聚合的批量数据。它分离了实时和批处理的职责,查询层提供了所有数据的统一视图。
这个概念很简单:生成数据时,会在存储之前对其进行处理,因此分析可以包括最后一秒、最后一分钟或最后一小时生成的数据,只处理传入的数据,而不是所有的数据。
Lambda 架构
微服务架构
微服务是一种架构风格,它将应用程序构造为小型、自主、松耦合和协作的服务集合,围绕业务领域进行建模。这些服务使用同步协议(HTTP/REST)或异步协议(AMQP)进行通信,可以彼此独立地开发和部署。每个服务都有自己的数据库,以便与其他服务分离。
微服务架构
REST 架构
REST 是一种用于开发 Web 服务的架构风格,它建立在 Internet HTTP 的现有特性之上,允许以无状态的方式传输、访问和操作文本数据,即应用程序可以在不知道状态的情况下进行通信。
RESTful API 服务通过统一资源定位器(URL)公开,提供了创建、请求、更新或删除(CRUD)数据的功能。它最适合处理解耦(生成/消费的 ) 信息和(生成/使用信息的 ) 技术的系统。
REST 架构
设计模式
我们将浅显地介绍这个主题,并且只讨论我在本系列的第二部分中可能提到的那些模式。(虽然我每天都在使用它们,但很难用简单的语言全部解释清楚)
软件设计模式是针对软件工程中常见问题的优化的、可重复的解决方案。它是一个解决问题的模板,可以在许多不同的情况下使用。
策略
策略模式定义了一系列算法,将每个算法放在一个单独的类中,相互之间可互换。将行为封装在单独的类中,消除了任何条件语句,在运行时可以选择正确的算法(即策略)。
使用说明:业务规则有不同的实现,或者需要算法的不同变体。
策略模式
模板方法
模板方法旨在从不同的过程中抽象出一个通用的过程。它定义了算法的骨架,将某些步骤推迟到子类。子类可以覆盖某些行为,但不能更改骨架。
使用说明:遵循一系列一致的步骤,各个步骤可能有不同的实现。
⭐ 与策略模式的区别:
模板:子类在编译时选择算法。
策略:控制在运行时选择算法。
模板方法模式
责任链
责任链模式通过启用一个或多个处理程序来满足请求,避免将客户端(请求的发送者)与接收者耦合。这些处理程序连接到一个链中,每个处理程序都包含对链中下一个处理程序的引用。
使用说明:多个对象可以处理一个请求,处理程序(或序列)的优先级事先不知道。
责任链模式
观察者
观察者模式(缩写为 Publish/Subscribe 或简称 PubSub)通过定义对象之间的一对多依赖关系,可以轻松地进行通信广播,当一个对象的状态发生变化时,它的所有依赖关系都会自动得到通知和更新。观察者负责注册它们所“观察”的事件。
使用说明:当改变某个对象时同时需要改变其他对象,不知道多少对象需要改变。
观察者模式
建造者
建造者模式旨在逐步构造一个复杂的对象,分离构造与表示。实质上,它允许使用相同的代码,生成对象的不同类型和表示形式。
使用说明:虽然各个构造步骤有所不同,但可以使用相同的整体构建过程来构建多种复杂对象。
建造者模式
工厂方法
工厂方法定义了一个用于创建对象的接口,由子类来完成实例化。
使用说明:对象的具体类型和依赖关系事先不知道。
工厂方法模式
抽象工厂
抽象工厂关注如何创建相关产品的系列,不指定它们具体的类。
使用说明:不同的规则采用不同的实现,这些规则要么是未知的,要么是可扩展的。
⭐️ 与抽象方法的区别:
抽象工厂:创建其他工厂,这些工厂反过来创建从基类派生的对象。
抽象方法:创建从特定基类派生的对象。
抽象工厂模式
装饰
装饰模式动态地将新的职责附加到对象上,将对象放置在包含行为的特殊包装类(wrapper)中,因此不会影响原来方法的签名(继承上的组合)。
使用说明:运行时为对象分配额外的行为,不会破坏使用这些对象的代码。
装饰模式
仓库
仓库模式解决了数据检索和持久化的代码集中化问题,并为数据访问操作提供了抽象,类似内存中域对象的集合,允许执行 CRUD 方法,消除了各种数据库问题。
使用说明:分离业务逻辑和访问数据的代码。
资源库模式
小奖励
想进一步了解模式?可以先从“Gang of Four”的书《设计模式:可重用的面向对象软件的基础》开始。下面的图展示了模式之间的关系,非常重要:
设计模式之间的关系
SOLID
我们唯一的设计原则是 SOLID,它们对于每个软件开发人员来说都是必不可少的。
正如Bob大叔所说:“它们不是法律,也不是完美的真理。它们类似于一个规则:每天一个苹果,医生远离我 ”。
这意味着,它们不是某种“魔法”,不是带来牛奶、蜂蜜和优质软件的应许之地,它们是健壮、持久软件的关键贡献者。
简而言之,这些原则围绕两个主要的概念展开,是成功企业应用程序的基石:耦合是类了解另一个类并与之交互的程度;而内聚表示类具有单一用途的程度。换一种说法:
- 耦合是关于类如何相互作用的;
- 内聚关注单个类的设计方式。
单一责任原则
一个类应该有一个,而且只有一个变化的理由
这是不言自明的,但说起来容易做起来难。我们总是想在现有类中添加新的行为,但这是一个引发灾难的处方:每个行为都可能成为未来变化的理由,因此行为越少,在变化时产生错误的机会就越少。
开闭原则
能够扩展类的行为,而无需对其进行修改
类应该对扩展“开放”,但是对修改“关闭”。要实现这一点可以通过继承,即创建一个子类,关闭对原始类进行修改,将自定义的代码添加到子类来引入新的行为。
里氏替换原则
派生类必须可替代基类
当类 A 的行为扩展到子类 B 时,可以确保在不造成任何破坏的情况下用 B 替换 A。这点可能比较吸引人,特别是当把这一原则与开闭原则结合起来时。
接口隔离原则
创建特定于客户端的细粒度接口
接口和类必须尽可能特定,客户端的调用不依赖未使用的方法。这与单一责任原则是一致的。
依赖倒置原则
依赖抽象,而不是具体实现
高层的类不应该依赖于低层的类。它们都应该依赖于抽象。同样,抽象不依赖于细节,细节依赖于抽象。
小奖励
我创建了下面这个快速参考图。如果想知道左边小符号的灵感来自哪里,请参看:“SOLID原则,用励志海报解释” —— 我喜欢作者在这些原则上增加的有趣改变???。
SOLID
脚注
这里无法涵盖所有软件工程概念的详尽列表,它只是阅读下一篇文章的基础,我希望它能让读者了解构建可扩展软件的主要贡献因素。应用程序设计是否能够适应变化,是构建成功解决方案的关键,如果设计过程很仓促,项目结束时,一定会为犯下的错误交付学费。
好的设计是易懂的。伟大的设计是透明的。
评论