当咱们说面向XX编程时,咱们实际在说什么?

面试官:「谈谈面向对象的特性」面试

码农:「封装」、「继承」和「多态」编程

面试官:能具体说一下吗?设计模式

码农:「封装」隐藏了某一方法的具体运行步骤,取而代之的是经过消息传递机制发送消息。「继承」即子类继承父类,子类比本来的类(称为父类)要更加具体化。这意味着咱们只须要将相同的代码写一次。而「多态」可使同一类型的对象对同一消息会作出不一样的响应。markdown

上面是一个普通的面试场景。这么回答是否正确呢?数据结构

你有没有想过何谓「特性」?架构

「特性」指某事物所特有的性质函数式编程

那么问题来了,「封装」、「继承」和「多态」是面向对象所特有的吗函数

  • Java是面向对象语言
  • C是面向过程语言
  • Go是面向类型的语言
  • Clojure是函数式语言

这四种范式的语言都支持「封装」、「继承」和「多态」吗?工具

咱们经过例子来验证四种不一样范式的语言是否能实现「封装」、「继承」和「多态」!oop

封装

先来看Java:

  • Java是经过类来进行封装,将相关的方法和属性封装到了同一个类中

  • 经过访问权限控制符来控制访问权限。

    public class Person { private String name;

    public String say(String someThing) { ... } }

而C则是:

  • 经过方法来进行封装,将具体的过程封装到一个个方法中

  • 经过头文件来隐藏具体的细节

    struct Person;

    void say(struct Person *p);

相对于Java来讲,C的封装性实际更好!由于Person里的结构都被隐藏了!

对Go语言来讲,乍看之下像是以函数进行封装的,可是实际上在Go语言中函数也是一种类型。因此能够说Go语言是以类型来进行封装的。

func say(){
 fmt.Println("Hello")
}

func main() {
 a := say
 a()
}
复制代码

而Clojure则主要以函数的形式进行封装。

(defn say []
 (println "Hello"))
复制代码

能够看出来,四种语言都支持封装,只是封装的方式不一样而已

继承

再来看继承,继承实际就是代码复用

继承自己是与类或命名空间无关的独立功能。只不过面向对象语言将继承绑定到了类层面上,而面向对象语言是比较广泛的语言,一直强调继承,因此当咱们说继承的时候,默认就是在说基于类的继承。

Java是基于类的继承。也就是说子类能够复用父类定义的非私有属性和方法。

class Man extends Person {
 ...
}
复制代码

C语言能够经过指针来复用。和下面Go语言比较相似,Go语言相对更简单。而C则更像是奇技淫巧!

struct Person {
 char* name;
}

struct Man {
 char* name;
 int age;
}

struct Man* m = malloc(sizeof(struct Man));
m->name = "Man";
m->age = 20;
struct Person* p = (struct Person*) m; // Man能够转换为Person
复制代码

而Go语言则是经过匿名字段来实现继承。即一个类型能够经过匿名字段复用另外一个类型的字段或函数。

type Person struct {
 name string
}

type Man struct {
 ...
 Person // 引入Person内的字段
}
复制代码
  • Man经过直接在定义中引入Person,就能够复用Person中的字段
  • 在Man中,既能够经过this.Person.name来访问name字段,也能够直接经过this.name来访问
  • 若是Man中也有name这个字段,则经过this.name访问的则是Man的name,Person里的name被覆盖了
  • 此方案对函数也适用

对于Clojure来讲,则是经过高阶函数来实现代码的复用。只须要将须要复用的函数做为参数传递给另外一个函数便可。

; 复用的打印函数
(defn say [v]
 (println "This is " v))
 
; 打印This is Man
(defn man [s]
 (s "Man"))

; 打印This is Women
(defn women [s]
 (s "Women")) 
复制代码

同时Clojure能够基于Ad-hoc来实现继承,这是基于symbol或keyword的继承,适用范围比基于类的继承普遍。

(derive ::man ::person)
(isa? ::man ::person) ;; true
复制代码

能够看出,四种语言也都能实现继承

多态

多态实际是对行为的动态选择

Java经过父类引用指向子类对象来实现多态。

Person p = new Man();
p.say();
p = new Woman();
p.say();
复制代码

C语言的多态则是由函数指针来实现的。

struct Person{
 void (* say)( void ); //指向参数为空、返回值为空的函数的指针
}

void man_say( void ){
 printf("Hello Man\n");
}
 
void woman_say( void ){
 printf("Hello Woman\n");
}
...
p->say = man_say;
p.say(); // Hello Man
p->say = woman_say;
p.say(); // Hello Woman
复制代码

Go语言经过interface来实现多态。这里的interface和Java里的interface不是一个概念

; 定义interface
type Person interface {
 say()
}

type Man struct {}
type Women struct {}

func (this Man) say() {
 fmt.Println("Man")
}

func (this Women) area() {
 fmt.Println("Women")
}

func main() {
 m := Man{}
 w := Women{}
 exec(m) // Man say
 exec(w) // Women say
}

func exec(a Person) {
 a.say()
}
复制代码
  • Man和Women并无像在Java里同样实现了interface,而是定义了和在Person里相同的方法
  • exec函数接收参数为interface

