【设计原则】循序渐进理解设计原则,不再死记硬背

363 阅读13分钟

一、引言

程序员同志们,我们要深入贯彻面向对象编程的思想,以设计原则为指导方针,在实践中灵活运用设计模式,一步一步实现成为一名开发老油条的目标!!!

1.1 贯彻思想:面向对象编程

相信很多初入职场的程序员,跟笔者一样,会感受到在学校里写代码做实验和在公司里做项目搞开发存在很大不同,最明显的区别是:

  • 在学校:面向结果编程

    写代码自己看得懂就行,能用就行,基本没有设计

  • 在公司:面向对象编程

    既要看别人写的代码,自己写的代码又要给别人看。软件设计之初架构师会充分考虑可维护性,设计合理代码框架,每个人在已有的框架下进行业务开发。

因此,软件开发工程师要完成从学校到职场的转变,最重要的就是要从面向结果编程转变为面向对象编程

1.2 指导方针:设计原则

前人通过长期的实践和积累,总结出了面向对象编程的七大设计原则。遵循设计原则,总结出了实践中常用的23种设计模式。许多人在工作后会捧起一本《大话设计模式》:

image.png

🤔学习过程中可能有人会遇到问题:7种设计原则只能记住名字,每个原则对应的定义可能都说不明白,即使能记住定义,也是死记硬背的,没有真正理解其中的内涵,更谈不上在实践中去灵活运用了

设计原则定义
单一职责一个类或者模块只负责完成一个职责
开闭原则一个软件实体应当对扩展开放,对修改关闭。
里氏替换所有引用基类的地方必须透明地使用其子类的对象
接口隔离建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少
依赖倒置高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象
迪米特法则一个类应该对自己需要耦合或调用的类知道得最少
合成复用在复用功能时,应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系

单一职责原则(Single Responsibility Principle,SRP)、开闭原则(Open-Closed Principle,OCP)、里氏替换原则(Liskov Substitution Principle,LSP)、接口隔离原则(Interface Segregation Principle,ISP)、依赖倒置原则(Dependency Inversion Principle,DIP)又合称为 SOLID 原则

如何将设计原则融会贯通? 本文旨在帮助你循序渐进理解这几种设计模式,摆脱毫无作用的死记硬背

二、设计原则

接下来以世界性难题——“今晚吃什么?” 分享笔者如何记忆理解SOLID原则

2.1 单一职责

单一职责指的是一个类只能因为一个原因被修改,一个类只做一件事

下班了下班了,晚上吃啥?好久没吃肯德基了,那今晚K一下吧!

于是先创建一个肯德基类:

image.png

单一职责原则比较好理解

  • 一个类只负责一件事:肯德基只是我们吃饭的地方,什么?你说你还想吃完饭泡个脚?那出门右拐大唐足道,肯德基里没有技师
  • 一个类只能因为一个原因被修改:职责是引起类变化的原因。肯德基里的只能更新汉堡炸鸡套餐,精油开背套餐不能加在这里

✨单一职责原则最重要的作用是指导我们在设计时要注意 『职责拆分』 ,如果发现一个类太大了,职责和功能太多,就要考虑拆分出若干个细粒度小、功能单一的类,通过类与类之间组合、聚合等关系,来共同实现某个复杂功能。

因此我们可以粗略拆分出三个类:点餐类、取餐类、用餐类。三个类各司其职,相互协作,共同解决了我的晚饭问题

image.png

对于肯德基类来说,它不用关注每个点餐、取餐、用餐的具体实现细节,只需要依次执行即可,这样就做到了高内聚低耦合

2.2 开闭原则

一个软件实体应当对扩展开放,对修改关闭。也就是说设计的模块能在不被修改源代码的前提下,对其进行扩展。

那有人说,你为什么不吃麦当劳?

麦当劳当然也会吃,我还经常吃大米先生啥的

👨‍💻假如今天帮同事解决了一个bug

为表谢意,同事说:牛啊牛啊!今晚请你吃饭!✔

虽然我们经常一起去吃肯德基,但是同事不会说:牛啊牛啊!今晚请你吃肯德基!❌

你看,怪不得是程序员呢,“请你吃饭”比起“请你吃肯德基”,就完美遵守了开闭原则

image.png
  • 对扩展开放: 除了肯德基,我们平时也经常吃麦当劳、大米先生,但是他不知道前天附近新开了一家遇见小面,那今晚也可以让他请我吃遇见小面

  • 对修改关闭: 同事说今晚要开会就不和我一起吃了,准备给我发个红包报销我的晚餐费。这是非常自然而然的做法,不会有人会像下面这样:如果我想吃肯德基,替我打开肯德基小程序点餐付款;如果我想吃麦当劳,替我打开麦当劳小程序点餐付款......那我想吃遇见小面的话,他还得找到遇见小面的点餐小程序呢。同事不需要替我进行晚餐选择的逻辑判断,只需要做一件事——发红包就行了

💡实现开放封闭的核心思想就是面对抽象编程,而不是面对具体编程,因为抽象相对稳定。 让类依赖于固定的抽象,所以对修改是封闭的;而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。

2.3 里氏替换原则

里氏替换原则要求所有引用基类(父类)的地方必须透明地使用其子类的对象。可以通俗地理解为:在软件中如果将基类对象替换为子类对象,程序不会出现错误。

