原文:SOLID Principles every Developer Should Know – Bits and Pieces

SOLID Principles every devloper should know

面向对象为软件开发带来了新的设计方式,它使得开发者可以将具有相同目的或功能的数据结组合到一个类中来完成单一的目的,不需要考虑整个应用。

但是,面向对象编程没有减少混乱和不可维护的程序。正是这样,Robert C. Martin发展出了5条指南/准则,让开发者可以易于创建可读且易于维护的程序。

这5条准则就是S.O.L.I.D原则(缩写是Michael Feathers推演出来的)

  • S: Single Responsibilty Principle 单一功能原则
  • O: Open-Closed Principle 开闭原则
  • L: Liskov Substitution Principle 里氏替换
  • I: Interface Segregation Principle 接口分离
  • D: Dependency Inversion Principle 依赖反转

接下来我们详细讨论上述原则。

注意: 本文的大部分例子可能不能满足或者适用现实世界的应用程序。要视你自己的实际设计和使用场景来定。最重要的是理解和掌握如何运用或遵循这些原则。

建议:使用Bit这样的工具来实践SOLID原则,它能帮助你组织,发现和重用构建新应用程序的组件。组件可以在不同项目之间被发现和共享,所以你可以更快地构建应用程序,不妨试试。

单一功能原则 Single Responsibilty Principle

“...You had one job”---Loki to Skurge in Thor: Ragnarok

一个类只做一件工作

一个类只负责一件事。如果一个类有多项责任,它就变耦合了。一个功能的变动会造成另外一个功能改变。

  • 注意: 这条原则不仅仅适用于类,也适用于软件组件和微服务。

例如,考虑这样一个设计:

    class Animal{
constructor(name: string){}
getAnimalName(){}
saveAniamal(a: Animal){}
}

这里的Animal类是否违背了单一功能原则(SRP)?

怎样违背的?

SRP中说一个类应只含一个功能,现在我们能分出两个功能:动物数据管理和动物特性管理。构造函数和getAnimalName管理动物特性,而saveAnimal负责动物在数据库中的存储。

这个设计将来会引发怎样的问题?

那部分如果应用程序对数据库管理相关函数作变更,使用了动物特性功能的代码也要会受影响并且要重新编译来适应新的变更。

可见这个系统显得很死板,好像一个多米诺骨牌效应,触动一张牌就会影响排列中的所有其他牌。

为了符合SRP,我们创建另一个单一功能的类只负责将一个动物存储到一个数据库中:

class Animal {
constuctor(name: string) { }
getAnimalName() { }
} class AnimalDB {
getAnimal(a: Animal) { }
saveAnimal(a: Animal) { }
}
When designing our classes, we should aim to put related features together,
so whenever they tend to change they change for the same reason.
And we should try to separate features if they will change for different reasons.
我们在设计类的时候,要以将相关的特性放在一起为目标,
当他们需要改变时应当是出于相同的原因,
如果我们发现他们会因为不同的原因改变,则需考虑将特性拆分开来
---Steve Fenton

开闭原则 Open-Closed Principle

Software entities(Classes, modules, functions) should be open for extension, not modification.

软件实体(类,模块,函数等)应当对扩展开放,而对变更是封闭的

继续讨论Animal类,

class Animal {
construtor(name: string) { }
getAnimalName() { }
}

我们想遍历一个animal列表并且让发出他们的声音。

//...
const animals: Array<Animals> = [
new Animal('lion'),
new Animal('mouse')
]; function AnimalSound(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++){
if(a[i].name == 'lion')
log('roar');
if(a[i].name == 'mouse')
log('squeak');
}
}
AnimalSound(animals);

AnimalSound这个函数并不符合开闭原则,因为它不能对新的动物种类保持闭合。如果我们添加一种新的动物,Snake:

//....
const animals: Array<Animal> = [
new Animal('lion'),
new Animal('mouse'),
new Animal('snake')
];
//...

我们不得不修改AnimalSoound函数

//...
function AnimalSound(a: Array<Animal>) {
for (int i = 0; i <= a.length; i++){
if(a[i].name == 'lion')
log('roar');
if(a[i].name == 'mouse')
log('squeak');
if(a[i].name == 'snake')
log('hiss');
}
} AnimalSound(animals);

可见,没新增一种动物,AnimalSound函数就要增加新的逻辑。这个例子已经十分简单。当应用程序随着变得更大而且更加复杂时,你会发现每当你增加一种新动物,AnimalSound中的if语句将在程序中不断地重复出现。

那怎样使它符合开闭原则(OCP)呢?

class Animal {
makeSound();
//...
} class Lion extends Animal {
makeSound() {
return 'roar';
}
} class Squirrel extends Animal {
makeSound() {
return 'squeak';
}
} class Snake extends Animal {
makeSound() {
return 'hiss';
}
} //...
function AnimalSound(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
log(a[i].makeSound());
}
} AnimalSound(animals);

