谈谈我所理解的面向对象

“什么是面向对象?”这个问题往往会问到刚毕业的新手or实习生上,也是往往作为一个技术面试的开头题。在这里我们不去谈如何答(fu)好(yan)问(guo)题(qu),仅谈谈我所理解的面向对象。

从历史上看,从20世纪60年代末期到70年代,分别有几个不同领域都发展了面向对象的思想。比如数据抽象的研究、人工智能领域中的知识表现(框架模型)、仿真对象的管理方法(Simula)、并行计算模型(Actor)以及在结构化编程思想影响下而产生的面向对象方法。
框架模型是现实世界的模型化。从这个角度来看,“对象是对现实世界中具体事物的反映”这个观点并没有错。

但是不管过去怎样,现在对面向对象最好的理解是,面向对象编程是结构化编程的延伸。

结构化编程基本上实现了控制流程的结构化。但是程序流程虽然结构化了,要处理的数据却并没有被结构化。面向对象的设计方法是在结构化编程对控制流程实现了结构化后,又加上了对数据的结构化。
众多面向对象的编程思想虽不尽一致,但是无论哪种面向对象编程语言都具有以下的共通功能。
1 . 不需要知道内部的详细处理就可以进行操作(封装、数据抽象)。
2 . 根据不同的数据类型自动选择适当的方法(多态性)。

为什么“面向对象”?

最早的时候是面向过程。想象一下一堆C语言or汇编堆砌在一起的函数互相调(shang)用(hai)的场景————什么?你说你没学过C语言?那么你就想象一下一个复杂的SQL语句吧,有点像。

评论中有人提到了C语言并非完全不支持面向对象,Struct就是一个不错的选择。的确,但是C语言对面向对象的支持并不是那么的好。在绝大多数语言中都为Class(C++同时支持Struct和Class),但也有小部分语言沿用了这个经典的名字——比如Go语言。在这里特别说明是为了防止误导新手

把大象装进冰箱需要几步?

我们以“把大象装进冰箱需要几步”这个经典的脑经急转弯来举个例子吧:

面向过程

打开冰箱,装入大象,关上冰箱。这三步就是面向过程的思考方式,这种思想强调的是过程,也可以叫做动作。

open(icebox);
putIn(icebox,elephant);
close(icebox);

面向对象

冰箱打开,冰箱存储,冰箱关闭。这就是面向对象的思考方式,这种方式强调是对象,也可以说是实例。

//我们有一个冰箱
Icebox iceBox = new iceBox();
//可不能忘记大象,就叫它jake吧
Elephant jake = new Elephant();
icebox.open();
icebox.save(jake);
icebox.close();

什么是面向对象?

一种编程范式,相对于面向过程。为了方便在编程中更接近地去描述现实世界中的万物(万物皆对象),我们将对一个事物的描述称之为类,而对象则是该事物的实例

而面向对象的编程方法常见的有三种:

  1. 类模板方法
  2. 委派面向原型
  3. 组合

类模板

在类中,我们把事物的属性转变为编程中的变量,把事物的行为转变为方法。

Class Elephant{
  public String name;
  public int age;
  public double weight;
  //更多的属性......

  //在这里的方法为了方便演示都是void
  public void eat(Food food){
    //吃东西
  }
  //更多的行为.......
}

对象

//我们再次召唤了jake
Elephant jake = new Elephant();
//他随便吃了点什么
jake.eat(new Something);

面向对象所提供的特性

继承

可以使子类复用父类公开的变量、方法

//几百年后,jake和它的子孙们进化成了更强的大象
//它们被称为:飞象
Class FlyElephant extends Elephant{
  public void fly(){
    //i belive i can fly~~
  }
}

//其中有一头飞象叫jason
FlyElephant jason = new FlyElephant();
//不要问大象为什么能飞!
jason.fly();
//而且还可以像其他大象一样正常的吃东西
jason.eat(new Something);
如果把类当作模块,继承就是利用模块的方法。继承的思想好像有其现实的知识基础,但是把它看做纯粹的模块利用方法则更恰当。
因为继承只不过是抽象的功能利用方法,所以不必把对继承的理解束缚在“继承是对现实事物的分类的反映”。实际上这样的想法反而妨碍了我们对继承的理解。

封装

屏蔽一系列的细节。使外部调用时只要知道这个方法的存在

  • jason在eat的时候它或许先会分泌一点激素有助于它进食,然而我们在调用的时候并不知道发生了什么。

多态

