闲话设计模式认知与 SOLID 原则

共 7638字,需浏览 16分钟

 ·

2021-06-11 10:30




👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇








转自:掘金  ZenonHuang


https://juejin.cn/post/6929290462117232654




设计模式是所有开发者都要学习的。


本文将基于实际开发经验谈谈对设计模式认知,再说一说实践当中的原则和个人的理解。


对设计模式的认知


在我有限经验的认知里,在复杂业务开发场景,能合理运用设计模式更是尤为重要。



对于设计模式,每个人都可以说上一二,然而落地到实际开发当中,好的代码设计少之又少。



是什么造成了这种现象?


我回顾过去发现有 4 点原因:





  1. 设计思考是一项门槛。因为思维局限或惯性,大部分人(包括我自己)如没有专门的训练意识,是会直接动手的,用最简单的方式,想到哪写到哪。



  2. 设计实施是一项成本。稍有经验的工程师,是能预见产品某业务复杂度增长的,可按时交付的念头,会让人倾向更容易的选择。



  3. 没有被坏设计坑害过 : (



  4. 没有体验过好设计带来的舒适 :)





现在我来看这 4 点原因,也是作为一个工程师成长的历程。


思维局限


对于 1 阶段的工程师来说,由于经验缺乏,连系统 API 都没有熟悉,自身没有经历过复杂项目或看过开源项目,所以更谈不上什么设计模式,就是一张白纸。这阶段的开发者,首先就是要 启发认知,先知道有什么。


我当时以实习生转成 iOS 开发,靠一本基础知识书就开始上路,没有任何 iOS 的前人指导。所幸那时直播火起来,当时 iOS 大V免费直播,逐渐知道MVC ,基础的工程结构划分,业务逻辑能用 manager 来统一管理等等。感谢互联网,感谢前人们的分享。


当然我相信,想向上的人总能找到自己的出路。优秀的开源项目或者书籍都可以开阔认知,只是都不如一个前辈手把手来的好。


怠于实践


对于 2 阶段来说,起码工作几年,属于知道设计模式,也拥有处理某些复杂业务经验。掣肘的地方,常常在于工期的矛盾,让人做出 赶工 的决定。这时我们一定要有概念:坚持做正确的事


某种程度来说,把事情做对,比做快更重要。


就我自身而言,随着项目复杂,在几次上级亲自下场 coding 时问我,为什么当时不做 xxx 设计呢?我说我想到了,但是时间紧没有写。


随后开始进行改造,往往半天就完成了。更费时间 也许是心理上制造的拦路虎,有时对 2-3天的业务工期,只多花费 1-2 小时罢了,并不会有实质的阻碍。


自食恶果


对于 3 阶段来说,没有被坏设计坑害过在我看来是一个瓶颈


如果产品没有好的发展(持续的盈利/用户数增长),你是等不到产品业务复杂的那一天,大多数产品甚至停留在 2.0 版本以下就消亡了。这一点对于服务端来说都是一样。


宝剑锋从磨砺出,梅花香自苦寒来。作为 iOS 工程师,要在产品狗子复杂的业务中成长起来,实践是检验真理的唯一标准。


等你被坏设计坑害,然后想改又不敢的时候,你才会大呼 – 坑害我的竟是我自己!





img


你不用问我为什么会知道 : (


当然啦,坑害你的也可能是别人..总之,你需要擦屁股就对了。


这时你才会在小心翼翼的重构当中,痛定思痛,认真思考和设计。


毕竟,人总是对痛苦记忆感受深刻。


走向正轨


对于 4 阶段,如果能走出重构灾难,工程师一般都将吸取被毒打的教训,开始对接手的业务运用设计模式,最后再享受到扩展功能/修改功能/移除功能的好处。


严谨的设计,实施起来必定是稍复杂的,没有接触复杂业务以前,是无法深切体会所谓好设计带来的收益。


业务比基础技术挑战更大的地方,就在于因地制宜,判断什么地方要设计/不设计;而设计的话该怎么做。既要强调代码上做设计,也要避免过度设计


总之,一个能做好业务的工程师,必定是珍贵的。


对于 4 阶段来说,最重要是反复的:知道+做到->得到。验证好流程带来的收益,形成正向反馈,大呼:舒服了!





img


以上是我个人的小小经验之谈,难免局限,希望大家有看法的话,能多多评论交流。


如何落地设计模式


具体来说,一个好的业务设计在我眼里有这么几个特点:



  • 易于扩展。


  • 易于修改。


  • 易于删除。



其实就是 增/删/改 的成本最小。要改动的地方越少,它的危险系数也相对少。


相信大多数工程师都经历过 ‘牵一发动全身’ 的问题,也出现过遗漏了地方最后导致上线有 Bug 。


今年本想结合自己项目去好好改造,这里不做借口,只能说,想的有多美,等到自己干的时候就有多难 : )


