软件常用设计原则与实践:契约式接口设计、安全编码实践

软件设计原则与实践(汇总)

本文是我在网上各种收集翻译整理后的缝合怪。

1. 软件组件设计原则:SOLID (面向对象设计)

背景

SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)
当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。

SOLID原则具体指代:

  • S (SRP, Single-responsibility principle),单一职责原则,认为对象应该仅具有一种单一功能。

  • O (OCP, Open-closed Principle),开放-封闭原则,认为软件体应该是对于扩展开放的,但是对于修改是封闭的。

  • L ( LSP, Liskov substitution principle),里氏替换原则,认为程序中的对象应该是可以在不改变程序正确性的前提下被它的子类替换的。

  • I( ISP, Interface segregation principle),接口隔离原则,认为多个特定客户端接口要好于一个宽泛用途的接口。

  • D (DIP, Dependency inversion principle),依赖倒转原则,认为一个方法应该遵从「依赖于抽象而不是一个实例」的概念。依赖注入是该原则的一种实现方式。

百科定义: 类的实质是一种引用数据类型,类似于byte、short、int(char)、long、float、double等基本数据类型,不同的是它是一种复杂的数据类型。因为它的本质是数据类型,而不是数据,所以不存在于内存中,不能被直接操作,只有被实例化为对象时,才会变得可操作。类是对现实生活中一类具有共同特征的事物的抽象。

S: 单一职责原则 (SRP)

基本概念

英文全称为 Single Responsibility Principle,是最简单,但也是最难用好的原则之一。它的定义也很简单:对于一个类而言,应该仅有一个引起它变化的原因。其中变化的原因就表示了这个类的职责,它可能是某个特定领域的功能,可能是某个需求的解决方案。
这个原则表达的是不要让一个类承担过多的责任,一旦有了多个职责,那么它就越容易因为某个职责而被更改,这样的状态是不稳定的,不经意的修改很有可能影响到这个类的其他功能。因此,我们需要将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,不同类之间的变化互不影响。

相关设计模式

面对违背单一职责原则的程序代码,我们可以利用外观模式,代理模式,桥接模式,适配器模式,命令模式对已有设计进行重构,实现多职责的分离。

小结

单一职责原则用于控制类的粒度大小,减少类中不相关功能的代码耦合,使得类更加的健壮;另外,单一职责原则也适用于模块之间解耦,对于模块的功能划分有很大的指导意义。

这一条是用来帮助我们创建更为松耦合和模块化的程序,因为每种不同的行为我们都封装到一个新的类或对象里面去了。未来要增加新的行为或特性,新增的内容也会增加新的对象里面,而不是把这些行为加到原来的对象上去。
这样做的好处是更安全,更不容易出错,因为以前的类可能是含有多种多样的依赖关系的,但新增加的类却没有那些依赖在里面。所以我们可以放心的修改系统行为,不必担心修改带来的副作用。

在分层结构中,这个思想也有体现。我们的UI/Presentation层专管UI的显示,logic层专攻业务逻辑,数据访问层只做数据访问相关内容。其实,都是同样的道理。

O:开闭原则 (OCP)

基本概念

开闭原则 (OCP) 英文全称为 Open-Closed Principle,基本定义是软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是封闭的。这里的对扩展开放表示这添加新的代码,就可以让程序行为扩展来满足需求的变化;对修改封闭表示在扩展程序行为时不要修改已有的代码,进而避免影响原有的功能。

要实现不改代码的情况下,仍要去改变系统行为的关键就是抽象和多态,通过接口或者抽象类定义系统的抽象层,再通过具体类来进行扩展。这样一来,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,达到开闭原则的要求。

相关设计模式

面对违背开闭原则的程序代码,可以用到的设计模式有很多,比如工厂模式,观察者模式,模板方法模式,策略模式,组合模式,使用相关设计模式的关键点就是识别出最有可能变化和扩展的部分,然后构造抽象来隔离这些变化。

小结

有了开闭原则,面向需求的变化就能进行快速的调整实现功能,这大大提高系统的灵活性,可重用性和可维护性,但会增加一定的复杂性。