父类的方法继承的到子类以后可以有不同的实现方式

  • jason在eat的时候它或许先会分泌一点激素有助于它进食,而jack在eat的时候或许会先刷个牙齿。

原型编程

以类为中心的传统面向对象编程,是以类为基础生成新对象。类和对象的关系可以类比成铸模和铸件的关系。

而原型模式的面向对象编程语言没有类这样一个概念。

以JavaScript为例。需要生成新的对象时,只要给对象追加属性。设置函数对象作为属性的话,就成为方法。当访问对象中不存在的属性时,JavaScript 会去搜索该对象 prototype 属性所指向的对象。

JavaScript 利用这个功能,使用“委派”而非“继承”来实现面向对象编程。

// 生成Doge。...(1)
function Doge(){
  this.sit = function () {return "I'm the king of the world"}
}
// 从Doge 生成对象dog...(2)
var doge = new Doge()
// doge 是狗,所以能 sit...(3)
alert(doge.sit())
// 生成新型myDoge...(4)
function MyDoge () {}
// 指定委派原型
MyDoge.prototype = new Dog()
// 从MyDoge 生成新对象myDoge...(5)
var myDoge = new MyDoge()
document.write(myDoge.sit()) 
  1. 函数其实做到了对象构造器的作用
  2. 从原型生成对象:

    1. 生成对象;
    2. 将委派原型的内部属性(__proto__)设置为 Dog.prototype;
    3. 调用函数 Dog,参数即为传递给 new 时的参数;
    4. 返回新生成的对象。
  3. 调用方法
  4. 定义原型函数,是空的
  5. 类似与第2步,生成新对象

和之前的Java通过类模板来实现面向对象的编程方式相比,原型对象系统支持一个更为直接的对象创建方法。例如,在 JavaScript 中,一个对象是一个简单的属性列表。每个对象包含另一个父类或原型 的一个特别引用,对象从父类或原型中继承行为。

传统对象系统和原型对象系统有本质的区别。传统对象被抽象地定义为概念组的一部分,从对象的其他类或组中继承一些特性。相反,原型对象被具体地定义为特定对象,从其他特定对象中继承行为。

因此,基于类的面向对象语言具有双重特性,至少需要 2 个基础结构:类和对象。由于这种双重性,随着基于类的软件的发展,复杂的类层次结构继承也将逐渐开发出来。通常无法预测出未来类需要使用的方法,因此,类层次结构需要不断重构,让更改变得更轻松。

基于原型的语言会减少上述双重性需求,促进对象的直接创建和操作。如果没有通过类来束缚对象,则会创建更为松散的类系统,这有助于维护模块性并减少重构需求。

然而话虽这么讲,一大串原型链还是会让人头痛不已的,特别还是在动态语言中。

组合

继承(inheritance)是实现代码重用的有力手段,但它并非永远是完成这项任务的最佳工作。使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处于同一个程序员的控制下。对于专门为了继承而设计的并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对于普通的具体类进行跨超包边界的继承则是非常危险的。本条目并不适用于接口继承(一个类实现一个接口,或者一个接口扩展另一个接口)。

方法调用不同的是,继承打破了封装性。子类信赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有变化,子类有可能会被破坏。

在Java中,我们总是推荐使用interface而不是abstract class,这样可以使代码更加的灵活。在Java8后interface也是得到了增强——可以提供默认的方法实现。

另外,Go语言也是将组合发挥到极致的语言。

面向对象的好处

  1. 接近人的思维,符合人类对现实世界的认知;
  2. 封装特性可以使开发者不必在意内部的具体实现,更方便互相协作;
  3. 继承特性可以减少代码冗余,实现代码复用;
  4. 多态特性令子类相比父类有不同的行为,这是非常接近现实的;

什么是面向对象

一种相对于面向过程的编程范式。

Java程序员应了解的10个面向对象设计原则-原文

面向对象设计原则是 OOPS(Object-Oriented Programming System,面向对象的程序设计系统)编程的核心,但大多数 Java 程序员追逐像 Singleton、Decorator、Observer 这样的设计模式,而不重视面向对象的分析和设计。甚至还有经验丰富的 Java 程序员没有听说过 OOPS 和 SOLID设计原则,他们根本不知道设计原则的好处,也不知道如何依照这些原则来进行编程。