现在 Animal类拥有一个虚函数makeSound,我们让每个动物都继承Animal类并且实现自己makeSound的方法。

每种动物都在makeSound添加发声音的实现,遍历动物数组的时候只需要调用它们的makeSound方法。

这样,如果有新动物要添加,AnimalSound不要改变。我们只需要向动物数组中添加新的动物。

再举一例:

假设你有一家商店,你希望给你最喜爱的那些顾客20%的优惠,下面是类实现:

class Discount {
giveDiscount() {
return this.price * 0.2;
}
}

当你决定给VIP用户的折扣翻倍,你可能会这样修改类:

class Discount {
giveDiscount() {
if(this.customer == 'fav')
return this.price * 0.2;
if(this.customer == 'vip')
return this.price * 0.4;
}
}

错!这不符合OCP原则,OCP反对这样做。如果你想提供新的折扣给其他不同的顾客,你就得增加新的逻辑。

为了使它符合OCP,我们需要增加一个类来扩展Discount类,在新的这个类实现它的新行为:

class VIPDiscount: Discount {
getDiscount() {
return super.getDiscount() * 2;
}
}

如果需要给超级VIP顾客80%的优惠,实现方式可能就是这样:

class SuperVIPDiscount: VIPDiscount {
getDiscount() {
return super.getDiscount() * 2;
}
}

这样,不需修改就实现了扩展。

里氏替换 Liskov Substitution Principle

A sub-class must be substitutable for its super class

子类一定能用父级类替换

这条原则就是目的就是确保子类能无差错地代替父类的位置。如果代码发现它还需要检查子类的类型,那么它就不符合这条原则。

用Animal类来举例:

function AnimalLegCount(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
}
}
AnimalLegCount(animals);

这段代码不符合LSP,也不符合OCP。它必须确定每种动物的类型并调用相应的计腿方法。

每当新增一种动物,这个函数都需要做出修改来适应。

//...
class Pigeon extends Animal { }
const animals[]: Array<Animal>) = [
//...
new Pigeon();
]; function AnimalLegCount(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
if(typeof a[i] == Pigeon)
log(PigeonLegCount(a[i]));
}
}
AimalLegCount(animals);

要使这个函数符合LSP,需要遵循Steven Fenton 提出的以下要求:

  • 如果父类(Animal)有一个接受父类类型(Animal)的参数的方法,它的子类(Pigeon)应该接受一个父类类型(Animal)或子类类型(Pigeon)作为参数
  • 如果父类返回一个父类类型(Animal),其子类应当返回一个父类类型(Animal)或子类类型(Pigeon)。

现在来重新实现AnimalLegCount函数:

function AnimalLegCount(a: Array<Animal>) {
for(let i = 0; i <= a.length; i++) {
log(a[i].LegCount());
}
}
AnimalLegCount(animals);

AnimalLegCount函数现在更少关心传递的Animal的类型,它只是调用LegCount方法。它只知道传入的参数必须是Animal类型,无论是Animal类型还是他的子类。

Animal类型现在需要实现/定义一个LegCount方法:

class Animal {
//...
LegCount();
}

它的子类也需要实现LegCount方法:

class Lion extends Animal{
//...
LegCount() {
//...
}
}

当它被传递给AnimalLegCount函数时,他将返回一头狮子的腿数。

可见AnimalLegCount函数不需要知道Animal的具体类型,只需要调用Animal类的LegCount方法,因为按约定Animal类的子类都必须实现LegCount函数。

接口分离原则 Interface Segregation Principle

Make fine grained interfaces that are client specific

为特定客户制作细粒度的接口

Clients should not be forced to depend upon interfacees that they do not use

客户应当不会被迫以来他们不会使用的接口

这条原则用于处理实现大型接口时的弊端。来看如下接口IShape:

interface Ishape {
drawCircle();
drawSquare();
drawRectangle();
}

这个接口可以画圆形,方形,矩形。Circle类,Square类,Rectangel类实现IShape接口的时候必须定义drawCircle(),drawSqure(),drawRectangle()方法。

class Circle implements Ishape {
drawCircle(){
//...
} drawSquare(){
//...
} drawRectangle(){
//...
}
} class Square implements Ishape {
drawCircle() {
//...
} drawSquare(){
//...
} drawRectangle(){
//...
}
} class Rectangel implements Ishape {
drawCircle() {
//...
} drawSquare(){
//...
} drawRectangle(){
//...
}
}

上面的代码看起来就很怪。Rectangle类药实现它用不上的drawCircle(),drawSquare()方法,Square类和Circle类也同理。

如果我们向Ishape中增加一个接口,如drawTriangle():

interface IShape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}