L: 里式替换原则 (LSP)

基本概念

里式替换原则 (LSP) 英文全称为 Liskov Substitution Principle,基本定义为:在不影响程序正确性的基础上,所有使用基类的地方都能使用其子类的对象来替换。这里提到的基类和子类说的就是具有继承关系的两类对象,当我们传递一个子类型对象时,需要保证程序不会改变任何原基类的行为和状态,程序能正常运作。

小结

要让程序代码符合里式替换原则,需要保证子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,换句话就是子类可以扩展父类的功能,但不能改变父类原有的功能。

另一方面,里式替换原则也是对开闭原则的补充,不仅适用于继承关系,还适用于实现关系的设计,常提到的 IS-A 关系是针对行为方式来说的,如果两个类的行为方式是不相容,那么就不应该使用继承,更好的方式是提取公共部分的方法来代替继承。

I:接口隔离原则 (ISP)

基本概念

接口隔离原则 (ISP) 英文全称为 Interface Segregation Principle,基本定义:客户端不应该依赖那些它不需要的接口。客户端应该只依赖它实际使用的方法,因为如果一个接口具备了若干个方法,那就意味着它的实现类都要实现所有接口方法,从代码结构上就十分臃肿。

基于接口隔离原则,我们需要做的就是减少定义大而全的接口,类所要实现的接口应该分解成多个接口,然后根据所需要的功能去实现,并且在使用到接口方法的地方,用对应的接口类型去声明,这样可以解除调用方与对象非相关方法的依赖关系。总结一下,接口隔离原则主要功能就是控制接口的粒度大小,防止暴露给客户端无相关的代码和方法,保证了接口的高内聚,降低与客户端的耦合。

D:依赖倒置原则 (DIP)

基本概念

依赖倒置原则 (DIP 英文全称 Dependency Inversion Principle, DIP),基本定义是:

  • 高层模块不应该依赖低层模块,应该共同依赖抽象;
  • 抽象不应该依赖细节,细节应该依赖抽象。

这里的抽象就是接口和抽象类,而细节就是实现接口或继承抽象类而产生的类。
而最佳的做法,在高层模块构建一个稳定的抽象层,并且只依赖这个抽象层;而由底层模块完成抽象层的实现细节。这样一来,高层类都通过该抽象接口使用下一层,移除了高层对底层实现细节的依赖。

相关设计模式

关于依赖倒置原则,可以用到的设计模式有工厂模式,模板方法模式,策略模式。

小结

依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。同时依赖倒置原则也是框架设计的核心原则,善于创建可重用的框架和富有扩展性的代码。

依赖翻转是构建松耦合应用的关键所在,既然具体的实现都依赖于更高层的抽象了,那么程序就应该更容易被测试、维护和模块化。

额外的原则

一次且仅一次

一次且仅一次(英语:Once and only once,简称OAOO)又称为Don‘t repeat yourself(不要重复你自己,简称DRY)或一个规则,实现一次(One rule, one place)是面向对象编程中的基本原则,程序员的行事准则。旨在软件开发中,减少重复的信息。

DRY的原则是“系统中的每一部分,都必须有一个单一的、明确的、权威的代表”,指的是(由人编写而非机器生成的)代码和测试所构成的系统,必须能够表达所应表达的内容,但是不能含有任何重复代码。当DRY原则被成功应用时,一个系统中任何单个元素的修改都不需要与其逻辑无关的其他元素发生改变。此外,与之逻辑上相关的其他元素的变化均为可预见的、均匀的,并如此保持同步。

违反DRY原则的解决方案通常被称为WET,其有多种全称,包括“Write everything twice”(把每个东西写两次)、“We enjoy typing”(我们就是喜欢打字)或“Waste everyone‘s time”(浪费大家的时间)。

重复代码是后期bug修复的大敌。

我们要尽量避免编程时使用复制和粘贴的功能,如果同样的逻辑在代码里的多个位置出现,每次我们维护的时候就不得不同时维护多处。当功能转交给其他人时,其他人也会厌烦多处查找这些问题;其他人将这类代码转交给你时,你也会头疼不已。