众所周知,Java 编程最基本的原则就是要追求高内聚和低耦合的解决方案和代码模块设计。查看 Apache 和 Sun 的开放源代码能帮助你发现其他 Java 设计原则在这些代码中的实际运用。Java Development Kit 则遵循以下模式:BorderFactory 类中的工厂模式、Runtime 类中的单件模式。你可以通过 Joshua Bloch 的《Effective Java》一书来了解更多信息。我个人偏向的另一种面向对象的设计模式是 Kathy Sierra 的 《Head First设计模式》 以及 《Head First Object Oriented Analysis and Design》。

虽然实际案例是学习设计原则或模式的最佳途径,但通过本文的介绍,没有接触过这些原则或还在学习阶段的 Java 程序员也能够了解这 10 个面向对象的设计原则。其实每条原则都需要大量的篇幅才能讲清楚,但我会尽力做到言简意赅。

原则1:DRY(Don’t repeat yourself)

即不要写重复的代码,而是用“abstraction”类来抽象公有的东西。如果你需要多次用到一个硬编码值,那么可以设为公共常量;如果你要 在两个以上的地方使用一个代码块,那么可以将它设为一个独立的方法。SOLID 设计原则的优点是易于维护,但要注意,不要滥用,duplicate 不是针对代码,而是针对功能。这意味着,即使用公共代码来验证 OrderID 和 SSN,二者也不会是相同的。使用公共代码来实现两个不同的功能,其实就是近似地把这两个功能永远捆绑到了一起,如果 OrderID 改变了其格式,SSN 验证代码也会中断。因此要慎用这种组合,不要随意捆绑类似但不相关的功能。

原则2:封装变化

在软件领域中唯一不变的就是“Change”,因此封装你认为或猜测未来将发生变化的代码。OOPS 设计模式的优点在于易于测试和维护封转的代码。如果你使用 Java 编码,可以默认私有化变量和方法,并逐步增加访问权限,比如从 private 到 protected 和 not public。有几种 Java 设计模式也使用封装,比如 Factory 设计模式是封装“对象创建”,其灵活性使得之后引进新代码不会对现有的代码造成影响。

原则3:开闭原则

即对扩展开放,对修改关闭。这是另一种非常棒的设计原则,可以防止其他人更改已经测试好的代码。理论上,可以在不修改原有的模块的基础上,扩展功能。这也是开闭原则的宗旨。

原则4:单一职责原则

类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题。

原则5:依赖注入或倒置原则

这个设计原则的亮点在于任何被 DI 框架注入的类很容易用 mock 对象进行测试和维护,因为对象创建代码集中在框架中,客户端代码也不混乱。有很多方式可以实现依赖倒置,比如像 AspectJ 等的 AOP(Aspect Oriented programming)框架使用的字节码技术,或 Spring 框架使用的代理等。

原则6:优先利用组合而非继承

如果可能的话,优先利用组合而不是继承。一些人可能会质疑,但我发现,组合比继承灵活得多。组合允许在运行期间通过设置类的属性来改变类的行为,也可以通过使用接口来组合一个类,它提供了更高的灵活性,并可以随时实现。《Effective Java》也推荐此原则。

原则7:里氏代换原则(LSP)

根据该原则,子类必须能够替换掉它们的基类,也就是说使用基类的方法或函数能够顺利地引用子类对象。LSP 原则与单一职责原则和接口分离原则密切相关,如果一个类比子类具备更多功能,很有可能某些功能会失效,这就违反了 LSP 原则。为了遵循该设计原则,派生类或子类必须增强功能。

原则8:接口分离原则

采用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。设计接口很棘手,因为一旦释放接口,你就无法在不中断执行的情况 下改变它。在 Java 中,该原则的另一个优势在于,在任何类使用接口之前,接口不利于实现所有的方法,所以单一的功能意味着更少的实现方法。

原则9:针对接口编程,而不是针对实现编程

该原则可以使代码更加灵活,以便可以在任何接口实现中使用。因此,在 Java 中最好使用变量接口类型、方法返回类型、方法参数类型等。《Effective Java》 和《Head First Design Pattern》书中也有提到。

原则 10:委托原则

该原则最典型的例子是 Java 中的 equals () 和 hashCode () 方法。为了平等地比较两个对象,我们用类本身而不是客户端类来做比较。这个设计原则的好处是没有重复的代码,而且很容易对其进行修改。

总之,希望这些面向对象的设计原则能帮助你写出更灵活更好的代码。理论是第一步,更重要的是需要开发者在实践中去运用和体会。

扩展阅读

阅读 4.9k

推荐阅读
泊浮说
用户专栏

作者是个热爱分享交流的人,所以有了这个专栏。你的点赞是我最大的更新动力。

56 人关注
45 篇文章
专栏主页
目录