所有子类都需要实现这个新方法,否则就会报错。

也能看出不可能实现一个可以画圆但是不能画方,或画矩形及三角形的图形类。我们可以只是为上述子类都实现所有方法但是抛出错误指明不正确的操作不能被执行。

ISP不提倡IShape的上述实现。客户(这里的Circle, Rectangle, Square, Triangle)不应被强迫依赖于它们不需要或用不上的方法。ISP还指出一个接口只做一件事(与SRP类似),所有其他分组的行为都应当被抽象到其他的接口中。

这里, Ishape接口执行了本应由其他接口独立处理的行为。

为了使IShape符合ISP原则,我们将这些行为分离到不同的接口中去:

interface Ishape {
draw();
} interface ICircle {
drawCircle();
} interface ISquare {
drawSquare();
} interface IRecetangle {
drawRectangle();
} interface ITriangle {
drawTriangle();
} class Circle implements ICircle {
drawCircle() {
//...
}
} class Square implements ISquare {
drawSquare() {
//...
}
} class Rectangle implements IRectangle {
drawRectangle() {
//...
}
} class Triangle implements ITriangle {
drawTriangle() {
//...
}
} class CustomShape implements IShape {
draw() {
//...
}
}

ICircle接口只处理圆形绘制,IShape处理任意图形的绘制,ISquare只处理方形的绘制,IRectangle只处理矩形的绘制。

或者

子类可以直接从Ishape接口继承并实现自己draw()方法:

class Circle implements IShape {
draw() {
//...
}
} class Triangle implements IShape {
draw() {
//...
}
} class Square implements IShape {
draw() {
//...
}
} class Rectangle implements IShape {
draw() {
//...
}
}

我现在还可以使用I-接口来创建更多特殊形状,如Semi

circle, Right-Angled Triangle, Equilateral Triangle, Blunt-Edged Rectangle等等。

依赖反转 Dependency Inverse Principle

Dependency should be on abstractions not concretion

依赖于抽象而非具体实例

A. High-level modules should not depend upon low-level modules. Both should depend upon avstractions.

B. Abstractions should not depend on deatils. Details should depend upon abstractions.

A. 上层模块不应该依赖于下层模块。它们都应该依赖于抽象。

B. 抽象不应该依赖于细节。细节应该依赖于抽象。

这对开发由许多模块构成的应用程序十分重要。这时候,我们必须使用依赖注入(dependency injection) 来理清关系、上层元件依赖于下层元件来工作。

class XMLHttpService extends XMLHttpRequestService {}

class Http {
constructor(private xmlhttpService:XMLHttpService ){ } get(url: string, options: any) {
this.xmlhttpService.request(url, 'GET');
} post() {
this.xmlhttpService.request(url, 'POST');
}
//...
}

这里Http是上层元件,而HttpService则是下层元件。这个设计违背了DIP原则A: 上层模块不应该依赖于下层模块。它们都应该依赖于抽象。

这个Http类被迫依赖于XMLHttpService类。如果我们想要改变Http连接服务, 我们可能通过Nodejs甚至模拟http服务。我们就要痛苦地移动到所有Http的实例来编辑代码,这将违背OCP(开放闭合)。

Http类应当减少关心使用的Http 服务的类型, 我们建立一个Connection 接口:

interface Connection {
request(url: string, opts: any);
}

Connection接口有一个request方法。我们通过他传递一个Connection类型的参数给Http类:

class Http {
constructor(private httpConnection: Connection) {} get(url: string, options: any) {
this.httpConnection.request(url, 'GET');
} post() {
this.httpConnection.request(url, 'POST');
//...
}
}

现在,无论什么类型的Http连接服务传递过来,Http类都可以轻松的连接到网络,无需关心网络连接的类型。

现在我们可以重新实现XMLHttpService类来实现Connection 接口:

class XMLHttpService implements Connection {
const xhr = new XMLHttpRequest();
//...
request(url: string, opts: any) {
xhr.open();
xhr.send();
}
}

我们可以创建许多的Http Connection类型然后传递给Http类但不会引发任何错误。

class NodeHttpService implements Connection {
request(url: string, opts: any){
//...
}
} class MockHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}

现在,可以看到上层模块和下层模块都依赖于抽象。 Http类(上层模块)依赖于Connection接口(抽象),而且Http服务类型(下层模块)也依赖于Connection接口(抽象)。

结语

我们讨论了每个软件开发者都需要遵从的五大原则。刚开始的时候要遵守这些原则可能会有点难,但是通过持续的练习和坚持,它将成为我们的一部分并且对维护我们的应用程序产生巨大的影响。

如果您有任何疑问或者有认为需要增加,更正或者移除的内容,尽管在下方留言,我会乐意与您讨论!

原文:SOLID Principles every Developer Should Know – Bits and Pieces