控制反转

通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递(注入)给它。

控制反转的设计目标:

  1. 将执行任务这个动作和任务(具体实现)解耦。

  2. 让模块专注于它自己的设计目标。不需要考虑别的模块实现了什么,是如何实现的。模块之间依赖于接口。

  3. 让符合相同契约的模块能够互相替代。

    看的好像很高深的样子,下面就用大白话来撕下它们的伪装:

    框架:有些太繁琐太基础的事情你就别做了,我已经帮你做好了,但是我做不了的那部分我以坑的形式留给你了,去填完了你的工作也就完成了,后面的事情我来处理吧。

    回调,调度器,事件循环:你先把这个坑填好填扎实了,不要担心没人来看望你,待时机一到我就来和你相会。

    策略模式:我来定义好这个坑的游戏规则(输入和输出),你负责帮我填好。

    模板方法模式:我来定义有哪些坑以及踩坑的顺序,你负责帮我填好。

实现方法:依赖注入

在软件工程中,依赖注入(dependency injection)的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”[1]。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。

注:编程语言层次下,“调用方”为对象和类,“依赖”为变量。在提供服务的角度下,“调用方”为客户端,“依赖”为服务。

该设计的目的是为了分离关注点,分离调用方和依赖,从而提高可读性以及代码重用性。
基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
依赖注入是控制反转的最为常见的一种技术。

2. 编码的实现原则:契约式设计

契约式设计(英语:Design by Contract,缩写为 DbC),一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。

软件常用设计原则与实践:契约式接口设计、安全编码实践

描述

核心思想

DbC的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。这种比喻从商业活动中“客户”与“供应商”达成“契约”而得来。例如:

  • 供应商必须提供某种产品(责任),并且他有权期望客户已经付款(权利)。
  • 客户必须付款(责任),并且有权得到产品(权利)。
  • 契约双方必须履行那些对所有契约都有效的责任,如法律和规定等。

同样的,如果在面向对象程序设计中一个类的函数提供了某种功能,那么它要:

  • 期望所有调用它的客户模块都保证一定的进入条件:这就是函数的先验条件—客户的义务和供应商的权利,这样它就不用去处理不满足先验条件的情况。
  • 保证退出时给出特定的属性:这就是函数的后验条件—供应商的义务,显然也是客户的权利。
  • 在进入时假定,并在退出时保持一些特定的属性:不变条件。

契约的保证

契约就是这些权利和义务的正式形式。我们可以用“三个问题”来总结DbC,并且作为设计者要经常问:

  • 它期望的是什么?
  • 它要保证的是什么?
  • 它要保持的是什么?
    很多编程语言都有对这种断言的支持。然而DbC认为这些契约对于软件的正确性至关重要,它们应当是设计过程的一部分。实际上,DbC提倡首先写断言。

契约的实现要素

契约的概念扩展到了方法/过程的级别。对于一个方法的契约通常包含下面这些信息:

  • 可接受和不可接受的值或类型,以及它们的含义
  • 返回的值或类型,以及它们的含义
  • 可能出现的错误以及异常情况的值和类型,以及它们的含义
  • 副作用
  • 先验条件
  • 后验条件
  • 不变条件
  • 性能上的保证,如所用的时间和空间

继承中的子类型可以弱化先验条件(但不可以加强它们),并且可以加强后验条件和不变式(但不能弱化它们)。这些原则很接近里氏代换原则。

3. 安全编码实践方法

验证输入。

验证来自所有不受信任数据源的输入。正确的输入验证可以消除绝大多数软件漏洞。怀疑大多数外部数据源,包括命令行参数,网络接口,用户输入参数等。

注意编译器警告。

使用可用于编译器的最高警告级别编译代码,并通过修改代码消除警告。使用静态和动态分析工具来检测和消除其他安全缺陷。

安全策略的架构师和设计。

创建软件体系结构并设计软件以实施和实施安全策略。例如,如果系统在不同的时间需要不同的特权,请考虑将系统划分为不同的互通子系统,每个子系统都具有适当的特权集。