Clojure除了能够经过高阶函数来实现多态(上面的例子就是高阶函数的例子)。还能够经过「多重方法」来实现多态。

(defmulti say (fn [t] t))

(defmethod run
 :Man
 [t]
 (println "Man"))

(defmethod run
 :Women
 [t]
 (println "Women"))

(rsay :Man) ; 打印Man,结合Ad-hoc,能够实现相似Java的多态
复制代码

四种语言一样都能实现多态

问题的解决

从上面的对比可知,「封装」、「继承」和「多态」并非面向对象所特有的

那么当咱们说「面向XX编程时,咱们实际在说什么呢」?

咱们从解决问题的方式来回答这个问题!

对于一些很简单的问题,咱们通常能够直接获得解决方案。例如:1+1等于几?

当咱们说面向XX编程时,咱们实际在说什么?

可是对于比较复杂的问题,咱们不能直接获得解决方案。例如:鸡兔同笼问题,有若干只鸡兔同在一个笼子里,从上面数,有35个头,从下面数,有94只脚。问笼中各有多少只鸡和兔?

对于这类问题,咱们的通常作法就是先对问题进行抽象建模,而后再针对抽象来寻找解决方案。

当咱们说面向XX编程时,咱们实际在说什么?

对应到软件开发来讲,对于真实环境的问题,咱们先经过编程技术对其抽象建模,而后再解决这些抽象问题,继而解决实际的问题。这里的抽象方式就是:「封装」、「继承」、「多态」!而不管是基于类的实现、仍是基于类型的实现、仍是基于函数或方法的,都是抽象的具体实现。

当咱们说面向XX编程时,咱们实际在说什么?

如今再回到问题:当咱们说「面向XX编程时,咱们实际在说什么呢」?

实际就是,使用不一样的抽象方式,来解决问题

抽象方式:只是不一样,没有优劣

不管是面向对象编程仍是函数式编程亦或面向过程编程,只是抽象方式的差别,而抽象方式的不一样致使了解决问题方式的差别。

  • 面向对象将现实抽象为一个个的对象,以及对象间的通讯来解决问题。
  • 函数式编程将现实抽象为有限的数据结构,以及一个个的对这些数据结构进行操做的函数,经过函数对数据结构的操做,以及函数间的调用来解决问题。
  • 面向过程编程将现实抽象为一个个数据结构和过程方法,经过方法组合调用以及对数据结构的操做来解决问题。

每种抽象方式都既有优势也有缺点。没有完美的抽象方法

例如,对于面向对象来讲,能够很方便的自定义类,也就是增长类型,可是很难在不修改已定义代码的前提下,为既有的具体类实现一套既有的抽象方法(称为表达式问题)。而相对的,函数式编程能够很方便的增长操做(也就是函数),可是很难增长一个适应各类既有操做的类型。

举个例子,在Java里,String这个类在1.6以前是没有isEmpty这个方法的,若是咱们想判断一个字符串是否为空,咱们只能使用工具类或者就是等着官方提供,而理想的方法应该是“abc”.isEmpty()。虽然现代化的语言都提供了各类手段来解决这个问题,像Ruby这类动态语言能够经过猴子补丁来实现;Scala能够经过隐式转换实现;Kotlin能够经过intern方法实现。但这自己是面向对象这种抽象方式所须要面对的问题。

对于函数式语言来讲,好比上面提到的Clojure,它若是要新增一个相似的函数,直接编写一个对应的函数就能够了,由于它的数据结构都实现了统一的接口:Collection,Sequence,Associative,Indexed,Stack,Set,Sorted等。这使得一组函数能够应用到Set,List,Vector,Map。可是相应的,若是你要增长一个数据结构,那就须要实现上面全部的接口,难度可想而知了。

抽象程度与维护成本正相关

面向对象相较于其它抽象方式的优点可能就是粒度相对较大,相对的较易理解

这就像组装电脑同样:

  • 面向对象就像是将电脑拆分红了主板、CPU、显卡、机箱等,你稍微学一学就能够组装了。
  • 而函数式编程就像将电脑拆成了一个个的元器件。你既须要学习相关知识,还得将这些元器件组装起来。难度可想而知了。可是组合方式和自由度则比面向对象好得多。

抽象度越高,也就越难理解。可是抽象度越高,适应性就越强,代码相对就越简洁

抽象程度越高,相应的抽象粒度就更细。抽象粒度越细,灵活性就更好,但也致使了维护难度越大。

总结

编程在思想!编程范式只是辅助你思考的工具!不要被编程范式所限制!你须要考虑的是「我该如何使用XX编程范式实现XXX?」,而不是「XX编程范式能实现什么?」

每种抽象方式都有各自的优缺点。为了取长补短,每种编程范式都有本身的最佳实践。这些最佳实践被收集整理,成为了套路或模式。

例如面向对象里的:

  • 复用代码优先考虑组合而不是继承
  • 为多态而继承
  • 设计原则
  • 23种设计模式
  • ...

参考资料

相关文章
相关标签/搜索