精华内容
下载资源
问答
  • android 24种设计模式介绍与6大设计原则
  • 云原生应用的十大设计原则

    万次阅读 2021-03-10 22:19:12
    云应用程序的十大设计原则 自我修复设计 实现全面冗余 尽量减少协调 横向扩展设计 通过分区解决限制 运营设计 使用托管服务 使用最佳的数据存储完成作业 演变设计 根据业务需求构建 越来越多的企业选择...

    目录

    云应用程序的十大设计原则

    自我修复设计

    实现全面冗余

    尽量减少协调 

    横向扩展设计

    通过分区解决限制 

    运营设计

    使用托管服务

    使用最佳的数据存储完成作业

    演变设计

    根据业务需求构建

     

    越来越多的企业选择上云,在云上构建自己公司的核心应用程序,通过云来大大减少公司的IT运维成本,提高应用的弹性能力,并把一些新兴的技术能力,例如:人工智能,物联网,区块连等引入到应用程序中,来增强企业的能力。以下介绍了云应用程序需要遵循的十大设计原则及基于微软云Azure的一些设计案例,遵循这些设计原则,可以提高应用程序的可伸缩性、复原能力和易管理性。


    云原生应用的十大设计原则


    自我修复设计

    设计应用程序以在故障发生时进行自我修复

    在分布式系统中,可能会发生故障。 硬件可能发生故障。 网络也有可能发生暂时性故障。 极少数情况下,整个服务或区域可能会遇到中断,但这些故障必须在计划之内。

    因此,设计的应用程序在故障发生时可进行自我修复。 这需要从三个方面入手:

    • 检测故障。
    • 从容应对故障。
    • 记录和监视故障,获取操作见解。

    如何应对特定类型的故障可能取决于应用程序的可用性需求。 例如,如果需要非常高的可用性,则可能在区域中断期间自动故障转移到次要区域。 然而,这将使成本高于单区域部署。

    此外,不要只考虑像区域中断这类大事件,因为这种情况通常鲜有发生。 应该尽可能将注意力集中在处理本地短期的故障上,例如网络连接故障或数据库连接失败等。

    建议

    • 重试失败的操作。 发生暂时性故障的原因可能有:短暂的网络连接中断、删除了数据库连接或服务因繁忙而超时。 在应用程序中构建重试逻辑来处理暂时性故障。 对于许多 Azure 服务,客户端 SDK 可实施自动重试。 有关详细信息,请参阅暂时性故障处理重试模式
    • 保护故障远程服务(断路器)。 在暂时性故障后最好进行重试,但如果故障仍然存在,最终可能会有非常多的调用方攻击故障服务。 因为请求进行了备份,这可能导致级联故障。 使用 断路器模式 ,无需在操作发生故障时) 进行远程调用,就能快速 (。
    • 隔离关键资源(隔层)。 子系统中的故障有时会发生级联。 如果某个故障导致某些资源(例如线程或套接字)无法及时释放,导致资源耗尽,则可能就会发生这种连锁反应。 为了避免此问题,请将系统分区为独立的组,使一个分区中的故障不会导致整个系统瘫痪。
    • 执行负载分级。 应用程序可能会遇到突发流量高峰,导致后端上的服务瘫痪。 为了避免此问题,请使用基于队列的负载调节模式使工作项排队进行异步运行。 队列充当可平缓负载高峰的缓冲区。
    • 故障转移。 如果无法访问某个实例,请故障转移到另一个实例。 对于 Web 服务器之类的无状态对象,请在负载均衡器或流量管理器后放置一些实例。 对于数据库之类的存储状态的对象,请使用副本和故障转移。 根据数据存储和复制方式,可能需要应用程序处理最终的一致性。
    • 补偿失败的事务。 一般情况下,需避免分布式事务,因为它们需要协调服务和资源。 相反,应该用较小的单个事务组成操作。 如果在中途操作失败,请使用补偿事务撤销已完成的所有步骤。
    • 检查点长时间运行的事务。 如果长时间运行的操作失败,检查点可以提供复原能力。 当操作重新启动时(例如,它被另一个 VM 选中),它可以从上一个检查点恢复。
    • 正常降级。 有时某个问题无法解决,但可以提供仍然有用的缩减版功能。 假设某个应用程序显示图书目录。 如果该应用程序无法检索封面的缩略图图像,它可能显示占位符图像。 整个子系统可能对应用程序不重要。 例如,在电子商务网站,显示产品建议可能没有处理订单重要。
    • 限制客户端。 有时,少量的用户会产生过多的负载,降低了应用程序对其他用户的可用性。 在这种情况下,可以在一段时间内限制客户端。 请参阅限制模式
    • 阻止错误执行组件。 仅仅限制客户端并不意味着客户端的行为是恶意的。 它只意味着客户端超出其服务配额。 但如果客户端持续超出其配额或在其他方面具有不良行为,则可能需要进行阻止。 定义一个带外进程,供用户请求解除阻止。
    • 使用领导选拔。 当需要协调任务时,请使用领导选拔选择协调器。 这样,协调器不是单一故障点。 如果协调器失败,则选择一个新的协调器。 与其从头开始实施领导选举算法,不如考虑现成的解决方案,比如 Zookeeper。
    • 使用故障注入进行测试。 通常,成功的路径会得到精心的测试,而失败的路径却不会。 系统在生产中长时间运行后,才会执行失败路径。 通过触发实际故障或模拟故障,使用故障注入来测试系统对故障的复原能力。
    • 采用混沌工程。 混沌工程通过将故障或异常情况随机注入到生产实例中,扩展了故障注入的概念。

    实现全面冗余

    在应用程序中构建冗余,以避免出现单一故障点

    有弹性的应用程序围绕故障路由。 标识应用程序中的关键路径。 该路径中的每个点是否都存在冗余? 当子系统出现故障时,应用程序是否会故障转移到其他内容?

    建议

    • 考虑业务需求。 在系统中生成的冗余量会影响成本和复杂度。 体系结构应反映业务需求,如恢复时间目标 (RTO)。 例如,多区域部署比单区域部署昂贵,其管理也更复杂。 需要使用操作过程处理故障转移和故障回复。 可能为一些业务方案(而不是其他方案)考虑额外的成本和复杂性。
    • 将 VM 放在负载均衡器之后。 请勿将一个 VM 用于任务关键的工作负载。 而是将多个 VM 放置于负载均衡器之后。 如果任何 VM 变得不可用,负载均衡器会向其余正常运行的 VM 分配流量。 若要了解如何部署此配置,请参阅用于提高可伸缩性和可用性的多个 VM
    • 复制数据库。 Azure SQL 数据库和 Cosmos DB 自动复制区域内的数据,且可以跨区域启用异地复制。 如果使用 IaaS 数据库解决方案,请选择支持复制和故障转移的解决方案,如 SQL Server Always On 可用性组
    • 启用异地复制Azure SQL 数据库Cosmos DB 的异地复制在一个或多个次要区域中创建数据的可读次要副本。 出现故障时,数据库可以故障转移到供写入的次要区域。
    • 为提高可用性而分区。 数据库分区通常用于提高可伸缩性,但它还可以提高可用性。 如果一个分片出现故障,仍可以访问其他分片。 一个分片中的故障仅中断总事务的子集。
    • 部署到多个区域。 为实现最高可用性,请将应用程序部署到多个区域。 这样,在极少数情况下,当某个问题影响到整个区域时,应用程序可以故障转移到另一区域。 下图显示了使用 Azure 流量管理器处理故障转移的多区域应用程序。

    • 同步前端和后端故障转移。 使用 Azure 流量管理器将前端故障转移。 如果无法访问一个区域中的前端,流量管理器会将新请求路由到次要区域。 可能需要协调数据库的故障转移,具体取决于数据库解决方案。
    • 使用自动故障转移,但手动进行故障恢复。 将流量管理器用于自动故障转移,而不用于自动故障恢复。 自动故障恢复存在风险,即可能在区域尚未完全正常之前切换到主要区域。 请改为验证所有应用程序子系统均正常运行,然后再手动进行故障恢复。 此外,可能需要在故障恢复前检查数据一致性,具体取决于数据库。
    • 为流量管理器包括冗余。 流量管理器是一个潜在的故障点。 查看流量管理器 SLA,然后决定仅使用流量管理器是否能满足高可用性的业务需求。 如果不能,请考虑添加另一个流量管理解决方案作为故障回复机制。 如果 Azure 流量管理器服务出现故障,请将 DNS 中的 CNAME 记录更改为指向其他流量管理服务。

    尽量程度减少协调

    最大程度减少应用程序服务之间的协调以获得可伸缩性

    大多数云应用程序包含多个应用程序服务 — web 前端、数据库、业务流程、报告和分析等。 若要实现可伸缩性和可靠性,其中每一个服务都应在多个实例上运行。

    当两个实例尝试执行影响某种共享状态的并发操作时会发生什么? 在某些情况下,须跨节点进行协调,例如保留 ACID 保证。 此图中,Node2 正在等待 Node1 释放数据库锁定:

    协调限制了水平缩放的优点,且会形成瓶颈。 在此示例中,当横向扩展应用程序并添加更多实例时,锁定争用会增加。 而最糟的情况是前端实例将花费大部分时间等待锁定。

    “仅一次”语义是发生协调的另一个常见原因。 例如,一个顺序必须仅处理一次。 两个辅助角色正在侦听新顺序。 Worker1 选取一个顺序进行处理。 应用程序须确保 Worker2 不会重复工作,并且如果 Worker1 崩溃,不会删除顺序。

    可使用计划程序代理监督程序等模式在辅助角色之间进行协调,但在这种情况下,采用对工作进行分区的方法可能更好。 每个辅助角色都分配有某范围的顺序(比如按照计费区域)。 如果某辅助角色故障,新的实例会在前一个实例停止的位置启用,但不会出现多个实例争用的情况。

    建议

    • 采用最终一致性。 分布数据时,需要协调来强制执行可靠的一致性保证。 例如,假设通过一项操作更新两个数据库。 最好系统能调节最终一致性(方法是在发生故障后,使用补偿事务模式进行逻辑回滚),而不是将其置于单个事务范围。
    • 使用域事件同步状态。 域事件是一种事件,可记录域中发生的重要事情。 关注的服务会侦听事件,而不是使用全局事务来协调多个服务。 如果使用此方法,系统必须允许最终一致性(请参阅上一项)。
    • 请考虑 CQRS 和事件源等模式。 这两种模式有助于减少读取工作负载和写入工作负载之间的争用。

    CQRS 模式将读取操作从写入操作中分离。 在某些实现中,读取数据通过物理方式从写入数据中分离。

    事件源模式中,状态更改作为一系列事件被记录到仅追加数据存储中。 将事件追加到流是一种原子操作,需要最小锁定。

    • 这两种模式互为补充。 如果 CQRS 中的只写存储使用事件源,则只读存储可以侦听相同的事件,以创建当前状态的可读快照(已针对查询进行优化)。 但是,在采用 CQRS 或事件源之前,请先了解此方法的难题。
    • 分区数据。 避免将所有数据放入一个由多个应用程序服务共享的数据架构中。 微服务体系结构通过使每个服务对自己的数据存储负责来强制执行这一原则。 在单个数据库中,将数据分区到不同分片可以提高并发性,因为写入到一个分片的服务不会影响写入其他分片的服务。
    • 设计幂等运算。 如果可能,请将操作设计为幂等操作。 这样一来,可使用“至少一次”语义处理这些操作。 例如,可将工作项放入队列。 如果某辅助角色在操作期间故障,另一个辅助角色会选取此工作项。
    • 使用异步并行处理。 如果某项操作需要多个异步执行的步骤(例如远程服务调用),可并行调用,然后聚合结果。 此方法假定每个步骤不依赖上一步的结果。
    • 尽可能使用开放式并发。 悲观并发控件使用数据库锁定来防止冲突。 这可能会导致性能不佳,可用性降低。 对于乐观并发控件,每个事务修改数据的副本或快照。 提交事务时,数据库引擎会验证事务并拒绝会影响数据库一致性的任何事务。
    • 通过快照隔离,Azure SQL 数据库和 SQL Server 支持乐观并发。 通过使用 Azure Cosmos DBAzure 存储等 Etag,一些 Azure 存储服务支持乐观并发。
    • 请考虑 MapReduce 或其他并行的分布式算法。 根据要执行的工作的数据和类型,可将工作拆分为独立的任务,这些任务可以由并行工作的多个节点执行。 请参阅大计算的体系结构样式
    • 使用前导选举进行协调。 如果需要协调操作,请确保协调器不会成为应用程序中的单一故障点。 使用领导选择模式,一个实例始终是领导并充当协调器。 如果该领导失败,会选择新的实例作为领导。

    横向扩展设计

    设计应用程序,使其能够扩大,根据需要添加或删除新实例。

    云的主要优点是可以弹性缩放 — 能够根据需要使用容量,在负载增加时扩大,在不需要额外容量时缩小。 设计应用程序,使其能够扩大,根据需要添加或删除新实例。

    建议

    • 避免实例粘性。 在来自相同客户端的请求始终路由至同一台服务器时,才会出现粘性或会话相关性。 粘性限制了应用程序的横向扩展能力。例如,来自高容量用户的流量不会分布在多个实例中。 粘性的成因包括在内存中存储会话状态以及使用特定于计算的密钥加密。 请确保任何实例都可处理任何请求。
    • 确定瓶颈。 扩大并不是解决每个性能问题的万能方式。 例如,如果后端数据库是瓶颈,添加再多 Web 服务器也于事无补。 首先识别并解析系统中的瓶颈,然后针对该问题引发更多实例。 系统中有状态的部分最有可能引起瓶颈问题。
    • 通过可伸缩性要求分解工作负荷 应用程序通常由多个工作负荷组成,它们的缩放要求各不相同。 例如,应用程序可能有面向公众的网站和单独的管理网站。 公众网站可能会遇到流量突然激增的情况,而管理网站的负荷更小,且更具可预测性。
    • 卸载资源密集型任务 需要大量的 CPU 或 I/O 资源的任务应尽可能移动到后台作业以减少处理用户请求的前端上的负载。
    • 使用内置自动缩放功能。 许多 Azure 计算服务有内置的自动缩放支持。 如果应用程序具有可预测的常规工作负荷,则可按计划扩大。 例如,在营业时间期间扩大。 否则,如果工作负荷不可预测,则可使用 CPU 或请求队列长度等性能指标来触发自动缩放。 有关自动缩放最佳做法的信息,请参阅自动缩放
    • 请为关键工作负荷考虑自动缩放。 对于关键工作负荷,你希望供应一直超过需求。 在负载加重的情况下,最好是快速添加新实例来处理额外的流量,然后逐渐缩减。
    • 缩小式设计。 请记住,使用弹性缩放时,应用程序会有缩小时期,此时实例会被移除。 应用程序必须妥善处理正在移除的实例。 以下是一些处理缩小功能的方法:

    侦听关闭事件(可用时),然后全部关闭。

    服务的客户端/使用者应支持暂时性故障处理和重试。

    对于运行时间长的任务,请考虑使用检查点,或者管道和筛选器模式分解工作。

    如果实例是在处理过程中移除的,请将工作项放在队列中,以便另一个实例可以接收工作。


    通过分区解决限制 

    使用分区来解决数据库、网络和计算限制

    在云中,所有服务都有纵向扩展的限制。 Azure 订阅和服务限制、配额和约束中介绍了 Azure 服务限制。 限制包括内核数、数据库大小、查询吞吐量和网络吞吐量。 如果系统增长到足够大,可能会命中一个或多个限制。 使用分区来解决这些限制。

    有很多方法可用于分区系统,例如:

    • 分区数据库,以避免对数据库大小、数据 I/O 或并发会话数的限制。

    • 分区队列或消息总线,以避免对请求数或并发连接数的限制。

    • 分区应用服务 Web 应用,以避免对每个应用服务计划的实例数的限制。

    可以水平、垂直、或按功能分区数据库。

    • 在水平分区(也称为分片)中,每个分区保存总数据集的子集的数据。 这些分区共享相同的数据架构。 例如,名称以 A–M 开头的客户进入一个分区,以 N–Z 开头的进入另一个分区。

    • 在垂直分区中,每个分区保存数据存储中项字段的子集。 例如,将经常访问的字段放在一个分区中,将较不经常访问的字段放在另一个分区。

    • 在功能分区中,根据系统中每个界限上下文使用数据的方式对数据进行分区。 例如,在一个分区中存储发票数据,在另一个分区中存储产品库存数据。 架构是独立的。

    有关更多详细指南,请参阅数据分区

    建议

    • 分区应用程序的不同部分。 数据库显然很适合用于分区,但也需考虑存储、缓存、队列和计算实例。
    • 设计分区键以避免热点。 如果分区数据库,但一个分片仍然获取大多数请求,那么问题还未解决。 理想情况下,负载会在所有分区中均匀分布。 例如,按客户 ID 而不是客户名称的首字母进行哈希分区,因为某些首字母会更集中。 分区消息队列时,该原则也同样适用。 选择一个可以在队列集中平均分布消息的分区键。 有关更多信息,请参阅分片
    • 通过分区解决 Azure 订阅和服务限制。 单个组件和服务有限制,但订阅和资源组也有限制。 对于非常大的应用程序,可能需要进行分区来解决这些限制。
    • 在不同级别分区。 请考虑部署在 VM 上的数据库服务器。 VM 有一个由 Azure 存储支持的 VHD。 存储帐户属于 Azure 订阅。 请注意,层次结构中的每个步骤都有限制。 数据库服务器可能有连接池限制。 VM 有 CPU 和网络限制。 存储有 IOPS 限制。 订阅有 VM 内核数的限制。 一般来说,在较低的层次结构更容易分区。 仅大型应用程序需要在订阅级别进行分区。

    运营设计

    合理设计应用程序,使运营团队获得所需的工具。

    云已经显著改变了运营团队的角色。 他们不再负责管理托管应用程序的硬件和基础结构。 即便如此,运营仍是成功运行云应用程序的关键环节。 运营团队的一些重要功能包括:

    • 部署
    • 监视
    • 升级
    • 事件响应
    • 安全审核

    可靠的记录和跟踪对于云应用程序非常重要。 在设计和规划中涉及操作团队,以确保应用程序为其提供成功所需的数据和见解。

    建议

    • 确保可以观测到所有内容。 部署和运行解决方案后,日志记录和跟踪的结果将是对系统的主要见解。 “跟踪”就是记录系统中的路径,有助于找出瓶颈、性能问题和故障点。 “记录”就是捕获单个事件,例如应用程序状态更改、错误和异常。 请在生产时记录,否则将在最需要它的时候缺乏见解。
    • 用于监视的手段。 通过监视可了解应用程序在可用性、性能和系统运行状况方面的表现是否良好。 例如,监视可指示是否符合 SLA。 在系统的常规运行期间都会进行监视。 应尽可能实时监视,以便操作人员可以迅速对问题作出反应。 理想情况下,监视可在导致严重故障之前,帮助避免问题的出现。 有关详细信息,请参阅监视和诊断
    • 用于根本原因分析的手段。 根本原因分析是查找故障的根本原因的过程。 它发生在故障出现后。
    • 使用分布式跟踪。 使用专为并发、异步和云规模设计的分布式跟踪系统。 跟踪应包括跨服务边界的关联 ID。 单个操作可能涉及对多个应用程序服务的调用。 如果操作失败,关联 ID 可帮助找出失败的原因。
    • 将日志和指标标准化。 运营团队需要在解决方案中聚合来自各种服务的日志。 如果每种服务使用各自的日志格式,将很难或不可能从中获取有用的信息。 定义包括关联 ID、事件名称、发送者 IP 地址等字段的常见架构。 单个服务可以派生继承基础架构并包含附加字段的自定义架构。
    • 自动化管理任务,包括预配、部署和监视。 自动化任务具有可重复性并且可以减少人为错误。
    • 将配置视为代码。 通过将配置文件签入版本控制系统,可以对更改进行跟踪和版本控制,并在需要时回滚。

    使用托管服务

    如果可能,请使用平台即为服务 (PaaS),而不是基础结构即服务 (IaaS)

    IaaS 就像有一盒零件。 你可以构建任何东西,但必须自己组装。 PaaS 选项更易于配置和管理。 无需提供 VM、设置 VNet、管理补丁和更新,以及与在 VM 上运行软件相关的所有其他开销。

    例如,假设应用程序需要一个消息队列。 可以使用类似 RabbitMQ 的东西在 VM 上设置自己的消息传递服务。 但 Azure 服务总线已经提供了可靠的消息传递作为服务,而且它更易于设置。 只需创建一个服务总线命名空间(这可以作为部署脚本的一部分来完成),然后使用客户端 SDK 调用服务总线。

    当然,应用程序可能具有某些特定要求,会使 IaaS 方法更合适。 但是,即使您的应用程序基于 IaaS,查找在哪里可以自然地包含 PaaS 选项。 其中包括缓存、队列和数据存储。

     
    取消运行…考虑使用…
    Active DirectoryAzure Active Directory
    ElasticsearchAzure 搜索
    HadoopHDInsight
    IIS应用服务
    MongoDBCosmos DB
    Redis用于 Redis 的 Azure 缓存
    SQL ServerAzure SQL 数据库
    文件共享Azure NetApp 文件

    请注意,这并不是一个详尽的列表,而是等效选项的子集。


    使用最佳的数据存储完成作业

    选择最适合数据的存储技术,并了解如何使用该技术

    现在再也无需将所有数据粘贴到大型 SQL 关系数据库。 关系数据库非常擅长通过关系数据为事务提供 ACID 保证。 但这会产生一些费用:

    • 查询可能需要高成本联接。
    • 数据必须规范化,且符合预定义架构(写入架构)。
    • 锁定争用可能会对性能产生影响。

    在任何大型解决方案中,单个数据存储技术很可能满足不了所有需求。 关系数据库的替代方案包括键/值存储、文档数据库、搜索引擎数据库、时间序列数据库、列系列数据库和图形数据库。 每个方案都有其优缺点,不同类型的数据适合其中不同的方案。

    例如,可将产品目录存储在文档数据库(如 Cosmos DB)中,这可实现灵活架构。 在这种情况下,每个产品描述是一个自包含文档。 若要查询整个目录,可以索引目录并将索引存储在 Azure 搜索中。 产品清单可以存储在 SQL 数据库中,因为这类数据需要 ACID 保证。

    请记住,数据不仅仅包括持久化的应用程序数据。 还包括应用程序日志、事件、消息和缓存。

    建议

    请勿在任何情况下都使用关系数据库。 请考虑在适当的时候使用其他数据存储。 请参阅选择适当的数据存储

    采用混合持久性。 在任何大型解决方案中,单个数据存储技术很可能满足不了所有需求。

    请考虑数据的类型。 例如,将事务数据存储在 SQL 中,将 JSON 文档存储在文档数据库中,将遥测数据存储在时序数据库中,将应用程序日志存储在 Elasticsearch 中,并将 Blob 存储在 Azure Blob 存储中。

    优先考虑可用性而非(强)一致性。 CAP 定理意味着分布式系统须权衡可用性和一致性。 (的网络分区、CAP 定理的另一条脚永远无法完全避免。 ) 通常,可以通过采用 最终一致性 模型来实现更高的可用性。

    请考虑开发团队的技能组合。 使用混合持久性有一些好处,但也有可能适得其反。 采用新的数据存储技术需要新的技能组合。 开发团队须了解如何充分利用此技术。 他们必须了解恰当的使用模式、如何优化查询、调整性能等。 请在考虑存储技术时将此因素考虑在内。

    使用补偿事务。 混合持久性的副作用是单个事务可能会将数据写入多个存储。 如果出现故障,使用补偿事务来撤消任何已完成的步骤。

    查看界限上下文。 界限上下文是域驱动设计中的一个术语。 界限上下文是域模型的显式边界,它定义模型适用于域的哪个部分。 理想情况下,界限上下文将映射到业务域的子域。 系统中的界线上下文是考虑混合持久性的自然位置。 例如,“产品”可能会出现在产品目录子域和产品清单子域,但这两个子域很可能对存储、更新、查询产品的要求不同。


    演变设计

    进化型设计是持续创新的关键

    无论是修复 bug、添加新功能、引入新技术,还是使现有系统更具伸缩性和弹性,所有成功的应用程序都在随着时间而不断变化。 如果应用程序的所有部分都紧密耦合,则很难将更改引入系统。 应用程序中一个部分的更改可能会破坏另一部分,或者改变整个代码库。

    这个问题并不局限于单片应用程序。 应用程序可分解为服务,但仍会表现出那种紧密耦合性,使系统变得刚性和脆弱。 但当服务设计为可以改进时,团队可以创新并不断提供新功能。

    微服务正在成为实现进化设计的一种热门方式,因为它们可以解决此处列出的许多值得注意的问题。

    建议

    • 强制执行高度内聚和松散耦合。 如果服务提供逻辑上具有共同所属的功能,则该服务具有内聚性。 如果可以在更改一个服务的同时不会更改另一个服务,则服务具有松散耦合性。 高度内聚通常意味着更改一个函数时还需更改其他相关函数。 如果发现更新某个服务时需要对其他服务进行协调更新,则可能表示该服务不具有内聚性。 域驱动设计 (DDD) 的目标之一就是标识这些边界。
    • 封装域知识。 客户端使用服务时,强制执行域的业务规则的责任不应落在客户端上。 相反,服务应封装属于其责任范围内的所有域知识。 否则,每个客户端都必须强制执行业务规则,最终域知识会分散在应用程序的不同部分。
    • 使用异步消息传递。 异步消息传递是一种将消息创建者与使用者分离的方法。 创建者不依赖于使用者回复消息或采取任何特定操作。 有了 pub/sub 体系结构,创建者甚至可能不知道谁在使用消息。 新服务可以轻松地使用消息,而不需要对创建者进行任何修改。
    • 不要将域知识构建到网关中。 在微服务体系结构中,网关对于请求路由、协议转换、负载均衡或身份验证等操作非常有用。 但网关应该仅限于这种基础结构功能。 它不应实施任何域知识,以避免成为严重的依赖项。
    • 公开开放接口。 避免在服务之间创建自定义转换层。 相反,服务应该公开具有明确定义的 API 协定的 API。 API 应拥有版本控制,以便在保持向后兼容性的同时改进 API。 这样就可以更新服务,而无需对依赖它的所有上游服务进行协调更新。 面向公众的服务应通过 HTTP 公开一个 RESTful API。 因性能原因,后端服务可能会使用 RPC 样式的消息传递协议。
    • 针对服务协定进行设计和测试。 在服务公开了明确定义的 API 后,可以针对这些 API 进行开发和测试。 这样可以开发和测试单个服务,而无需启动所有的依赖服务。 (当然,仍然可以针对实际服务执行集成和负载测试。)
    • 分清基础结构与域逻辑。 不要将域逻辑与基础结构相关的功能(如消息传递或暂留)混在一起。 否则,更改域逻辑时需要对基础结构层进行更新,反之亦然。
    • 将跨领域问题转移到单独服务上。 例如,如果多个服务需要对请求进行身份验证,可将此功能移到各自的服务中。 然后便可改进身份验证服务(例如,通过添加新的身份验证流),而无需涉及使用它的任何服务。
    • 独立部署服务。 DevOps 团队可以独立地为应用程序中的其他服务部署单个服务时,更新就会更快,更安全。 Bug 修复和新功能便能按更常规的节奏推出。 同时设计应用程序和发布过程以支持独立更新。

    根据业务需求构建

    必须根据业务需求作出每一个设计决策

    此设计原则看似不言自明,但在设计解决方案时请务必遵循此原则。 预期的用户数是数千还是数百万人? 是否可接受一个小时的应用程序中断? 是否预计流量过大或可预测的工作负荷? 最终,必须根据业务需求作出每一个设计决策。

    建议

    • 定义业务目标,包括恢复时间目标 (RTO)、恢复点目标 (RPO) 和可容忍的最长中断时间 (MTO)。 这些数字应为决策提供有关体系结构的信息。 例如,若要实现较低的 RTO,可能会向次要区域实施自动故障转移。 但如果你的解决方案可以接受较高的 RTO,则可能不需要该程度的冗余。
    • 记录服务级别协议 (SLA) 和服务级别目标 (SLO),包括可用性和性能指标。 你可能会构建可用性为 99.95% 的解决方案。 这够了吗? 答案是需要业务决策。
    • 围绕业务领域为应用程序建模。 首先分析业务需求。 根据这些需求为应用程序建模。 请考虑使用领域驱动设计 (DDD) 方法创建反映业务流程和用例的域模型
    • 捕获功能性和非功能性需求。 可以通过功能性需求判断应用程序是否执行了所需操作。 可以通过非功能性需求判断应用程序的操作执行是否则正常。 具体而言,确保你理解自己对可伸缩性、可用性和延迟的需求。 这些需求将影响设计决策和技术选择。
    • 按工作负荷分解。 在此语境中,术语“工作负荷”是指某个离散的功能或计算任务,可以将它和其他任务逻辑分离。 不同的工作负荷在可用性、可伸缩性、数据一致性和灾难恢复方面具有不同的需求。
    • 规划增长。 某解决方案可能满足你当前对用户数、事务量和数据存储等方面的需求。 但是,可靠的应用程序可在不进行主要体系结构更改的情况下实现增长。 请参阅设计为横向扩展分区避开限制。 也请注意,你的业务模型和业务需求很可能在一段时间后发生更改。 如果应用程序的服务模型和数据模型过于死板,则很难将应用程序用于新的用例和方案。 请参阅 Design for evolution(旨在改进)。
    • 管理成本。 在传统的本地应用程序中,按资本支出为硬件付费。 在云应用程序中,为使用的资源付费。 确保了解所使用服务的定价模型。 总成本包括网络带宽使用量、存储、IP 地址、服务消耗和其他因素。 有关详细信息,请参阅 Azure 定价。 也需考虑操作成本。 在云中,无需管理硬件或其他基础结构,但仍需管理应用程序,包括 DevOps、事件响应和灾难恢复等。

     

    ----------------------------------
    大家好,我是流水,一个资深的IT从业人员和架构师. 非常高兴您能搜索到,并看到这篇文章,希望这篇文章的内容能给您带来新的知识和帮助。

    也欢迎扫描以下的二维码或微信搜索 “superxtech”,关注我的微信公众号 , 我会把更多更好的IT领域技术知识带给您!

    ----------------------------------

    展开全文
  • 设计模式(三)——面向对象设计原则

    千次阅读 多人点赞 2019-10-16 07:28:15
    设计模式需要遵循基本的软件设计原则。可维护性(Maintainability)和可复用性(Reusability)是衡量软件质量的重要的两个属性: 可维护性:软件能够被理解、改正、适应及扩展的难易程度 可复用性:软件能够被复用...

    设计模式需要遵循基本的软件设计原则。可维护性(Maintainability)和可复用性(Reusability)是衡量软件质量的重要的两个属性:

    • 可维护性:软件能够被理解、改正、适应及扩展的难易程度
    • 可复用性:软件能够被复用的难易程度

    面向对象设计的原则是支持可维护性复用,一方面需要实现设计方案或代码的复用,另一方面要保证系统易于扩展和修改,具有良好的可维护性。面向对象设计原则蕴含在各个设计模式中,是学习设计模式的基石,也是用于评价某个设计模式效果(Consequence)的重要指标。常见的面向对象设计原则包括:单一职责原则、开闭原则、里氏代换原则、依赖倒转原则、接口隔离原则、合成复用原则、迪米特法则。

    1.单一职责原则

    单一职责原则:

    定义1:一个对象应该只包含单一的职责,并

    展开全文
  • 软件设计的七大设计原则

    千次阅读 2019-01-05 00:02:35
    七大设计原则是23种设计模式的基础,体现了软件设计的思想,但并不是所有设计模式都遵循这七大设计原则,有些设计模式只遵循一部分设计原则,是对一些实际情况做的一些取舍。在我们项目中也并不一定完全遵循所有设计...

    一、前言

          七大设计原则是23种设计模式的基础,体现了软件设计的思想,但并不是所有设计模式都遵循这七大设计原则,有些设计模式只遵循一部分设计原则,是对一些实际情况做的一些取舍。在我们项目中也并不一定完全遵循所有设计模式,因为受一些因素如时间、人力、成本等,如果一开始将扩展性做的很完美,那么成本就上来了。所以遵循设计模式不要过度,一定要适度。
          本文将讲解每一种设计原则,从定义开始进行分析,理解,然后使用代码示例和UML图,分析存在的问题,逐步演进,力求将设计原则讲透,使读者理解。后续的设计模式文章也将采用这种方式。

    二、开闭原则

          开闭原则的定义是:一个软件实体(如类、模块、函数)应该对扩展开放、对修改关闭。生活中也有很多开闭原则的体现,比如公司的8小时弹性上班制度,对8小时的上班时间是的修改是关闭的,但是什么时候来什么时候走是开放的。
          开闭原则的含义其实是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现。在项目中需求变更是非常常见的,如果我们频繁修改原有的代码会增加系统的复杂度,增加项目的风险。使用开闭原则可以提高系统的可复用性及可维护性,具体做法就是用抽象构建框架,用实现扩展细节。
          下面用一个例子来讲解开闭原则,有一个在线书店卖书的场景,每本书有价格和名称,我们新建一个书的接口IBook,和该接口的一个实现类JavaBook。

    public interface IBook {
        String getName();
        double getPrice();
    }
    public class JavaBook implements IBook {
        @Override
        public String getName() {
            return "Java入门到精通";
        }
        @Override
        public double getPrice() {
            return 68.99;
        }
    }
    

          此时的UML图是这样的(图有点错误,IDEA生成UML时get开头的方法它会认为是属性)
    在这里插入图片描述
          现在有一个需求,就是有些书需要打折销售,我们需要获取打折后的价格,一种思路是修改IBook,新增一个打折价格的方法,但是这种改动影响很大,实现了该接口的类都要实现这个方法;另一种思路就是将JavaBook中的getPrice() 方法修改成打折后的价格,但是这样就获取不到原价了;还一种思路是在JavaBook中新增一个获取打折后价格的方法,这样既能获取原价又能获取打折后的价格,但是这三种种做法都违背了开闭原则,也就是对JavaBook进行了修改来实现变化,正确的做法是新增一个JavaBook的打折类,继承自JavaBook,新增一个获取打折后价格的方法。

    public class JavaDiscountBook extends JavaBook {
        double getDiscountPrice(){
            return super.getPrice() * 0.8;
        }
    }
    

          此时的UML图是:
    在这里插入图片描述
          这样在不修改原来代码的基础上,实现了需求变更,其实实现开闭原则的核心是面向抽象编程,后面一些设计原则也是如此。

    三、依赖倒置原则

          依赖倒置原则定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象。也就是说针对接口编程,不要针对实现编程,针对接口编程包括使用接口或抽象类,这样可以使得各个模块彼此独立,降低模块间的耦合性。而且在实现类中尽量不发生直接的依赖关系,依赖关系通过接口或抽象类产生。
          有一个场景,司机可以开车,我们新建一个Driver类和一个Benz类,司机可以开奔驰车,代码如下:

    public class Driver {
        public void driver(Benz benz){
            benz.run();
        }
    }
    public class Benz {
        public void run(){
            System.out.println("奔驰车可以跑!");
        }
    }
    

    此时的UML图是这样的
    在这里插入图片描述
          Driver类就依赖Benz类,显然违反了依赖倒置原则,如果我们司机想开其他车,就必须修改Driver类,我们进一步修改,增加IDriver的接口和ICar接口,IDriver的driver类参数是ICar,这样使得依赖关系发生在这两个接口上,不同的司机实现IDriver接口,不同的车实现ICar接口就可以了,代码如下:

    public interface IDriver {
        void driver(ICar car);
    }
    public interface ICar {
        void run();
    }
    public class Benz implements ICar{
        @Override
        public void run(){
            System.out.println("奔驰车可以跑!");
        }
    }
    public class Driver implements IDriver{
        @Override
        public void driver(ICar car){
            car.run();
        }
    }
    

          此时的UML图是这样的
    在这里插入图片描述
          这样不论什么类型的车都可以传入Driver的driver()方法里面,进行调用。

    四、 单一职责原则

          单一职责原则的定义:不要存在多于一个导致类变更的原因。如果我们一个类有两个职责:职责1和职责2,当我们需求变更的时候,职责1需要改变,变更的时候很可能会导致原本正常的职责2出问题。所以一个类、接口方法只负责一项职责,这样能降低类的复杂度,提高类的可读性,提高可维护性,降低修改带来的风险。在实际项目中,很多类不遵循单一职责原则,但是接口和方法要做到单一职责。单一职责原则还有一个很重要的点就是职责的划分,有些需求正常情况下有多个职责,但是某些特殊情况下又是一个职责,职责划分也需要视实际情况而定。

    五、接口隔离原则

          接口隔离原则定义:用多个专门的接口而不使用单一的总接口,客户端不应该依赖它不需要的接口。也就是说一个类对另一个类的依赖应该建立在最小的接口上,尽量细化接口,减少接口中的方法,但是一定要注意适度的原则,过分细化接口会带来复杂度。
          比如我们有个接口IAnimalAction,描述动物的行为,代码如下:

    public interface IAnimalAction {
        void eat();
        void fly();
        void swim();
    }
    

          这个接口中,我们定义了三个行为,后面需要的动物类实现这个接口,但是这就存在一个问题,如果某些动物不具备接口里面的三个行为中的某一个,但是它必须要实现那个方法,这就违背了接口隔离原则,正确的做法是将接口中的三个方法隔离开,分成三个接口,这样有具体行为的动物实现具体的接口,减少了耦合,将代码演进:

    public interface IEatAnimal {
        void eat();
    }
    public interface IFlyAnimal {
        void fly();
    }
    public interface ISwimAnimal {
        void swim();
    }
    

          接口隔离原则强调的是接口依赖隔离,单一职责原则强调的是职责单一。单一职责是对实现的约束,接口隔离原则是对抽象的约束。

    六、迪米特原则

          迪米特原则又叫最少知道原则,定义是:一个对象应该对其他对象保持最少的了解。简单讲就是只和朋友交流,不和陌生人说话,朋友指的是出现在成员变量、方法输入、方法输出中的类,但是出现在方法内部的类不属于朋友。不应该和这样的类发生关系。使用迪米特原则可以降低类与类之间的耦合,提高类的复用率,但是还是要强调适度的原则,过分使用迪米特原则会产生大量的中介类,使系统变复杂。
          现在有一个场景,学校里面有多个班级,班级里面有多个学生,我们现在要打印所有班级的所有学生,一种实现方式如下(这里将成员变量设置成public省略了get、set方法):

    public class School {
        public int id;
        public String schoolName;
        public List<Class> classes;
        public void print(){
            for(Class c : classes){
                for(Student s : c.students){
                    System.out.println(s.studentName);
                }
            }
        }
    }
    class Class{
        public int id;
        public String className;
        public List<Student> students;
    }
    class Student{
        public int id;
        public String studentName;
    }
    

          在School类中,Class类是它的属性,也就是它的朋友,但是Student类既不是成员变量,方法入参,也不是方法返回值,它不是School类的朋友,不应爱出现在方法内部,应该让Class类打印本班级的学生,代码演进如下:

    public class School {
        public int id;
        public String schoolName;
        public List<Class> classes;
        public void print(){
            for(Class c : classes){
                c.print();
            }
        }
    }
    class Class{
        public int id;
        public String className;
        public List<Student> students;
        public void print(){
            for(Student s : students){
                System.out.println(s.studentName);
            }
        }
    }
    class Student{
        public int id;
        public String studentName;
    }
    

          这样School类和Student就没有耦合了,逻辑也很清晰,符合迪米特原则。

    七、里氏替换原则

          里氏替换原则的定义是:对于每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有对象O1替换为O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。也就是子类替换父类,程序逻辑不变。里氏替换原则约束了继承,继承在程序设计中能够复用代码但是对程序是有入侵的,因为子类默认就拥有父类的行为,而且增加了耦合。
          在继承中如何遵守里氏替换原则?首先子类可以扩展父类的功能,但不能改变原有的功能,子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,如果重写了,那么用子类替换父类时,程序会调用子类的方法(否则重写就没有意义了),也就导致程序的行为发生变化。还有就是子类重载(不是重写)父类方法时,方法的前置条件(方法入参)要比父类更宽松比如父类的参数是HashMap,子类的话参数可以是Map,更宽松的话替换或不替换程序都会调用父类的方法,程序的行为也就不会改变,符合里氏替换原则。同理,方法的后置(方法返回值)条件要比父类更严格。使用里氏替换原则,可以避免子类重写父类的方法,降低代码出错的可能性。

    八、合成复用原则

          合成复用原则的定义是:尽量使用对象的组合/聚合,而不是继承关系达到软件复用的目的。
          组合是contains-A的关系,比如一个人的手、脚就是组合关系,这是一种强关系,其中一部分不存在了,所有的都不存在了,聚合是has-A的关系,是一种弱的关系,人群中的人就是聚合关系,其中一个人离开了,人群还是存在的,通过组合聚合也可以达到复用的目的,但是这种复用是黑箱复用,不需要知道细节,继承的复用是白箱复用,父类的细节会暴露给子类。尽量使用组合/聚合来实现软件复用并不是说抛弃继承,如果两个实体是is-A的关系时,可以使用继承。
          现在有个场景就是数据访问层要操作数据,需要先获得数据库连接,我们新建两个类:

    public class DBConnection {
        public String getConnection() {
            return "获得数据库连接";
        }
    }
    public class ProductDao extends DBConnection {
        private void addProduct(){
            getConnection();
            System.out.println("操作数据库!");
        }
    }
    

          通过继承我们实现了获取数据库连接,但是如果我们要更换一种数据库,那么就要修改DBConnection类,违背了开闭原则,现在我们可以通过抽象加组合的方式来实现变更后的需求,代码演进如下:

    public abstract class DBConnection {
        public abstract String getConnection();
    }
    public class MySQLConnection extends DBConnection {
        @Override
        public String getConnection() {
            return "MySQL的数据库连接";
        }
    }
    public class OracleConnection extends DBConnection {
        @Override
        public String getConnection() {
            return "Oracle数据库连接";
        }
    }
    public class ProductDao{
    
        private DBConnection dbConnection;
    
        ProductDao(DBConnection dbConnection){
            this.dbConnection = dbConnection;
        }
        public void addProduct(){
            String con = dbConnection.getConnection();
            System.out.println("使用"+con + "增加一个产品");
        }
    }
    

          这样,当我们新增一种数据库连接的时候,只要继承DBConnection这个抽象类就可以了,按我们实际传入的类型,ProductDao会调用对应的数据库连接来操作数据库,此时的UML是这样的。
    在这里插入图片描述

    展开全文
  • 面向对象设计的七大设计原则详解

    万次阅读 多人点赞 2018-10-03 12:32:21
    面向对象的七大设计原则 文章目录面向对象的七大设计原则简述七大原则之间的关系一、开闭原则(The Open-Closed Principle ,OCP)概念理解系统设计需要遵循开闭原则的原因开闭原则的实现方法一个符合开闭原则的...

    面向对象的七大设计原则


    简述

    类的设计原则有七个,包括:开闭原则里氏代换原则迪米特原则(最少知道原则)单一职责原则接口分隔原则依赖倒置原则组合/聚合复用原则

    七大原则之间的关系


    七大原则之间并不是相互孤立的,彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。

    开闭原则是面向对象的可复用设计的基石。其他设计原则是实现开闭原则的手段和工具。

    一般地,可以把这七个原则分成了以下两个部分:

    设计目标:开闭原则、里氏代换原则、迪米特原则
    设计方法:单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则

    一、开闭原则(The Open-Closed Principle ,OCP)


    软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。

    概念理解


    开闭原则是指在进行面向对象设计中,设计类或其他程序单位时,应该遵循:

    • 对扩展开放(open)
    • 对修改关闭(closed) 的设计原则。

    开闭原则是判断面向对象设计是否正确的最基本的原理之一。

    根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

    • 扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
    • 修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。

    通过下边的例子理解什么是扩展开放和修改关闭:

    在这里插入图片描述

    左边的设计是直接依赖实际的类,不是对扩展开放的。

    右边的设计是良好的设计:

    • Client对于Server提供的接口是封闭的;
    • Client对于Server的新的接口实现方法的扩展是开放的。

    系统设计需要遵循开闭原则的原因


    1. 稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。
    2. 扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。
      遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。

    开闭原则的实现方法


    为了满足开闭原则的对修改关闭原则以及扩展开放原则,应该对软件系统中的不变的部分加以抽象,在面向对象的设计中,

    • 可以把这些不变的部分加以抽象成不变的接口,这些不变的接口可以应对未来的扩展;
    • 接口的最小功能设计原则。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现;
    • 模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

    接口可以被复用,但接口的实现却不一定能被复用。
    接口是稳定的,关闭的,但接口的实现是可变的,开放的。
    可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能,实现软件系统的柔性扩展。

    好处:提高系统的可复用性和可维护性。

    简单地说,软件系统是否有良好的接口(抽象)设计是判断软件系统是否满足开闭原则的一种重要的判断基准。现在多把开闭原则等同于面向接口的软件设计。

    一个符合开闭原则的设计


    需求:创建一系列多边形。
    首先,下面是不满足开闭原则的设计方法:

    Shape.h

    enumShapeType{ isCircle, isSquare};
    typedef struct Shape {
    	enumShapeType type
    } shape;
    

    Circle.h

    typedef struct Circle {
    	enumShapeType type;
    	double radius;
    	Point center;
    } circle;
    void drawCircle( circle* );
    

    Square.h

    typedef struct Square {
    	enumShapeType type;
    	double side;
    	Point topleft;
    } square;
    void drawSquare( square* );
    

    drawShapes.cpp

    #include "Shape.h"
    #include "Circle.h"
    #include "Square.h"
    void drawShapes( shape* list[], intn ) {
    	int i;
    	for( int i=0; i<n; i++ ) {
    		shape* s= list[i];
    		switch( s->type ) {
    		case isSquare:
    			drawSquare( (square*)s );
    			break;
    		case isCircle:
    			drawCircle( (circle*)s );
    			break;
    		}
    	}
    }
    

    该设计不是对扩展开放的,当增加一个新的图形时:

    • Shape不是扩展的,需要修改源码来增加枚举类型
    • drawShapes不是封闭的,当其被其他模块调用时,如果要增加一个新的图形需要修改switch/case

    此外,该设计逻辑复杂,总的来说是一个僵化的、脆弱的、具有很高的牢固性的设计。

    用开闭原则重构该设计如下图:

    在这里插入图片描述

    此时,在该设计中,新增一个图形只需要实现Shape接口,满足对扩展开放;也不需要修改drawShapes()方法,对修改关闭。

    开闭原则的相对性


    软件系统的构建是一个需要不断重构的过程,在这个过程中,模块的功能抽象,模块与模块间的关系,都不会从一开始就非常清晰明了,所以构建100%满足开闭原则的软件系统是相当困难的,这就是开闭原则的相对性。

    但在设计过程中,通过对模块功能的抽象(接口定义),模块之间的关系的抽象(通过接口调用),抽象与实现的分离(面向接口的程序设计)等,可以尽量接近满足开闭原则。

    二、 里氏替换原则(Liskov Substitution Principle ,LSP)


    所有引用基类的地方必须能透明地使用其派生类的对象。

    概念理解


    也就是说,只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

    • 不应该在代码中出现if/else之类对派生类类型进行判断的条件。

    • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

    以下代码就违反了LSP定义。

    if (obj typeof Class1) {
        do something
    } else if (obj typeof Class2) {
        do something else
    }
    

    里氏替换原则(LSP)是使代码符合开闭原则的一个重要保证。

    同时LSP体现了:

    • 类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。

    • 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。
      示例:

    里式替换原则为我们是否应该使用继承提供了判断的依据,不再是简单地根据两者之间是否有相同之处来说使用继承。

    里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。

    具体来说:

    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
    • 子类中可以增加自己特有的方法。
    • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
    • 当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。

    下面举几个例子帮助更进一步理解LSP:
    例:1:
    在这里插入图片描述

    Rectangle是矩形,Square是正方形,Square继承于Rectangle,这样一看似乎没有问题。

    假如已有的系统中存在以下既有的业务逻辑代码:

    void g(Rectangle r)
    {
    r.SetWidth(5);
    r.SetHeight(4);
    assert(r.GetWidth() * r.GetHeight()) == 20);
    }
    

    则对应于扩展类Square,在调用既有业务逻辑时:

        Rectangle square = new Square();
        g(square);
    

    时会抛出一个异常。这显然违反了LSP原则。说明这样的继承关系在这种业务逻辑下不应该使用。

    例2:鲸鱼和鱼,应该属于什么关系?从生物学的角度看,鲸鱼应该属于哺乳动物,而不是鱼类。没错,在程序世界中我们可以得出同样的结论。如果让鲸鱼类去继承鱼类,就完全违背了Liskov替换原则。因为鱼作为基类,很多特性是鲸鱼所不具备的,例如通过腮呼吸,以及卵生繁殖。那么,二者是否具有共性呢? 有,那就是它们都可以在水中"游泳",从程序设计的角度来说,它们都共同实现了一个支持"游泳"行为的接口。

    例:3:运动员和自行车例子,每个运动员都有一辆自行车,如果按照下面设计,很显然违反了LSP原则。

    class Bike {
    public:
           void Move( );
           void Stop( );
           void Repair( );
    protected:
           int    ChangeColor(int );
    private:
           int    mColor;
    };
    
    
    class Player : private Bike
    {
    public:
          void  StartRace( );
          void  EndRace( ); 
    protected:
           int    CurStrength ( ); 
    private:
            int   mMaxStrength;
            int   mAge;
    };
    

    里式替换原则的优点


    • 约束继承泛滥,是开闭原则的一种体现。
    • 加强程序的健壮性,同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。

    重构违反LSP的设计

    如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

    • 创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。

    • 从B到A的继承关系改为关联关系。

    对于矩形和正方形例子,可以构造一个抽象的四边形类,把矩形和正方形共同的行为放到这个四边形类里面,让矩形和正方形都是它的派生类,问题就OK了。对于矩形和正方形,取width 和height 是它们共同的行为,但是给width 和height 赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。

    对于运动员和自行车例子,可以采用关联关系来重构:

    class Player 
    {
    public:
          void  StartRace( );
          void  EndRace( ); 
    protected:
           int    CurStrength ( ); 
    private:
            int   mMaxStrength;
            int   mAge;
    Bike * abike;
    };
    

    在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。

    如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这只是一个一般性的指导原则,使用的时候还要具体情况具体分析。

    在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。

    三、 迪米特原则(最少知道原则)(Law of Demeter ,LoD)


    迪米特原则(Law of Demeter)又叫最少知道原则(Least Knowledge Principle),可以简单说成:talk only to your immediate friends,只与你直接的朋友们通信,不要跟“陌生人”说话。

    概念理解


    对于面向OOD来说,又被解释为下面两种方式:

    1)一个软件实体应当尽可能少地与其他实体发生相互作用。

    2)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

    朋友圈的确定
    “朋友”条件:

    1. 当前对象本身(this)
    2. 以参量形式传入到当前对象方法中的对象
    3. 当前对象的实例变量直接引用的对象
    4. 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
    5. 当前对象所创建的对象

    任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。

    迪米特原则的优缺点


    迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

    迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度

    例如,购房者要购买楼盘A、B、C中的楼,他不必直接到楼盘去买楼,而是可以通过一个售楼处去了解情况,这样就减少了购房者与楼盘之间的耦合,如图所示。

    违反迪米特原则的设计与重构


    下面的代码在方法体内部依赖了其他类,这严重违反迪米特原则

    class Teacher { 
    public: 
     void command(GroupLeader groupLeader) { 
    	   list<Student> listStudents = new list<Student>; 
    	   for (int i = 0; i < 20; i++) { 
    	        listStudents.add(new Student()); 
    	   } 
    	   groupLeader.countStudents(listStudents); 
    } 
    }
    

    方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系(Teacher类中依赖了Student类,然而Student类并不在Teacher类的朋友圈中,一旦Student类被修改了,Teacher类是根本不知道的),这是不允许的。

    正确的做法是:

    class Teacher { 
    public:
    void command(GroupLeader groupLeader) { 
    	        groupLeader.countStudents(); 
      } 
    }
    
    class GroupLeader { 
    private:
    list<Student> listStudents; 
    public:
    GroupLeader(list<Student> _listStudents) { 
    	this.listStudents = _listStudents; 
    } 
    void countStudents() { 
    	cout<<"女生数量是:" <<listStudents.size() <<endl; 
       } 
    }
    

    使用迪米特原则时要考虑的


    • 朋友间也是有距离的

    一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private等。

    注意: 迪米特原则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、protected等访问权限。

    • 是自己的就是自己的

    如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

    四、单一职责原则


    永远不要让一个类存在多个改变的理由。

    换句话说,如果一个类需要改变,改变它的理由永远只有一个。如果存在多个改变它的理由,就需要重新设计该类。

    单一职责原则原则的核心含意是:只能让一个类/接口/方法有且仅有一个职责。

    为什么一个类不能有多于一个以上的职责?


    如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):

    • 一方面,如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
    • 另一方面,某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。
      违反了设计的开闭原则,也不是我们所期望的。

    职责的划分


    既然一个类不能有多个职责,那么怎么划分职责呢?

    Robert.C Martin给出了一个著名的定义:所谓一个类的一个职责是指引起该类变化的一个原因。

    如果你能想到一个类存在多个使其改变的原因,那么这个类就存在多个职责。

    SRP违反例:

    class Modem {
    		   void dial(String pno);    //拨号
               void hangup();        //挂断
               void send(char c);    //发送数据
               char recv();        //接收数据
    };
    

    乍一看,这是一个没有任何问题的接口设计。
    但事实上,这个接口包含了2个职责:第一个是连接管理(dial,hangup);另一个是数据通信(send,recv)。
    很多情况下,这2个职责没有任何共通的部分,它们因为不同的理由而改变,被不同部分的程序调用。所以它违反了SRP原则。

    下面的类图将它的2个不同职责分成2个不同的接口,这样至少可以让客户端应用程序使用具有单一职责的接口:
    在这里插入图片描述
    让 ModemImplementation实现这两个接口。我们注意到,ModemImplementation又组合了2个职责,这不是我们希望的,但有时这又是必须的。通常由于某些原因,迫使我们不得不绑定多个职责到一个类中,但我们至少可以通过接口的分割来分离应用程序关心的概念。

    事实上,这个例子一个更好的设计应该是这样的,如图:
    在这里插入图片描述
    例如,考虑下图的设计。

    Retangle类具有两个方法,如图。一个方法把矩形绘制在屏幕上,另一个方法计算矩形的面积。
    在这里插入图片描述
    有两个不同的Application使用Rectangle类,如上图。一个是计算几何面积的,Rectangle类会在几何形状计算方面给予它帮助。另一Application实质上是绘制一个在舞台上显示的矩形。

    这一设计违反了单一职责原则。Rectangle类具有了两个职责,第一个职责是提供一个矩形形状几何数据模型;第二个职责是把矩形显示在屏幕上。

    对于SRP的违反导致了一些严重的问题。首先,我们必须在计算几何应用程序中包含核心显示对象的模块。其次,如果绘制矩形Application发生改变,也可能导致计算矩形面积Application发生改变,导致不必要的重新编译,和不可预测的失败。

    一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类中。这个设计把Rectangle类中进行计算的部分移到GeometryRectangle类中。现在矩形绘制方式的改变不会对计算矩形面积的应用产生影响了。
    在这里插入图片描述

    使用单一职责原则的理由


    单一职责原则从职责(改变理由)的侧面上为我们对类(接口)的抽象的颗粒度建立了判断基准:在为系统设计类(接口)的时候应该保证它们的单一职责性。

    降低了类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险

    五、 接口分隔原则(Interface Segregation Principle ,ISP)


    不能强迫用户去依赖那些他们不使用的接口。

    概念理解


    换句话说,使用多个专门的接口比使用单一的总接口总要好。

    它包含了2层意思:

    • 接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。

    • 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。 反之,则说明接口a被b给污染了,应该重新设计它们的关系。

    如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。这显然违反了开闭原则,也不是我们所期望的。

    总而言之,接口分隔原则指导我们:

    1. 一个类对一个类的依赖应该建立在最小的接口上

    2. 建立单一接口,不要建立庞大臃肿的接口

    3. 尽量细化接口,接口中的方法尽量少

    违反ISP原则的设计与重构


    下面我们举例说明怎么设计接口或类之间的关系,使其不违反ISP原则。

    假如有一个Door,有lock,unlock功能,另外,可以在Door上安装一个Alarm而使其具有报警功能。用户可以选择一般的Door,也可以选择具有报警功能的Door。

    有以下几种设计方法:

    ISP原则的违反例一:在Door接口里定义所有的方法。
    在这里插入图片描述
    但这样一来,依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。违反了ISP原则。

    ISP原则的违反例二:在Alarm接口定义alarm方法,在Door接口定义lock,unlock方法,Door接口继承Alarm接口。
    在这里插入图片描述
    跟方法一一样,依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。违反了ISP原则。

    遵循ISP原则的例一:通过多重继承实现

    在这里插入图片描述
    在Alarm接口定义alarm方法,在Door接口定义lock,unlock方法。接口之间无继承关系。CommonDoor实现Door接口,AlarmDoor有2种实现方案:

    1)同时实现Door和Alarm接口。

    2)继承CommonDoor,并实现Alarm接口。

    第2)种方案更具有实用性。

    这样的设计遵循了ISP设计原则。

    遵循ISP原则的例二:通过关联实现
    在这里插入图片描述
    在这种方法里,AlarmDoor实现了Alarm接口,同时把功能lock和unlock委让给CommonDoor对象完成。

    这种设计遵循了ISP设计原则。

    接口分隔原则的优点和适度原则


    • 接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准:在为系统设计接口的时候,使用多个专门的接口代替单一的胖接口。

    • 符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。

    • 注意适度原则,接口分隔要适度,避免产生大量的细小接口。

    单一职责原则和接口分隔原则的区别


    单一职责强调的是接口、类、方法的职责是单一的,强调职责,方法可以多,针对程序中实现的细节;

    接口分隔原则主要是约束接口,针对抽象、整体框架。

    六、 依赖倒置原则(Dependency Inversion Principle ,DIP)


    A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
    B. 抽象不应该依赖于细节,细节应该依赖于抽象 C.针对接口编程,不要针对实现编程。

    概念理解


    依赖:在程序设计中,如果一个模块a使用/调用了另一个模块b,我们称模块a依赖模块b。

    高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。

    依赖倒置(Dependency Inversion)
    面向对象程序设计相对于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统的结构化程序设计中,高层模块总是依赖于低层模块。
    在这里插入图片描述
    问题的提出:
    Robert C. Martin氏在原文中给出了“Bad Design”的定义:

    1. 系统很难改变,因为每个改变都会影响其他很多部分。

    2. 当你对某地方做一修改,系统的看似无关的其他部分都不工作了。

    3. 系统很难被另外一个应用重用,因为很难将要重用的部分从系统中分离开来。

    导致“Bad Design”的很大原因是“高层模块”过分依赖“低层模块”。

    一个良好的设计应该是系统的每一部分都是可替换的。如果“高层模块”过分依赖“低层模块”,一方面一旦“低层模块”需要替换或者修改,“高层模块”将受到影响;另一方面,高层模块很难可以重用。

    问题的解决:

    为了解决上述问题,Robert C. Martin氏提出了OO设计的Dependency Inversion Principle (DIP) 原则。

    DIP给出了一个解决方案:在高层模块与低层模块之间,引入一个抽象接口层。
    在这里插入图片描述

    High Level Classes(高层模块) --> Abstraction Layer(抽象接口层) --> Low Level Classes(低层模块)

    抽象接口是对低层模块的抽象,低层模块继承或实现该抽象接口。

    这样,高层模块不直接依赖低层模块,而是依赖抽象接口层。抽象接口也不依赖低层模块的实现细节,而是低层模块依赖(继承或实现)抽象接口。

    类与类之间都通过抽象接口层来建立关系。

    依赖倒置原则的违反例和重构


    示例:考虑一个控制熔炉调节器的软件。该软件从一个IO通道中读取当前的温度,并通过向另一个IO通道发送命令来指示熔炉的开或者关。

    温度调节器的简单算法:

      const byte THERMONETER=0x86;
      const byte FURNACE=0x87;
      const byte ENGAGE=1;
      const byte DISENGAGE=0;
    
      void Regulate(double minTemp,double maxTemp)
      {
         for(;;)
         {
            while (in(THERMONETER) > minTemp)
               wait(1);
            out(FURNACE,ENGAGE);
            
            while (in(THERMONETER) < maxTemp)
               wait(1);
            out(FURNACE,DISENGAGE);
         }
      }
    

    算法的高层意图是清楚的,但是实现代码中却夹杂着许多低层细节。这段代码根本不能重用于不同的控制硬件。

    由于代码很少,所以这样做不会造成太大的损害。但是,即使是这样,使算法失去重用性也是可惜的。我们更愿意倒置这种依赖关系。
    在这里插入图片描述
    图中显示了 Regulate 函数接受了两个接口参数。Thermometer 接口可以读取,而 Heater 接口可以启动和停止。Regulate 算法需要的就是这些。这就倒置了依赖关系,使得高层的调节策略不再依赖于任何温度计或者熔炉的特定细节。该算法具有很好的可重用性。

    通用的调节器算法:

      void Regulate(Thermometer t, Heater h, double minTemp,
         double maxTemp)
      {
        for(;;)
        {
           while (t.Read() > minTemp)
              wait(1);
           h.Engate();
    
           while (t.Read() < maxTemp)
              wait(1);
           h.Disengage();
        }
      }
    

    怎么使用依赖倒置原则


    1. 依赖于抽象

    • 任何变量都不应该持有一个指向具体类的指针或引用。

    如:

    class class1{
    class2* cls2 = new class2();
    }
    class class2{
    .......
    }
    
    • 任何类都不应该从具体类派生。

    2. 设计接口而非设计实现

    • 使用继承避免对类的直接绑定
      在这里插入图片描述

    • 抽象类/接口: 倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口/类

    例外:

    有些类不可能变化,在可以直接使用具体类的情况下,不需要插入抽象层,如:字符串类

    3. 避免传递依赖

    • 避免高层依赖于低层
      在这里插入图片描述
    • 使用继承和抽象类来有效地消除传递依赖
      在这里插入图片描述

    依赖倒置原则的优点


    可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。

    七、 组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)


    尽量使用组合/聚合,不要使用类继承。

    概念理解


    即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的。

    组合和聚合都是关联的特殊种类。

    聚合表示整体和部分的关系,表示“拥有”。组合则是一种更强的“拥有”,部分和整体的生命周期一样。

    组合的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。

    组合是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。

    在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

    什么时候才应该使用继承


    只有当以下的条件全部被满足时,才应当使用继承关系:

    • 1)派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分"Has-A"和"Is-A"。只有"Is-A"关系才符合继承关系,"Has-A"关系应当用聚合来描述。

    • 2)永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。

    • 3)派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。

    • 4)只有在分类学角度上有意义时,才可以使用继承。

    总的来说:

    如果语义上存在着明确的"Is-A"关系,并且这种关系是稳定的、不变的,则考虑使用继承;如果没有"Is-A"关系,或者这种关系是可变的,使用组合。另外一个就是只有两个类满足里氏替换原则的时候,才可能是"Is-A" 关系。也就是说,如果两个类是"Has-A"关系,但是设计成了继承,那么肯定违反里氏替换原则。

    错误的使用继承而不是组合/聚合的一个常见原因是错误的把"Has-A"当成了"Is-A" 。"Is-A"代表一个类是另外一个类的一种;"Has-A"代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。

    看一个例子:

    如果我们把“人”当成一个类,然后把“雇员”,“经理”,“学生”当成是“人”的派生类。这个的错误在于把 “角色” 的等级结构和 “人” 的等级结构混淆了。“经理”,“雇员”,“学生”是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是学生,这显然不合理。
    在这里插入图片描述
    正确的设计是有个抽象类 “角色”,“人”可以拥有多个“角色”(聚合),“雇员”,“经理”,“学生”是“角色”的派生类。
    在这里插入图片描述

    通过组合/聚合复用的优缺点


    优点:

    • 1.新对象存取子对象的唯一方法是通过子对象的接口。
    • 2.这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的。
    • 3.这种复用更好地支持封装性。
    • 4.这种复用实现上的相互依赖性比较小。
    • 5.每一个新的类可以将焦点集中在一个任务上。
    • 6.这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
    • 7.作为复用手段可以应用到几乎任何环境中去。

    缺点: 就是系统中会有较多的对象需要管理。

    通过继承来进行复用的优缺点


    优点:

    • 新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。
    • 修改和扩展继承而来的实现较为容易。

    缺点:

    • 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。

    • 如果基类发生改变,那么派生类的实现也不得不发生改变。

    • 从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。

    展开全文
  • 数据库设计原则

    千次阅读 2019-06-09 09:52:33
    一,数据库设计原则 1. 原始单据与实体之间的关系  可以是一对一、一对多、多对多的关系。在一般情况下,它们是一对一的关系:即一张原始单据对应且只对应一个实体。 在特殊情况下,它们可能是一对多或多对一的...
  • 【Golang】interface接口设计原则

    万次阅读 2020-06-15 12:22:19
      我们就将这种的设计原则叫做依赖倒转原则 来一起看一下修改的代码: package main import "fmt" // ===== > 抽象层 () } type Driver interface { Drive(car Car) } // ===== > 实现层 (benz * BenZ) Run() { ...
  • 架构设计原则总结

    千次阅读 2018-09-14 15:27:51
    在系统设计时,应该多思考墨菲定律:  1、任何事都没有表面看起来那么简单;  2、所有的事情比你预计的时间长;  3、可能出错的事总会出错;  4、如果你担心某种情况发生,那么它就更有可能发生;  在系统划分...
  • 优秀的API接口设计原则及方法

    万次阅读 2019-05-28 15:30:37
    二、设计原则   1. 充分理由  不是随便一个功能就要有个接口,也不是随便一个需求就要加个接口。每新建一个接口,就要有充分的理由和考虑,即这个接口的存在是十分有意义额价值的,无意义的接口不仅增加了...
  • 写了这么久代码你了解Java面向对象的设计原则吗?

    千次阅读 多人点赞 2021-09-17 09:59:18
    面向对象设计原则是学习设计模式的基础,每一种设计模式都符合某一种或多种面向对象设计原则。通过在软件开发中使用这些原则,可以提高软件的可维护性和可复用性,让我们可以设计出更加灵活也更容易扩展的软件系统,实现...
  • 尼尔森十大交互设计原则

    千次阅读 2019-04-21 12:16:30
    Jakob Nielsen(雅各布·尼尔森)的十大交互设计原则。它们被称为“启发式”,因为它们是广泛的经验法则,而不是特定的可用性指导原则。因此,我们不能把它上升为一种标准,而是应该当做一种经验来学习,然后跟现实...
  • 六大设计原则

    千次阅读 2019-02-22 21:54:02
    六大设计原则 1.单一职责原则(Single Responsibility Principle) 定义:应该有且仅有一个原因引起类的变更。 单一职责原则的好处: 类的复杂性降低,实现什么职责都有清晰明确的定义 可读性提高,复杂性降低 可...
  • 简介: c++ 设计模式(8大设计原则、23中设计模式)李建忠 笔记总结 文章目录本博文的简述or解决问题?重要提示:重新认识面向对象面向对象设计原则(一共八种)将设计原则提升为设计经验C++设计模式(一共23种)本...
  • 软件工程六大设计原则

    千次阅读 2020-01-07 16:57:43
    1、单一职责原则 概念描述 对类来说,一个类应该只负责一项职责。如果一个类负责两个职责,可能存在职责1变化,引起职责2的变化情况。可以基于抽象逻辑,或者业务逻辑对类进行细化。 2、接口隔离原则 概念描述...
  • JAVA软件架构七大设计原则详解

    千次阅读 多人点赞 2019-12-13 15:56:37
    可以提高软件系统的可复用性和可维护性,是面向对象设计中最基础的设计原则。例如,很多项目都是迭代开发,我们想增加新的功能,尽量不修改原有的类中代码进行扩展。实现开闭原则的核心就是面向抽象编程,下面用代码...
  • 七大设计原则

    千次阅读 2018-11-17 10:44:10
    七大原则是程序员架构之路上躲...设计原则的出现是为了提高系统的可维护性和可复用性,提高系统的高内聚和低耦合! 每种设计原则的组合和使用都是在业务场景和需求量中进行取舍! =================================...
  • 安全机制设计原则

    千次阅读 2020-03-21 16:27:56
    安全机制设计八大原则: ①经济性原则(Economy of Mechanism) 安全机制设计尽可能简单短小,从而在排查缺陷、检测漏洞时代码更容易处理 ②默认拒绝原则(Fail-Safe Defaults) 只要没有授权的信息就不允许访问 ...
  • 九大交互设计原则

    千次阅读 2019-08-23 18:25:56
    交互设计作为原型设计里的重要环节,了解交互设计原则,给交互设计提供更多设计支持。 1. Fitts’ Law / 菲茨定律(费茨法则) 目标离的越远,到达就越是费劲。目标越小,就越难点中。如果我们要想鼠标比较快速...
  • 系统设计原则

    千次阅读 2018-10-03 20:27:35
    业务设计原则 高并发原则 1. 无状态 若应用无状态、方便水平扩展、则要保证配置服务有状态 eg. 不同的机房需要读取不同的数据源、此时可以通过配置中心指定 2. 拆分 拆分不是必须的、可以根据系统流量和人员...
  • 学习6大设计原则、23种设计模式

    万次阅读 多人点赞 2018-03-14 17:35:57
    了解设计模式的朋友们,想必都听说过“六大设计原则”吧。其实最经典的 23 种设计模式中或多或少地都在使用这些设计原则,也就是说,设计模式是站在设计原则的基础之上的。所以在学习设计模式之前,很有必要对这些...
  • 本篇概括性的介绍了OOD的设计原则,后续还有更多文章会详细剖析、吃透面向对象业务设计的原则。 什么是面向对象设计?它是怎么一回事?使用它会有什么利弊得失?似乎问出这些问题显得有些愚蠢,特别是在一个几乎每...
  • iOS 设计主旨和设计原则

    千次阅读 2018-02-06 15:52:38
    如果您设计并发布了高质量高功能的非凡产品,那么可能会位于App Store排行榜的顶端。 一、 三大主旨将iOS与其他平台区分开来: ① Clarity (清楚,明晰;透明) 在整个系统中,文字在每个尺寸上都是清晰的,图标...
  • 微服务的设计原则

    千次阅读 2019-05-30 20:01:16
    一 前言 ...那么关于微服务的设计原则有哪些呢?如下: AKF 拆分原则 前后端分离原则 无状态服务 RestFul 的通信风格 二AKF 拆分原则 有句挺流行的话:没有什么事是一顿烧烤解决不了的,如果...
  • 产品的10大设计原则

    千次阅读 2019-03-08 10:56:18
    迪特·拉姆斯提出了做好硬件产品的10个设计原则,乔布斯把这些产品设计的原则,不仅用在苹果硬件产品上,还用在软件产品上。而张小龙把这些好产品的原理应用到微信设计上。 迪特·拉姆斯的好产品设计的 10 个原则:...
  • web界面设计原则

    万次阅读 多人点赞 2019-07-08 12:06:32
    如果全凭自己的感觉和认识来组合布置设计,很可能自己觉得效果不错,而实际用户体验并不好,用户体验不好有什么后果就不用多说了吧,因此为了更好的进行界面搭建设计,提供几个专业成熟的原则,可以在工作中借鉴遵循...
  • 面向对象的设计原则(一)

    千次阅读 2017-11-15 21:10:45
    这时,遵从面向对象的设计原则,可以在进行设计方案时减少错误设计的产生,从不同的角度提升一个软件结构的设计水平。同时面向对象设计原则也是用于评价一个设计模式的使用效果的重要指标之一,在设计模式的学习中,...
  • Restful API 设计原则

    千次阅读 2018-04-10 05:00:11
    Restful API 设计原则 前言 网络应用程序分为前端和后端两个部分。 当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备……),这也就意味着前端的开发代码、开发框架变得多种多样。...
  • 实验项目一:面向对象设计原则实验 (1)实验目的与原理 ①深入理解和掌握面向对象设计原则。 ②能运用面向对象设计原则对系统进行重构。 ③能绘制系统结构图(类图) (2)实验内容与步骤 ①某绘图软件提供了多种...
  • 面向对象设计原则与23种设计模式

    千次阅读 2019-04-25 11:45:07
    23种设计模式优秀贴汇总: 1、【23种设计模式全解析】 2、【软件设计模式】 ...3、【面向对象设计原则与模式(JDK中模式)】 4、【23种设计模式(推荐)】 5、【设计模式】 6、【设计模式汇总】 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 688,653
精华内容 275,461
关键字:

设计原则