把事情简单化。

保持设计尽可能简单和小巧。复杂的设计增加了在其实现,配置和使用中出现错误的可能性。此外,随着安全机制变得越来越复杂,实现适当级别的保证所需的工作量也急剧增加。

默认拒绝。

访问权限的决定基于许可而不是排除。这意味着,默认情况下,访问被拒绝,并且保护方案标识允许访问的条件。

坚持最小特权原则。

每个进程都应以完成作业所需的最少特权集执行。任何提升的权限仅应在完成特权任务所需的最短时间内访问。这种方法减少了攻击者必须以提升的特权执行任意代码的机会。

清理发送到其他系统的数据。

对传递给复杂子系统的所有数据进行消毒(过滤),例如命令外壳,数据操作等。攻击者可能能够通过使用SQL,命令或其他注入攻击来调用这些组件中未使用的功能。这不一定是输入验证问题,因为被调用的复杂子系统无法理解在其中进行调用的上下文。因为调用过程了解上下文,所以它负责在调用子系统之前清理数据。

深入练习防御。

使用多种防御策略来管理风险,这样,如果一层防御措施被证明是不足的,另一层防御措施可以防止安全漏洞成为可利用的漏洞和/或限制成功利用漏洞的后果。例如,将安全编程技术与安全运行时环境相结合,应减少在操作环境中可以利用部署时代码中残留的漏洞的可能性。

使用有效的质量保证技术。

良好的质量保证技术可以有效地识别和消除漏洞。模糊测试,渗透测试和源代码审核都应纳入有效的质量保证计划的一部分。独立的安全审查可以导致更安全的系统。外部审稿人具有独立观点;例如,在识别和纠正无效假设时。

采用安全的编码标准。

为目标开发语言和平台开发和/或应用安全的编码标准。

额外的安全编码实践

  • 定义安全要求。

    在开发生命周期的早期识别并记录安全要求,并确保评估后续的开发工件是否符合那些要求。 如果未定义安全性要求,则无法有效地评估所得系统的安全性。

  • 模型威胁。

    使用威胁建模来预测软件将受到的威胁。 威胁建模包括识别关键资产,分解应用程序,对每个资产或组件的威胁进行识别和分类,基于风险等级对威胁进行评级,然后制定在设计,代码和测试用例中实施的威胁缓解策略。

4. Apache的架构师们遵循的设计原则

本文作者叫Srinath,是一位科学家,软件架构师,也是一名在分布式系统上工作的程序员。 他是Apache Axis2项目的联合创始人,也是Apache Software基金会的成员。 他是WSO2流处理器(wso2.com/analytics)的联席架构师。 Srinath撰写了两本关于MapReduce和许多技术文章的书。 他获得了博士学位。 来自美国印第安纳大学。

Srinath通过不懈的努力最终总结出了30条架构原则,他主张架构师的角色应该由开发团队本身去扮演,而不是专门有个架构师团队或部门。Srinath为了解决团队内部的架构纷争和抉择,制定了以下30条原则,这些原则被成员们广泛认可,也成为了新手架构师的学习途径。

基本原则

原则1: KISS(Keep it simple,sutpid) 和保持每件事情都尽可能的简单。用最简单的解决方案来解决问题。

原则2: YAGNI(You aren’t gonna need it)-不要去搞一些不需要的东西,需要的时候再搞吧。

原则3: 爬,走,跑。换句话说就是先保证跑通,然后再优化变得更好,然后继续优化让其变得伟大。迭代着去做事情,敏捷开发的思路。对于每个功能点,创建里程碑(最大两周),然后去迭代。

原则4: 创建稳定、高质量的产品的唯一方法就是自动化测试。所有的都可以自动化,当你设计时,不妨想想这一点。

原则5: 时刻要想投入产出比(ROI)。就是划得来不。

原则6: 了解你的用户,然后基于此来平衡你需要做哪些事情。不要花了几个月时间做了一个devops用户界面,最后你发现那些人只喜欢命令行。此原则是原则5的一个具体表现。