一个建议就是平时尽量按照 SOLID 原则去思考。


在这里并不想死记硬背,来罗列 23 种设计模式是什么,具体的设计模式可以说都是实现 SOLID 原则的手段。如果有方式可以达成 SOLID 原则 ,我认为即使不属于 23 种设计模式里也无妨。这方面网上文章或者书籍不说是汗牛充栋,也是非常之多了,可以自行了解。



一个建议是不要只看 iOS 相关,对于设计模式文章,很多搞 Java 写出来的不错,思想是通用的。



SOLID



  • S : 单一职责原则 (Single Responsibility Principle)


  • O : 开放封闭原则 (Open-closed Principle)


  • L : 里氏替换原则 (Liskov Substitution Principle)


  • I : 接口隔离原则 (Interface Segregation Principle)


  • D : 依赖倒置原则 (Dependence Inversion Principle)






学习建议

在逐一解释它们每个原则之前,我想说一下学习和理解的建议。有的弯路我曾经走过,如果能看到自己的文章,或许将来能走的快那么一点点…


其实上面说了 5 项原则,单纯看一遍,我们将很快就忘记了…


你会记得一周前你随意刷过一个搞笑视频的细节吗?


有时面试,面试官问到一个问题 X 时,明明很久前学过背过,但是临场记不全了..


核心原因还是在于,我们没有真正的得到它


最佳学习方式就是实践


对于部分问题,很遗憾,几乎没有机会实践它,那降低优先级,先用是什么/为什么/怎么做的方式大概了解即可。


最高优先级,最合适的方式,就是平时工作项目中的实践机会。


既符合实际场景,也能帮助自己和公司解决问题,还能提升自我的能力。


所以,工作中遇到问题时,务必好好抓住实践场景,去尝试解决/验证/总结。


同样的问题,大概率会再出现第二次,第三次。


这样反复下来,我们就能获得很好的反馈,以及解决经验 。


对于这 5 项原则,要做的就是刻意练习。就像学习客户端编程时,反复去写 UI ,写数据模型。



请谨记 – 没有人仅仅靠书本知识,就能成为一名真正的司机



单一职责


从字面上,很容易理解,就是一个对象只负责单一功能。


一个比较官方的指导说法是:



”A class should have only one reason to change.” – 仅有一个原因会使类变更



通俗具体的说,就是一个类的修改,不受多个功能修改的影响。


然而我相信它正如“蛋炒饭”一样,最简单也最困难,和上面提到的做业务最大的挑战,怎么衡量它的度?


业务划分不像性能指标一样,存在一条明显的分割线,去分割出我们的功能。


坦诚的说,包括我自己,9 成以上的工程师经常在违反着单一职责,而未加审视。


那什么时候要对一个类的功能进行代码拆分,或者什么时候需要合并几个功能到一个类中去?


实践的思路就是随着业务每次迭代,不断的拆分和封装,持续重构我们自己的代码,正如烧烤时做的,不断调整 “火候”


我作为客户端工程师,感受比较强烈的一点,就是在 MVC 的设计架构下,View 的变化总是最多的,Controller 逻辑总是堆积最多的,反而是 Model 的变化相对少一点,通常也只会增加新 Model。


开闭原则


开闭原则的经典定义:



实体应该对扩展是开放的,对修改是封闭的。



总的来说,就是当增加一个新功能的时候,尽量在不修改既有代码的情况下,来进行扩展。使用增加代码的方式,如新的类或新的方法来实现新功能。


实现开闭原则,可以说是设计的终极目标,其它的原则多少都是在为开闭原则服务。


因此在写代码的时候,我们可以对开闭原则做重点思考。


在 iOS 来说,经典的就是 OC 中对分类的使用,例如我们常常为 UIView / UIButton / UIImage 等增加分类,来扩展它们的功能。


但是不要因为我举了这么一个例子,就都使用它来编码…


实现开闭原则的关键在于抽象


最终我们仍旧要回到学习面向对象的起点之一 –- 抽象。


以前我们所做的抽象,更多其实只是建模,对需要做的东西,抽象成对象模型。


而要实现开闭原则,更重是牢记一点 :



针对接口编程,而不是针对实现编程



具体对于 iOS 来说,需要学会使用 协议 (protocol) 来抽象行为的接口,实现协议的类来做真正的操作和执行。


以我工作项目当中使用多个第三方游戏 SDK 引擎来做说明。


先说只针对实例的编程的情况会是什么样。


只有 1 个SDK,我们的代码是:


[EngineA do];

增加到 2 个SDK,我们的代码是:


if(A){
  [EngineA do];
}else{
  [EngineB do];
}

等到 3 个SDK,我们的代码是:


if(A){
  [EngineA do];
  return;
}
if(B){
  [EngineB do];
  return;
}
if(C){
  [EngineC do];
}

