理论一~理论七
理论一~理论七
理论一:
什么是面向对象编程和面向对象编程语言?
-
面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
-
面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
-
一般情况,面向对象编程都是通过使用面向对象编程语言来进行的。
-
不用面向对象编程语言,我们照样可以进行面向对象编程。
-
即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。
-
只要某种编程语言支持类或对象的语法概念,并且以此作为组织代码的基本单元,那就可以被粗略地认为它就是面向对象编程语言了。
什么是面向对象分析和面向对象设计?
- OOA、OOD、OOP 三个连在一起就是面向对象分析、设计、编程(实现),正好是面向对象软件开发要经历的三个阶段。
- 简单类比软件开发中的需求分析、系统设计即可。分析做什么,设计怎么做。
什么是 UML?我们是否需要 UML?
- UML(Unified Model Language),统一建模语言。
- UML图十分复杂,有很高的学习成本,包括画图的人和看图的人。
- 可以简化地画UML图,主要能够达意,方便沟通就够了。
理论二:
封装
- 封装也叫作信息隐藏或者数据访问保护。
- 类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
- 对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支
持。这个语法机制就是访问权限控制。 - 类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。
抽象
- 在面向对象编程中,我们常借助编程语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。
继承
- 继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。
- 继承最大的一个好处就是代码复用。
多态
- 多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
- 对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外,我们还有其他两种比较常见的的实现方式,一个是利用接口类语法,另一.个是利用 duck-typing 语法。
理论三:
什么是面向过程编程与面向过程编程语言?
- 面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
- 面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
- 面向过程风格的代码被组织成了一组方法集合及其数据结构,方法和数据结构的定义是分开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。
面向对象编程相比面向过程编程有哪些优势?
OOP 更加能够应对大规模复杂程序的开发
- 在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是采用曲线救国的策略,先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。
- 当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。
- 这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。
- 利用面向过程的编程语言照样可以写出面向对象风格的代码,只不过可能会比用面向对象编程语言来写面向对象风格的代码,付出的代价更高一些。
OOP 风格的代码更易复用、易扩展、易维护
- 封装特性是面向对象编程相比于面向过程编程的一个最基本的区别,因为它基于的是面向对象编程中最基本的类的概念。
- 基于接口的抽象,可以让我们在不改变原有实现的情况下,轻松替换新的实现逻辑,提高了代码的可扩展性。这是面向过程编程所不具备的。
- 继承和多态特性是面向对象编程相比于面向过程编程所特有的两个特性。
- 子类可以重用父类中的代码,避免了代码重复写多遍,提高了代码的复用性。
- 们在需要修改一个功能实现的时候,可以通过实现一个新的子类的方式,在子类中重写原来的功能逻辑,用子类替换父类。在实际的代码运行过程中,调用子类新的功能逻辑,而不是在原有代码上做修改。这就遵从了“对修改关闭、对扩展开放”的设计原则,提高代码的扩展性。
OOP 语言更加人性化、更加高级、更加智能
- 二进制指令、汇编语言、面向过程编程语言是一种计算机思维方式。
- 面向对象是一种人类的思维方式。
- 越高级的编程语言离机器越“远”,离我们人类越“近”,越“智能“。
理论四:
哪些代码设计看似是面向对象,实际是面向过程的?
滥用 getter、setter 方法
- 所有的属性都定义getter、setter 方法。
- 给所有的属性都定义public的getter、setter 方法,这就跟将这两个属性定义为 public 公有属性,没有什么两样,违反面向对象封装的访问权限控制。
- 如果用getter方法拿到了一个List集合容器,外部调用者在拿到这个容器之后,是可以操作容器内部数据。
滥用全局变量和全局方法
- 在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。
- 全局变量和全局方法中,Constants 类和 Utils类最常用到。
- 将 Constants 类拆解为功能更加单一的多个类,或者不单独地设计 Constants 常量类,而是哪个类用到了某个常量,我们就把这个常量定义到这个类中。
- 设计 Utils 类的时候,最好也能细化一下,针对不同的功能,设计不同的 Utils 类。
定义数据和方法分离的类
- 传统的 MVC 结构做前后端分离之后,分为 Controller 层、Service 层、Repository 层。
- 在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。
- VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller类、Service 类、Repository 类中。
- 这种开发模式叫作基于贫血模型的开发模式.。
在面向对象编程中,为什么容易写出面向过程风格的代码?
- 符合人的这种流程化思维方式。
- 面向对象编程要比面向过程编程难一些,面向对象编程需要设计、思考、封装和考虑类之间的交互。
面向过程编程及面向过程编程语言就真的无用武之地了吗?
- 脚本式的面向过程的编程风格就更适合一些。
- 类中每个方法的实现逻辑,就是面向过程风格的代码。
- 面向对象和面向过程两种编程风格,也并不是非黑即白、完全对立的。最终的目的还是写出易维护、易读、易复用、易扩展的高质量代码。
理论五:
接口vs抽象类的区别?
JAVA抽象类:
- 抽象类不允许被实例化,只能被继承。
- 抽象类可以包含属性和方法。
- 子类继承抽象类,必须实现抽象类中的所有抽象方法。
JAVA接口:
- 接口不能包含属性。
- 接口只能声明方法,方法不能包含代码实现。
- 类实现接口的时候,必须实现接口中声明的所有方法。
抽象类和接口能解决什么编程问题?
- 作者举例一个用抽象类比用普通父类更优雅的例子,类中有好多方法,其中有一个方法每个子类的实现方式都不同,这个时候定义一个抽象类中的抽象方法,比定义一个空的方法优雅。
- 接口是对行为的一种抽象,相当于一组协议或者契约,与类比较而言,不需要了解具体实现,更侧重于解耦。
- 实现一个没有属性,全是抽象方法的抽象类来模拟实现一个接口。
- 实现一个父类中的方法都是直接抛异常,这样子类就不得不自己写具体实现来模拟一个接口。
- 抽象类、接口等都是一种编程思想,而不是拘泥于概念。
/***
* 抽象类模拟接口
*/
public abstract class AbstractInterface {
abstract void funcA();
abstract void funcB();
}
/***
* 普通类模拟接口
*/
public class MockInterface {
protected MockInterface() {}
public void funcA() {
throw new UnsupportedOperationException();
}
}
/***
* 接口实现
*/
public class MockInterfaceImp extends MockInterface{
@Override
public void funcA(){};
public static void main(String[] args) {
MockInterface mi = new MockInterfaceImp();
mi.funcA();
}
}
/***
* 接口实现
*/
public class MockInterfaceImp extends AbstractInterface{
@Override
public void funcA(){}
@Override
void funcB() {
}
public static void main(String[] args) {
MockInterfaceImp mi = new MockInterfaceImp();
mi.funcA();
}
}
理论六:
解读“基于接口而非实现编程”
如何解读原则中的“接口”二字?
- 不要局限在编程语言的“接口”语法中。
- 接口”就是一组“协议”或者“约定”。
- 上游系统面向接口而非实现编程,不依赖不稳定的实现细节,降低耦合性,提高扩展性。
- 可以解读为“基于抽象而非实现编程”。
- 好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。
如何将这条原则应用到实战中?
- 以一个上传图片的代码举例。
- 封装了上传图片到阿里云的的类,以后图片不再存储到阿里云而是私有云等,就要修改代码。
- 还有类中的函数命名暴露了实现细节。
- 上传的实现过程不一致的,以后更改存储,这里面的实现细节都要修改。
基于接口而非实现编程”的3点原则
- 函数的命名不能暴露任何实现细节。
- 封装具体的实现细节。
- 为实现类定义抽象的接口。
如何来做权衡,怎样恰到好处地应用这条原则
- 过度使用这条原则,非得给每个类都定义接口,接口满天飞,也会导致不必要的开发负担。
- 如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。
- 如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。
理论七:
为什么不推荐使用继承?
- 以鸟类举例,里面有一个fly的方法,各种类型的鸟都继承这个鸟类。
- 那么当有不会飞的鸟比如鸵鸟来继承这个鸟类的时候,就要重写fly方法,方案并不优雅。多个不会飞的鸟来继承鸟类的时候,每个都要重写,问题就更突出了。
- 换个思路的话,在鸟类下面再设计两个子类,会飞鸟类和不会飞鸟类,让会飞的鸟和不会飞的鸟分别继承,三级层级。看似还算融洽。
- 按照以上这个思路,鸟还分会叫的不会叫的,会下蛋的不会下蛋的,这样一组合,简直就是灾难乱套。
- 总结,继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。
组合相比继承有哪些优势?
- 改造上面鸟类的例子,首先封装会飞、会叫、会下蛋这样的三个接口。
- 然后再给三个接口定义三个实现类,解决重复实现逻辑的问题,达到代码复用。
- 鸵鸟会叫、会下蛋:
- 鸵鸟类先实现先会叫、会下蛋两个接口。
- 在类中有会叫、会下蛋两个属性,这两个属性分别是实例化会叫、会下蛋两个实现类,这是组合的运用。
- 在类中有会叫、会下蛋的两个方法,这两个方法分别是实现类中的方法来实现的,这是委托的运用。
- 总结,可以利用组合、接口、委托三个技术手段,来解决继承存在的问题。
如何判断该用组合还是继承?
- 如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。
- 系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。
- A类和B类都用到了URL拼接和分割的功能,不能为了代码复用,硬抽象出来一个父类,可以用组合的方式实现。
- 当想修改一个外部类的方法时,我们没有权限修改这个外部类,只能继承这个外部类,以重写的方式修改其中的方法。
- 继承还是有使用场景和使用空间,只要不多度使用,继承还是可用的。
/**
* 组合实现
*/
interface Flyable{
void fly();
}
interface Tweetable{
void tweet();
}
interface EggLayable{
void layEgg();
}
class FlyAbility implements Flyable {
public void fly(){};
}
class TweetAbility implements Tweetable {
public void tweet(){};
}
class EggLayAbility implements EggLayable {
public void layEgg(){};
}
class Ostrich implements Tweetable, EggLayable {
private TweetAbility tweetAbility = new TweetAbility();
private EggLayable eggLayable = new EggLayAbility();
@Override
public void tweet() {
tweetAbility.tweet();
}
@Override
public void layEgg() {
eggLayable.layEgg();
}
}