原则7: 设计和测试一个功能得尽可能的独立。当你做设计时,应该想想这一条。从长远来看这能给你解决很多问题,否则你的功能只能等待系统其他所有的功能都就绪了才能测试,这显然很不好。有了这个原则, 你的版本将会更加的顺畅。

原则8: 不要搞花哨的。我们都喜欢高端炫酷的设计。最后我们搞了很多功能和解决方案到我们的架构中,然后这些东西根本不会被用到。

功能选择原则

原则9: 不可能预测到用户将会如何使用我们的产品。所以要拥抱MVP(Minimal Viable Product),最小可运行版本。这个观点主要思想就是你挑几个很少的使用场景,然后把它搞出来,然后发布上线让用户使用,然后基于体验和用户反馈再决定下一步要做什么。

原则10: 尽可能的做较少的功能。当有疑问的时候,就不要去做,甚至干掉。很多功能从来不会被使用。最多留个扩展点就够了。

原则11: 等到有人提出再说(除非是影响核心流程,否则就等到需要的时候再去做)。

原则12: 有时候你要有勇气和客户说不。这时候你需要找到一个更好的解决方案来去解决。记住亨利福特曾经说过的 :”如果我问人们他们需要什么,他们会说我需要一匹速度更快的马”。记住:你是那个专家,你要去引导和领导。要去做正确的事情,而不是流行的事情。最终用户会感谢你为他们提供了汽车。

服务端设计和并发原则

原则13: 要知道一个server是如何运行的,从硬件到操作系统,直到编程语言。优化IO调用的数量是你通往最好架构的首选之路。

原则14: 要了解Amdhal同步定律。在线程之间共享可变数据会让你的程序变慢。只在必要的时候才去使用并发的数据结构,只在必须使用同步(synchronization)的时候才去使用同步。如果要用锁,也要确保尽可能少的时间去hold住锁。如果要在加锁后做一些事情,要确保自己在锁内会做哪些事情。

原则15: 如果你的设计是一个无阻塞且事件驱动的架构,那么千万不要阻塞线程或者在这些线程中做一些IO操作,如果你做了,你的系统会慢的像骡子一样。

用户体验原则

原则22: 要了解你的用户和清楚他们的目标。他们是新手、专家还是偶然的用户?他们了解计算机科学的程度。极客喜欢扩展点,开发者喜欢示例和脚本,而普通人则喜欢UI。

原则23: 最好的产品是不需要产品手册的。

原则24: 当你无法在两个选择中做决定的时候,请不要直接把这个问题通过提供配置选项的方式传递给用户。这样只能让用户更加的发懵。如果连你这个专家都无法选择的情况下,交给一个比你了解的还少的人这样合适吗?最好的做法的是每次都找到一个可行的选项;次好的做法是自动的给出选项,第三好的做法是增加一个配置参数,然后设置一个合理的默认值。

原则25: 总是要为配置设置一个合理的默认值。

原则26: 设计不良的配置会造成一些困扰。应该总是为配置提供一些示例值。

原则27: 配置值必须是用户能够理解和直接填写的。比如:不能让用户填写最大缓存条目的数量,而是应该让用户填写可被用于缓存的最大内存。

原则28: 如果输入了未知的配置要抛出错误。永远不要悄悄的忽略。悄悄的忽略配置错误往往是找bug花了数小时的罪魁祸首。

总结

作为一个架构师,应该像园丁一般,更多的是修剪花草,除草而不是去定义和构建,你应该策划而不是指挥,你应该去修剪而不是去定义,应该是讨论而不是贴标签。虽然在短期内可能会觉得也没什么,但从长远看,指导团队找到自己的方式会带来好处。如果你稍不留神,就很容易让架构成为一个空洞的词汇。比如设计者会说他的架构是错误的,但不知道为什么是错误的。一个避免这种情况的好办法就是有一个原则列表,这个原则列表是被广泛接受的,这个列表是人们讨论问题的锚点,也是新手架构师学习的路径。

软件常用设计原则与实践:契约式接口设计、安全编码实践

上一篇:vue-cli3, vue 打包生产环境 npm run build 去掉console


下一篇:LAMP