实际代码当中,随着增加的 SDK 引擎,每次需要修改的地方有 N 个,工作量 = N*引擎数量,完全靠手动增加非常容易出错。


如果我们使用协议的方式呢?


那么我们的代码是:


id<EngineProtocol> engine = [Engine createWithType:type];
[engine do];

如果需增加SDK,只需要增加一个类型的返回逻辑:


- (id<EngineProtocol>)createWithType:(EngineType)type
{
     if(A){
        return EngineA<EngineProtocol>;
     }
     if(B){
        return EngineB<EngineProtocol>;
     }
     if(C){
        return EngineB<EngineProtocol>;
     }
     ...
}

然后对于新增的 SDK 的 Engine ,像原来一样实现,只是遵守 EngineProtocol 的协议方法即可。


实现新 Engine 的额外工作量,可以说是仅有 2 行 。


面向接口编程对比面向实例编程的方式,越是复杂的业务,它能节省的工作量就越多。


同时遵守协议一项项实现接口方法,也减少我们犯错的几率。


这就是面向接口编程的强大之处


里氏替换原则


一个父类实例在它出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误


看一下经典的正方形例子:



正方形(子类)是矩形(父类)的一种。


正方形对于面积定义的公式是:宽x宽;矩形对于面积定义的公式是:长x宽。


如果使用正方形类替换矩形,输入 长和宽(如 3 和 5),使用正方形公式得到的面积,显然跟原来矩形的结果不一致,是错误的。



联想到上面 SDK 引擎的例子,里氏替换原则某种程度上,就是在帮助我们完成开闭原则。


接口继承让我们可以用一个对象替换另一个对象,更重要是不影响业务的正确运行。


里氏替换可以说是开闭原则的解决方式之一。


接口隔离原则


接口隔离原则说的是使用者不应该被迫依赖于它不使用的方法


具体来说,就是顾客来到水果店买西瓜,但是店主只有捆绑销售到组合:苹果+梨子+西瓜。


那么怎么解决这一问题呢?


当然是从顾客需求出发,只提供顾客需要的 西瓜,或者尽量减少捆绑,提供 梨子+西瓜


回到具体的设计层面来说,如果接口功能过多,非常容易违反单一职责


而对于我们的编码来说,就是从调用方的需求出发,需要调用什么,我就提供什么。


尽可能提供瘦接口或者分割多个接口,而不是堆积接口方法。


同样从上面的多 SDK 引擎为例子,其实有些场景,不同的 SDK 引擎也并非都需要同样的方法。


例如:手游时不需要游戏按键功能,而端游需要游戏按键。那么我们可以这么做:


//通用场景,BaseEngineProtocl,最小化基本功能
id<BaseEngineProtocl> engineMobile = [Engine createWithType:type];

在另外的场景下:


//需要游戏按键场景,KeyboardEngineProtocl 提供额外的键盘功能。KeyboardEngineProtocl 继承于 BaseEngineProtocl
id<KeyboardEngineProtocl> engineKeyboard = [Engine createWithType:type];
[engineKeyboard tapA];

通过完成接口隔离原则,我们的代码不依赖多余的接口方法,将变得更容易维护和清晰。


接口隔离原则最重要的就是,从使用者角度出发定义接口方法。


依赖倒置原则


在开闭原则的阐述里,有说到,面向接口编程,而不是针对实例编程。


依赖导致原则就是针对这一说法的指导:



抽象不应该依赖于细节,两者都应依赖各自的抽象。



还是使用多 SDK 引擎为例子.原来的是:





img


改造后是:





img


在高层业务的游戏功能模块,代码依赖于 EngineProtocol 协议作为接口,来完成我们的业务功能。


作为 EngineProtocol 的实现细节,EngineA/EngineB 也都依赖 EngineProtocol 去做实现。


如果是 EngineA 依赖游戏功能,或者游戏功能依赖于 EngineA,那么我们就违反了依赖倒置。


由此看出依赖倒置原则也是帮助我们实现开闭原则的方法之一。


总结


说了些对设计模式的感想,以及如何理解 SOLID 原则,属于人人都很容易说,实践起来却需要见功力的东西。


尤其希望刚入行(1-2年)的工程师,不要为了做而做,在手里拿着锤子,去找钉子。而是先存有个理念,遇到合适问题,再拿出我们的武器,在做中学


对于有一些经验(3-5年)的工程师,就要开始着重锻炼我们的设计能力了,大胆的设计,小心的实践。


至于更久年限的程序员们…我还没到那个时候,无法建议😂


现在很少看到 10 年以上的前辈们发表文章,当然或许有些变成收费课程了...希望能多听到来自老江湖们的金玉良言。



-End-



最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!










点击👆卡片,关注后回复【面试题】即可获取









在看点这里好文分享给更多人↓↓










浏览 34
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报