架构师训练营第二周总结
1.从编程历史看面向对象编程的本质与未来
如何学习一项技术?
这项技术是如何发展现在的这样的?发展历程是什么?
这项技术要解决的问题是什么?为什么过往别的技术没有解决的更好(横向对比)?
要看到技术背后真正的本质,背后的原理,抓住技术背后的规律,不能只看到皮毛,只知道如何用。
面向对象编程:
(1)面向对象的本质是什么?
万物皆为对象,我们要解决的问题域是一个对象,这个对象内部调用各种各样的对象来实现、解决问题;
程序是对象的集合,它们通过发送消息来告诉彼此要做的事情(也就是对象之间的调用)
每个对象都有自己的由其他对象构成的存储(成员变量)
每个对象都拥有其类型
某一特定类型的所有对象能接收同样的消息
也就是说:对象具有状态、行为、标识
状态:表示每个对象可以有自己的数据(成员变量的具体值)
行为:表示对象可以产生行为或者说对象可以做出行为(调用、告诉彼此要做的事情)
标识:表示对象之间都有唯一标识(唯一地址)
(2)面向对象的特点是什么?
面向对象编程的三要素(三个特征):
封装性:隐藏实现细节(通过访问控制实现)、定义接口(和外部/其他对象交互);
继承性:HAS-A 关系、IS-A 关系(组合);
多态性:后期绑定(具体实现运行时决定、可以有很多的实现)、向上转形;
面向对象编程不是使用面向对象的编程语言进行编程,而是利用多态特性进行编程。
(3)面向对象要达到的目标是什么?
面向对象设计的目的:高内聚、低耦合,从而使系统:
易扩展:易于添加新功能
更强壮:不容易被粗心的程序员破坏
可移植:能够在多样的环境下运行
更简单:容易理解、容易维护
那如何达到这个目标呢?历史长河里的先人总结了各种指导原则(比如设计模式、设计原则),原则独立于编程语言
设计模式:它是前人总结的用于解决某一类特定问题的通用的解决方案,设计模式贯彻了设计原则;
框架:框架是用来实现某一类应用的结构性的程序(比如 Spring、tomcat),是对某一类架构方案可复用的设计与实现,用于简化开发者的工作,实现了多种设计模式,使开发者不需要花费太大力气就能设计出结构良好的程序来;
框架和工具的区别:
框架调用应用程序的代码;
应用程序调用的代码工具;
架构师用框架保证架构的落地;
架构师用工具提高开发效率;
2.设计臭味:糟糕的代码有哪些特点?
软件设计的最终目的,是使软件达到“强内聚、松耦合”,从而使软件:
• 易扩展 - 易于增加新的功能
• 更强壮 - 不容易被粗心的程序员破坏
• 可移植 - 能够在多样的环境下运行
• 更简单 - 容易理解、容易维护
与之相反,一个“不好的”软件,会发出如下“臭味”:
• 僵硬 - 不易改变。
• 脆弱 - 只想改 A,结果 B 被意外破坏。
• 不可移植 - 不能适应环境的变化。
• 导致误用的陷阱 - 做错误的事比做正确的事更容易,引诱程序员破坏原有的设计。
• 晦涩 - 代码难以理解。
• 过度设计、copy-paste 代码。
“臭味”代码的特征:程序设计的时候应该避免这些情况
(1)僵化性(Rigidity):
很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分 的改动。
如果单一的改动会导致依赖关系的模块中的连锁改动,那么设计就是僵化的,必须要改动的模块越多,设计就越僵化。
(2)脆弱性(Fragility):
对系统的改动会导致系统中和改动的地方无关的许多地方出现问题。
出现新问题的地方与改动的地方没有概念上的关联。要修正这些问题又会引出更多的问题,从而使开发团队就像一只不停追逐自己尾巴的狗一样。
(3)牢固性(Immobility)/不可重用:
很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
设计中包含了对其他系统有用的部分,而把这些部分从系统中分离出来所需的努力和风险是
巨大的。
(4) 粘滞性(Viscosity):
做正确的事情比做错误的事情要困难。
面临一个改动的时候,开发人员常常会发现会有多种改动的方法。有的方法会保持系统原来的设计,而另外一些则会破坏设计,当那些可以保持系统设计的方法比那些破坏设计的方法 跟难应用是,就表明设计具有高的粘滞性,作错误的事情就很容易。
(5)不必要的复杂性(Needless Complexity)/过度设计的一种:
设计中包含有不具任何直接好处的基础结构
如果设计中包含有当前没有用的组成部分,他就含有不必要的复杂性。当开发人员预测需求
的变化,并在软件中放置了处理那些潜在变化的代码时,常常会出现这种情况。
(6)不必要的重复(Needless Repetition):
设计中包含有重复的结构,而该重复的结构本 可以使用单一的抽象进行统一。
当 copy,cut,paste 编程的时候,这种情况就会发生。
(7) 晦涩性(Opacity)/可读性低:
很难阅读、理解。没有很好的表现出意图。
代码可以用清晰、富有表现力的方式编写,也可以用晦涩、费解的方式编写。一般说来,随
着时间的推移,代码会变得越来越晦涩。
你如何尽量避免这些情况呢(臭味代码)?前人总结了一些设计原则-OOD(面向对象设计)设计原则
3.OOD 设计原则
(1)开闭原则:OCP - Open/Closed Principle
• 对于扩展是开放的(Open for extension):可以很容易添加新加工
• 对于更改是封闭的(Closed for modification):添加新功能不需要修改原有的代码/核心代码、不破坏原有功能
• 简言之:不需要修改已有的软件实体(类、模块、函数等),就应该能实现功能的扩展。
传统的扩展模块的方式就是修改模块的源代码。
如何实现不修改而扩展呢? 关键是抽象!
心得:定义抽象的接口,依赖抽象,通过抽象的接口进行编程,那么当变更的时候,变更的是抽象接口的实现,而接口本身不变、接口的调用不变,即核心代码不变,从而实现开闭原则;
(2)依赖倒置原则:DIP - Dependency Inversion Principle
• 高层模块不能依赖低层模块,而是大家都依赖于抽象;
• 抽象不能依赖实现,而是实现依赖抽象。
DIP 倒置了什么?
• 模块或包的依赖关系:高层不再依赖底层-底层不再是实现业务然后提供抽象接口给高层用、底层也不依赖高层,而是高底层大家都依赖抽象-接口,这个抽象属于高层模块;定义好接口大家各自完成各自的职责,不必互相等待;高层也更能复用;
• 开发顺序和职责:定义好接口大家各自完成各自的职责,不必互相等待
软件的层次化
• 高层决定低层
• 高层被重用
好莱坞规则:
• Don't call me, I'll call you.
框架的核心-DIP
倒转的层次依赖关系:
(1)框架不依赖应用代码,而是以来抽象-接口,应用实现这些接口
(2)框架调用应用的代码,应用的代代码不能调用框架
(3)框架决定、定义好整体流程,应用实现具体业务逻辑
架构需要通过框架来落地!
(3)里氏替换原则:Liskov 替换原则(LSP)
在 Java/C++ 这样的静态类型语言中,实现 OCP 的关键在于抽象,而抽象的威力在于多
态和继承。
• 一个正确的继承要符合什么要求呢?
• 答案:Liskov 替换原则
1988 年,Barbara Liskov 描述这个原则:
• 若对每个类型 T1 的对象 o1,都存在一个类型 T2 的对象 o2,使得在所有针对 T2 编写的程
序 P 中,用 o1 替换 o2 后,程序 P 的行为功能不变,则 T1 是 T2 的子类型。
• 简言之:子类型(subtype)必须能够替换掉它们的基类型(base type)。
(1)里氏替换原则是用来解决继承(IS A)问题的,它是用来衡量继承(IS A)是否合理的一个重要原则;
即如何确定什么时候可以继承、什么时候可以成为父类的子类,判断的原则是:如果子类替换父类后,程序的行为功能不变(也就是要看具体的使用场景),则可以继承;也就是子类型必须能够替换父类型。
比如:场景为人骑马,依赖马完成骑马动作,马是父类,那小马可以成为马的子类吗?虽然从静态的角度分析小马 IS A 马是成立的,但是小马不能骑,所以不能替换马,所以小马在人骑马这个场景不能继承马;但是如果是人牵马的场景,小马就能成为马的子类;
(2)里氏替换原则也是用来衡量、分析继承的使用是否正确、子类和父类的的应用是否正确、接口和抽象的应用是否正确;
比如如下的代码就不符合里氏替换原则:先建一个不在 if 里的子类,程序执行就有问题;
void drawShape(Shape shape) {
if (shape instanceof Circle) {
drawCircle((Circle) shape);
} else if (shape instanceof Square) {
drawSquare((Square) shape);
} else {
......
}
}
那怎么解决呢?很简单,Shape 里定义一个 draw()方法,子类都去实现 draw;一行代码搞定,解决多个 if else
void drawShape(Shape shape) {
shape.draw();
}
符合设计原则的代码一般都是更简单的代码、代码量一般也会更少;工程师的工作好坏不应该用代码量来衡量,反而好的工程师实现同一业务的代码往往是更少的;
里氏替换原则一定要放到场景中去看!在场景中能够使用父类的地方能够使用子类替换吗?能够替换就是合理的继承,不能就是不合理的继承。
继承是不是合理,不能用静态的 IS A 关系判断,而是要到程序运行动态的上下文中判断,只有在程序运行的动态上下下文中子类可以替换父类,继承才是合理的,才是符合原则的。
从对象的属性来证明这一论点,对于同一个类,所创建的不同对象,它们的:
• 标识 - 是不同的。
• 状态 - 是不同的。
• 行为 - 是相同的。
• 因此,设计和界定一个类,应该以其行为作为区分。
(4)里氏替换原则:单一职责原则(SRP)
SRP - Single Responsibility Principle
• 又被称为“内聚性原则(Cohesion)”,意为:一个模块的组成元素之间的功能相关性。
• 将它与引起一个模块改变的作用力相联,就形成了如下描述: 一个类,只能有一个引起它的变化的原因。
什么是职责?
• 单纯谈论职责,每个人都会得出不同的结论
• 因此我们下一个定义 :
一个职责是一个变化的原因。
有一个这样的说法:通常一个类的代码,不要超过 idea 的一屏,这样的代码通常是符合单一职责原则的,更易于维护;
(5)接口分离原则(ISP) :ISP - Interface Segregation Principle
• 不应该强迫客户程序依赖它们不需要的方法。
ISP 和 SRP 的关系
• ISP 和 SRP 是相关的,都和“内聚性”有关。
• SRP 指出应该如何设计一个类 —— 只能有一种原因才能促使类发生改变。
• ISP 指出应该如何设计一个接口 —— 从客户的需要出发,强调不要让客户看到他们不需要 的方法。
接口隔离的核心目的是:让不需要它们的应用程序、不强迫它们的应用程序,关注它不需要的方法;
主要实现办法:多重继承,一个实现类实现多个接口,不同的应用场景调用不同的接口,访问实现类,从而实现接口隔离,不同的应用程序不会看到它不需要的方法;
版权声明: 本文为 InfoQ 作者【lakers】的原创文章。
原文链接:【http://xie.infoq.cn/article/3f179c7498c9bbfcf6870a904】。未经作者许可,禁止转载。
评论