国外做外链常用的网站,500强室内设计公司排名,网站后台界面 园林设计,怎么制作公众号动图每个开发人员都应了解的 SOLID 原则 面向对象的编程类型为软件开发带来了全新的设计。 这使开发人员能够将具有相同目的/功能的数据合并到一个类中#xff0c;以处理该类的唯一目的#xff0c;而无需考虑整个应用程序。 但是#xff0c;这种面向对象编程并不能防止程序混乱或… 每个开发人员都应了解的 SOLID 原则 面向对象的编程类型为软件开发带来了全新的设计。 这使开发人员能够将具有相同目的/功能的数据合并到一个类中以处理该类的唯一目的而无需考虑整个应用程序。 但是这种面向对象编程并不能防止程序混乱或无法维护。 因此罗伯特-C-马丁Robert C. Martin制定了五项准则。这五条准则/原则使开发人员能够轻松创建可读和可维护的程序。 这五项原则被称为 S.O.L.I.D 原则缩写由 Michael Feathers 提出。 S单一责任原则 O: 开放-封闭原则 L利斯科夫替代原则 I接口隔离原则 D依赖反转原则 下面我们将详细讨论这些原则。 注本文中的大多数示例可能并不适合实际情况或不适用于现实世界的应用。这完全取决于你自己的设计和用例。最重要的是理解并知道如何应用/遵循这些原则。 提示使用 Bit (GitHub) 等工具可以轻松地在项目和应用程序中共享和重用组件和小模块。 它还能帮助您和您的团队节省时间、保持同步并加快共同开发的速度。它是免费的不妨一试。 作者的推荐协作工具 跨应用程序和项目轻松共享组件 组件发现与协作 - Bit Bit 是开发人员共享组件和协作的地方让他们共同打造令人惊叹的软件。发现共享组件... 单一责任原则 ......你只有一项工作--《雷神索尔毁灭之战》中洛基对斯库奇说 一个类应该只有一项工作。 一个类只能负责一件事。如果一个类有多个职责它就会变得耦合。一个职责的改变会导致另一个职责的修改。 注这一原则不仅适用于类也适用于软件组件和微服务。 例如请考虑以下设计 class Animal { constructor(name: string){ } # 构造函数名称字符串 getAnimalName() { } saveAnimal(a: Animal) { }} Animal 类的构造违反了 SRP 如何违反 SRP 的 SRP 规定类应有一项职责在这里可以得出两项职责 动物数据库管理和动物属性管理。 构造函数和 getAnimalName 管理动物属性而 saveAnimal 则管理动物在数据库中的存储。 这种设计将来会产生什么问题 如果应用程序发生变化影响到数据库管理功能。使用动物属性的类就必须修改并重新编译以适应新的变化。 你看充满了僵化的味道就像多米诺骨牌效应触动一张牌就会影响到其他所有的牌。 为了使这个系统符合 SRP我们创建了另一个类专门负责将动物存储到数据库中 class Animal { constructor(name: string){ } getAnimalName() { }}class AnimalDB { getAnimal(a: Animal) { } saveAnimal(a: Animal) { }} 在设计我们的类时我们应该将相关的功能放在一起这样当它们发生变化时变化的原因就会相同。如果功能变化的原因不同则应尽量将它们分开。- 史蒂夫-芬顿 正确运用这些原则我们的应用程序就会变得高度内聚。 开放-封闭原则 软件实体类、模块、函数应开放供扩展而非修改。 让我们继续我们的动物类。 class Animal { constructor(name: string){ } getAnimalName() { }} 我们要遍历动物列表并发出它们的声音。 //...const animals ArrayAnimal [ new Animal(lion)、 new Animal(mouse)];function AnimalSound(a: ArrayAnimal) { 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 不符合开放-封闭原则因为它不能对新的动物种类进行封闭。 如果我们添加一种新的动物蛇 //...const animals ArrayAnimal [ new Animal(lion)、 new Animal(mouse)、 new Animal(snake)]//... 我们必须修改 AnimalSound 函数 //...function AnimalSound(a: ArrayAnimal) { 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 蛇) log(hiss) }}AnimalSound(animals) 你看每出现一种新动物AnimalSound 函数就会增加一个新逻辑。 这只是一个简单的例子。当您的应用程序发展壮大并变得复杂时您会发现每次添加新动物时if 语句都会在 AnimalSound 函数中重复出现遍布整个应用程序。 我们如何使它AnimalSound符合 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: ArrayAnimal) { for(int i 0; i a.length; i) { log(a[i].makeSound()); }}AnimalSound(animals); 动物现在有了一个虚拟方法 makeSound。我们让每个动物扩展 Animal 类并实现虚拟 makeSound 方法。 每种动物都会在 makeSound 中添加自己的发声方法。AnimalSound 会遍历动物数组并调用其 makeSound 方法。 现在如果我们添加一个新的动物AnimalSound 不需要更改。我们只需将新动物添加到动物数组中即可。 现在AnimalSound 符合 OCP 原则。 另一个例子 假设你有一家商店你可以使用这个类给你最喜欢的顾客打八折 class Discount { giveDiscount() { return this.price * 0.2 }} 当您决定向 VIP 客户提供双倍的 20% 折扣时。您可以这样修改该类 class Discount { giveDiscount() { if(this.customer fav) { return this.price * 0.2; } if(this.customer vip) { return this.price * 0.4; } }} 不这违反了 OCP 原则。 OCP 禁止这样做。如果我们想给不同类型的客户提供新的折扣你会发现需要添加一个新的逻辑。 为了遵循 OCP 原则我们将添加一个新类来扩展折扣类。在这个新类中我们将实现它的新行为 class VIPDiscount: Discount { getDiscount() { return super.getDiscount() * 2; }}如果您决定向超级 VIP 客户提供 80% 的折扣它应该是这样的class SuperVIPDiscount: VIPDiscount { getDiscount() { return super.getDiscount() * 2; }} 你看扩展无需修改。 利斯科夫替代原则 子类必须可以替代其超类 该原则的目的是确保子类可以替代其超类而不会出错。如果代码发现自己在检查类的类型那么它一定违反了这一原则。 让我们以动物为例。 //...function AnimalLegCount(a: ArrayAnimal) { 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] 蛇) log(SnakeLegCount(a[i])) }}AnimalLegCount(animals) 这违反了 LSP 原则以及 OCP 原则。它必须知道每一种动物类型并调用相关的计算腿函数。 每创建一个新动物都必须修改函数以接受新动物。 //...class Pigeon extends Animal { }const animals[]: ArrayAnimal [ //..., new Pigeon();]function AnimalLegCount(a: ArrayAnimal) { 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])); }}AnimalLegCount(animals); 为了使该函数遵循 LSP 原则我们将遵循 Steve Fenton 提出的 LSP 要求 如果超类 Animal有一个接受超类类型 Animal参数的方法。其子类 Pigeon 应接受超类类型 Animal 类型或子类类型 Pigeon 类型 作为参数。 现在我们可以重新实现 AnimalLegCount 函数 function AnimalLegCount(a: ArrayAnimal) { for(let i 0; i a.length; i) { a[i].LegCount() }}AnimalLegCount(animals) AnimalLegCount 函数不关心传递的动物类型它只是调用 LegCount 方法。 它只知道参数必须是 Animal 类型要么是 Animal 类要么是它的子类。 现在动物类必须实现/定义一个 LegCount 方法 class Animal { //... LegCount();} 它的子类必须实现 LegCount 方法 //...class Lion extends Animal{ //... LegCount() { //... }}//... 当传递给 AnimalLegCount 函数时它会返回狮子的腿数。 你看AnimalLegCount 不需要知道 Animal 的类型就能返回它的腿数它只需调用 Animal 类型的 LegCount 方法 因为根据契约Animal 类的子类必须实现 LegCount 函数。 接口隔离原则 制作客户机专用的细粒度接口 不应强迫客户依赖他们不使用的接口。 这一原则解决了实现大型接口的弊端。 让我们看看下面的 IShape 接口 interface IShape { drawCircle(); drawSquare(); drawRectangle();} 该接口用于绘制正方形、圆形和矩形。 实现 IShape 接口的类 Circle、Square 或 Rectangle 必须定义 drawCircle()、drawSquare()、drawRectangle() 方法。 class Circle implements IShape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }class Square implements IShape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }class Rectangle implements IShape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } } 上面的代码非常有趣 矩形类实现了它用不上的方法 drawCircle 和 drawSquare 正方形类实现了drawCircle 和 drawRectangle 圆形类实现了 drawSquare、drawSquare 如果我们在 IShape 接口中再添加一个方法 如 drawTriangle() interface IShape { drawCircle(); drawSquare(); drawRectangle(); drawTriangle();} 这些类必须实现新方法否则会出错。 我们看到要实现一个能画圆但不能画矩形、正方形或三角形的形状是不可能的。我们只需实现抛出错误的方法说明无法执行操作即可。 ISP 不赞成这种 IShape 接口的设计。 客户端此处为矩形、圆形和正方形不应被迫依赖于它们不需要或不使用的方法。 此外ISP 还规定接口应只执行一项工作就像 SRP 原则一样任何额外的行为分组都应抽象到另一个接口中。 在这里我们的 IShape 接口执行的操作应由其他接口独立处理。 为了使 IShape 接口符合 ISP 原则我们将这些行为分离到不同的接口中 interface IShape { draw();}interface ICircle { drawCircle();}interface ISquare { drawSquare();}interface IRectangle { 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 接口并实现自己的绘制行为。 class Circle implements IShape { draw(){ //... }}class Triangle implements IShape { draw(){ //... }}class Square implements IShape { draw(){ //... }}class Rectangle implements IShape { draw(){ //... }} 然后我们就可以使用 I 接口创建半圆、直角三角形、等边三角形、钝角矩形等特定形状。 依赖反转原则 应依赖于抽象而非具体事物 A. 高层模块不应依赖低层模块。两者都应依赖抽象。 B. 抽象不应依赖细节。细节应依赖抽象。 在软件开发过程中我们的应用程序将主要由模块组成。这时我们必须使用依赖注入来理清思路。高层组件依赖于低层组件来运行。 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 接口有一个请求方法。有了它我们就可以向 Http 类传递 Connection 类型的参数 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 连接类型并将其传递给我们的 Http 类而不必担心出错。 class NodeHttpService implements Connection { request(url: string, opts:any) { //... }}class MockHttpService implements Connection { request(url: string, opts:any) { //... } } 现在我们可以看到高层模块和低层模块都依赖于抽象。 Http 类高级模块依赖于 Connection 接口抽象而 Http 服务类型低级模块反过来也依赖于 Connection 接口抽象。 此外DIP 将迫使我们不违反利斯科夫替代原则连接类型 Node-XML-MockHttpService 可替代其父类型 Connection。 结论 我们在这里介绍了每个软件开发人员都必须遵守的五项原则。一开始遵守所有这些原则可能会让人望而生畏但通过不断的实践和坚持这些原则将成为我们的一部分并将对我们应用程序的维护产生巨大的影响。 本文由 mdnice 多平台发布