面试官:「谈谈面向对象的特性」面试
码农:「封装」、「继承」和「多态」编程
面试官:能具体说一下吗?设计模式
码农:「封装」隐藏了某一方法的具体运行步骤,取而代之的是经过消息传递机制发送消息。「继承」即子类继承父类,子类比本来的类(称为父类)要更加具体化。这意味着咱们只须要将相同的代码写一次。而「多态」可使同一类型的对象对同一消息会作出不一样的响应。markdown
上面是一个普通的面试场景。这么回答是否正确呢?数据结构
你有没有想过何谓「特性」?架构
「特性」指某事物所特有的性质!函数式编程
那么问题来了,「封装」、「继承」和「多态」是面向对象所特有的吗?函数
这四种范式的语言都支持「封装」、「继承」和「多态」吗?工具
咱们经过例子来验证四种不一样范式的语言是否能实现「封装」、「继承」和「多态」!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内的字段
}
复制代码
对于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()
}
复制代码
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等于几?
可是对于比较复杂的问题,咱们不能直接获得解决方案。例如:鸡兔同笼问题,有若干只鸡兔同在一个笼子里,从上面数,有35个头,从下面数,有94只脚。问笼中各有多少只鸡和兔?
对于这类问题,咱们的通常作法就是先对问题进行抽象建模,而后再针对抽象来寻找解决方案。
对应到软件开发来讲,对于真实环境的问题,咱们先经过编程技术对其抽象建模,而后再解决这些抽象问题,继而解决实际的问题。这里的抽象方式就是:「封装」、「继承」、「多态」!而不管是基于类的实现、仍是基于类型的实现、仍是基于函数或方法的,都是抽象的具体实现。
如今再回到问题:当咱们说「面向XX编程时,咱们实际在说什么呢」?
实际就是,使用不一样的抽象方式,来解决问题。
不管是面向对象编程仍是函数式编程亦或面向过程编程,只是抽象方式的差别,而抽象方式的不一样致使了解决问题方式的差别。
每种抽象方式都既有优势也有缺点。没有完美的抽象方法。
例如,对于面向对象来讲,能够很方便的自定义类,也就是增长类型,可是很难在不修改已定义代码的前提下,为既有的具体类实现一套既有的抽象方法(称为表达式问题)。而相对的,函数式编程能够很方便的增长操做(也就是函数),可是很难增长一个适应各类既有操做的类型。
举个例子,在Java里,String这个类在1.6以前是没有isEmpty这个方法的,若是咱们想判断一个字符串是否为空,咱们只能使用工具类或者就是等着官方提供,而理想的方法应该是“abc”.isEmpty()。虽然现代化的语言都提供了各类手段来解决这个问题,像Ruby这类动态语言能够经过猴子补丁来实现;Scala能够经过隐式转换实现;Kotlin能够经过intern方法实现。但这自己是面向对象这种抽象方式所须要面对的问题。
对于函数式语言来讲,好比上面提到的Clojure,它若是要新增一个相似的函数,直接编写一个对应的函数就能够了,由于它的数据结构都实现了统一的接口:Collection,Sequence,Associative,Indexed,Stack,Set,Sorted等。这使得一组函数能够应用到Set,List,Vector,Map。可是相应的,若是你要增长一个数据结构,那就须要实现上面全部的接口,难度可想而知了。
面向对象相较于其它抽象方式的优点可能就是粒度相对较大,相对的较易理解。
这就像组装电脑同样:
抽象度越高,也就越难理解。可是抽象度越高,适应性就越强,代码相对就越简洁。
抽象程度越高,相应的抽象粒度就更细。抽象粒度越细,灵活性就更好,但也致使了维护难度越大。
编程在思想!编程范式只是辅助你思考的工具!不要被编程范式所限制!你须要考虑的是「我该如何使用XX编程范式实现XXX?」,而不是「XX编程范式能实现什么?」
每种抽象方式都有各自的优缺点。为了取长补短,每种编程范式都有本身的最佳实践。这些最佳实践被收集整理,成为了套路或模式。
例如面向对象里的: