写点什么

架构师训练营第二周总结

用户头像
lakers
关注
发布于: 2020 年 11 月 02 日

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 指出应该如何设计一个接口 —— 从客户的需要出发,强调不要让客户看到他们不需要 的方法。

 

接口隔离的核心目的是:让不需要它们的应用程序、不强迫它们的应用程序,关注它不需要的方法;

主要实现办法:多重继承,一个实现类实现多个接口,不同的应用场景调用不同的接口,访问实现类,从而实现接口隔离,不同的应用程序不会看到它不需要的方法;


发布于: 2020 年 11 月 02 日阅读数: 43
用户头像

lakers

关注

还未添加个人签名 2018.04.25 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营第二周总结