✨根据开闭原则,程序中的参数或引用类型尽量使用抽象类或者接口。具体的功能实现交给子类或者实现类完成

里氏替换原则可以指导继承关系中子类的合理性,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

比方说,如果给晚饭类添加一个子类——徐记海鲜,人均两百,那么对于请客的同事来说,破坏了他原有的计划,因为他原来只打算30块钱以内搞定的。所以徐记海鲜不适合作为同事请的晚饭的子类。

image.png

如何理解多态和里氏替换原则:

  • 里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

  • 如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

2.4 接口隔离

接口隔离原则要求我们将一些较大的接口进行细化,使用多个专门的接口来替换单一的总接口,也就是使用某接口的客户端仅需知道与之相关的接口。

✨根据开闭原则,程序中的参数或引用类型尽量使用抽象类或者接口。具体的功能实现交给子类或者实现类完成

(C++中没有Java中接口的专门表示,为了方便阐述,统一叫做抽象类和抽象方法吧)

肯德基作为一个具体的类,通常会继承某几个抽象类,然后实现其中的抽象方法。“某几个抽象类”这几个字很重要,对于接口隔离原则来说,首先一定是有多个抽象方法,然后互相之间是属于不同的功能,才需要将它们隔离。所谓隔离,并不是要把肯德基本身做什么肢解,而是多个抽象方法分别来自于肯德基类所继承的不同的抽象类,

举个例子,肯德基是个吃快餐的地方,也是可以做兼职的地方。肯德基要分别提供和快餐相关的方法,以及和兼职相关的方法。按照接口隔离原则,需要创建快餐和兼职两个抽象类,定义其中的抽象方法,再由肯德基继承快餐类和兼职类,实现各自的抽象方法。

这么做的好处是,如果你是一个要吃饭的人,在你还不确定到底吃什么时,只需要知道快餐类里有哪些方法,然后调用它们去制定你关于吃饭的计划;如果你是要一个找兼职的人,在你还不确定找什么兼职时,只需要知道兼职类里有哪些方法,然后调用它们去制定你自己与兼职有关的计划。等你最后确定是要去肯德基吃饭或者兼职,都不影响你原来的计划。

image.png

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

接口隔离原则和单一职责原则的区别有两个,第一,单一职责原则指的是类、接口和方法的职责是单一的,强调的是职责,也就是说在一个接口里,只要职责是单一的,有10个方法也是可以的。

第二,接口隔离原则指的是在接口中的方法尽量越来越少,接口隔离原则的前提必须先符合单一职责,在单一职责的前提下,接口尽量是单一接口。

2.5 依赖倒置

高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该取决于抽象。

依赖倒置的核心是面向接口编程,或者说面向抽象编程。类中的成员、函数中的参数都应该是抽象类或者接口,而不是具体的实现类,这样就可以避免直接的依赖关系。

  • 怎么理解依赖

所谓依赖,就是“没你不行”。高层模块和低层模块如果都是具体实现类,那么由于高层模块依赖低层模块,在低层模块还没完工之前,高层模块只能干瞪眼——“我是谁?我在哪?我该干嘛”,高层模块得明确知道低层模块有哪些方法可以调用之后,才能完成自己的逻辑。

  • 怎么理解倒置

首先“倒置”肯定不是指底层模块依赖高层模块!

通过面向接口编程,高层模块就不需要知道低层模块的实现细节了,这时高层模块就掌握了主动权,他可以先决定需要低层模块为他提供什么功能,然后低层模块的实现类去完成具体功能。这样依赖关系就“倒置”过来了。

👉从“低层有什么,高层用什么”变成“高层需要什么,低层实现什么

举个例子。肯德基里现在的可乐是百事可乐,但是指不定哪一天肯德基想把百事可乐换成可口可乐,或者换成非常可乐。那么肯德基就应该抽象出一个可乐供应商接口,然后指定需要可乐供应商提供的哪些服务,例如供应无糖可乐、供应柠檬味可乐、供应桂花味可乐,然后各个可乐品牌去制作自己的可乐配方。

image.png

另外,这些可乐公司也不单单只供应肯德基,所有的餐饮店都可以找他们供应可乐。“高层模块不应该依赖低层模块,两者都应该依赖于抽象”,肯德基也需要抽象成餐饮店,这样才算完美遵循了依赖倒置原则

image.png

依赖倒置原则降低了类或模块的耦合性,降低了维护成本,降低了由于类或实现发生变化带来的修改成本,提高了代码稳定性。

三、 总结

本文通过讲解“今晚吃什么”,循序渐进地介绍了SOLID原则,旨在帮助读者理解其内涵,而不是停留在背诵各自的定义上。

这五种模式相互之间其实都是紧密联系的,一者的出现必定同时伴随其他几个的应用,它们拥有一个共同的内核——“面向抽象编程

只有通过足够的抽象,才能实现良好的可维护性、可扩展性、可复用性。在接下来学习23种设计模式模式的过程中,很重要的是要理解每种模式是如何针对一个具体问题场景进行抽象的。

参考资料

[1] Android架构演进 · 设计模式· 为什么建议你一定要学透设计模式?

[2] 「设计模式」理解面向对象的七大设计原则,消除代码中的坏味道

[3] SOLID设计原则:里氏替换原则

[4] 设计模式六大原则:里氏替换原则

[5] 依赖倒置原则:高层代码和底层代码,到底谁该依赖谁?