[译]开发者须知的SOLID原则的更多相关文章

  1. 【译】浅谈SOLID原则

    SOLID原则是一种编码的标准,为了避免不良设计,所有的软件开发人员都应该清楚这些原则.SOLID原则是由Robert C Martin推广并被广泛引用于面向对象编程中.正确使用这些规范将提升你的代码 ...

  2. 每个开发者都应该知道的SOLID原则

    每个开发者都应该知道的SOLID原则 单一职责原则(SRP) 它为什么违反了 SRP? 这种设计将来会带来什么问题? 开闭原则(OCP) 如何使它(AnimalSound)符合 OCP? 里氏替换原则 ...

  3. 每个Web开发者都应该知道的SOLID原则

    面向对象的编程并不能防止难以理解或不可维护的程序.因此,Robert C. Martin 制定了五项指导原则,使开发人员很容易创建出可读性强且可维护的程序.这五项原则被称为 S.O.L.I.D 原则. ...

  4. 浅谈 SOLID 原则的具体使用

    SOLID 是面向对象设计5大重要原则的首字母缩写,当我们设计类和模块时,遵守 SOLID 原则可以让软件更加健壮和稳定.那么,什么是 SOLID 原则呢?本篇文章我将谈谈 SOLID 原则在软件开发 ...

  5. 【转】面向对象设计的SOLID原则

    S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility ...

  6. 面向对象设计的SOLID原则

    S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility ...

  7. SOLID 原则

     世纪的前几年里,“ Uncle Bob”Robert Martin 引入了用OOP 开发软件的五条原 则,其目的是设计出更易于维护的高质量系统.无论是设计新应用程序,还是重构现有基 本代码,这些 S ...

  8. [译] 开发者角度,王道之论:Android 与 Windows Phone

    前几天,在codeproject搜索Silverlight资料,偶然看到这篇文章,耐心读了2遍,非常不错:文章通过访谈聊天形式叙述,2位主角目前在<斯法克斯国家工程学院>软件学院上学. 周 ...

  9. 面向对象涉及SOLID原则

    S = Single Responsibility Principle 单一职责原则 O = Opened Closed Principle 开放闭合原则  L = Liscov Substituti ...

随机推荐

  1. 洛谷 P1144 最短路计数 题解

    P1144 最短路计数 题目描述 给出一个\(N\)个顶点\(M\)条边的无向无权图,顶点编号为\(1-N\).问从顶点\(1\)开始,到其他每个点的最短路有几条. 输入格式 第一行包含\(2\)个正 ...

  2. [技术博客] 【vagrant】硬盘扩容

    同样,这也是少昂早年走过的坑,这里直接贴出少昂个人博客链接:https://www.cnblogs.com/HansBug/p/9447020.html PS:有一位经验丰富的后端大佬坐镇指挥是多么幸 ...

  3. [Beta阶段]第十一次Scrum Meeting

    Scrum Meeting博客目录 [Beta阶段]第十一次Scrum Meeting 基本信息 名称 时间 地点 时长 第十一次Scrum Meeting 19/05/20 大运村寝室6楼 15mi ...

  4. 第08组 Alpha冲刺(2/4)

    队名 八组评分了吗 组长博客 小李的博客 作业博客 作业链接 组员1李昕晖(组长) 过去两天完成了哪些任务 文字/口头描述 11月17日了解各个小组的进度与难以攻破的地方,与隔壁第七组组长讨论进度发展 ...

  5. OpenGL的核心模式与立即渲染模式

    早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便.OpenGL的大多数功能都被库隐藏起来,开发者很少能控制OpenGL如何进行计算的自由 ...

  6. git clone指定branch或tag

    git clone指定branch或tag发布时间:October 28, 2018 // 分类: // No Comments 取完整: git clone https://github.com/a ...

  7. H3C/华为交换机配置NTP客户端

    H3C clock timezone UTC add ntp-service unicast-server 1.1.1.1 //ntp服务器地址 clock protocol ntp ntp-serv ...

  8. WebRTC搭建前端视频聊天室——信令篇

    这篇文章讲述了WebRTC中所涉及的信令交换以及聊天室中的信令交换,主要内容来自WebRTC in the real world: STUN, TURN and signaling,我在这里提取出的一 ...

  9. [转][c++]关于构造函数不能有返回类型的错误

    转自:https://blog.csdn.net/sky_freebird/article/details/6687892 构造函数不能有返回类型,可是自己定义的构造函数本来就没写返回类型啊. 最后发 ...

  10. matlab学习笔记11_3高维数组操作 filp, shiftdim, size, permute, ipermute

    一起来学matlab-matlab学习笔记11 11_3 高维数组处理和运算 filp, shiftdim, size, permute, ipermute 觉得有用的话,欢迎一起讨论相互学习~Fol ...