今年的第一本书阅读了王争(@小争哥)的《设计模式之美》,读后感觉受益匪浅。要学习好设计模式,首先需要深刻理解面向对象,本文将深入浅出地介绍面向对象的编程的思想和我的一些理解。同时这里也表达一下对把这本好书赠予我的韩骏韩老师的感谢。大家可以点击下方公众号名片关注韩老师:
在5年前,我刚开始学习编程的时候,我也被面向对象卡了很久。我是从Python开始入门编程的,当时学习的时候计算阶乘、水仙花数用高中数学的知识都能轻松做出来。但是学到class
的时候就不会了,我一直没搞清楚self
在类的方法中起到什么作用。后来过了一段时间又从头学了一次,才慢慢有了一点感觉。第二次是学习Java的时候,有个例子是使用不同的打印机调用打印机接口的同一个函数,输出了不同的打印结果,也就是多态,也让我理解了非常久。
回顾我的学习过程,我认为之所以我在学习的过程中频繁卡壳,根本原因是我当时对面向对象这个编程思想一知半解,一直用面向过程的方式去思考问题,解决问题。并且现在,随着编程语言的发展,软件的复杂度也越来越高,几乎不可能凭借一己之力完成大型软件的开发。为了更好地进行协作,我们必须要学习好面向对象,同时面向对象的蓬勃发展也暗示着面向对象当下就是最适合的编程思想。那首先需要回答一个问题:什么是面向对象?
现代的编程语言的编程范式(Programming paradigm)指的是编程风格,主要包含面向过程编程、面向对象编程和函数式编程等。区分这几种编程范式主要是通过分析问题的角度。面向过程编程主要是从问题出发,将大的问题(函数)拆分成小的问题逐个击破;函数式编程则是将运算都看作函数,主要通过输入和返回一个函数完成问题的解决;而面向对象以类或者对象作为载体,实现了封装、继承、多态三大特性,解决问题的函数(方法)和数据都由类或者对象所持有。
封装(Encapsulation)主要是通过类暴露有限的接口,限制外部访问和修改类或对象所持有的数据。为什么我们需要封装呢,比如说,我有一个类用来生成一个人的对象:
class People {
public int age;
public long money;
}
如果我们对这个人的类不加以限制,任何程序员都能够获取这个人的年龄和身上所持有的钱,那么也就意味着,这个程序员对这个属性有完全的控制权限,他可以任意修改这些属性。可以想象一下,如果有个上帝能随意修改你的年龄,剥夺你的金钱,你该多么难受。但是实际上我们只需要知道这个人的年龄,对于修改他所持有的钱,我们需要通过一定的方式修改而不是完全交给别的程序员来修改。改动后的代码大致如下:
class People {
private int age;
private long money;
public int getAge() {
return age;
}
public void setMoney(long money) {
this.money += money;
}
}
继承(Inheritance)主要是为了代码复用。比如动物都能发出叫声,但是鸟可以飞,鸭子可以游泳,可以根据不同的动物给他们加上不同的运动方式:
class Animal {
public void speak() {
System.out.println("I can speak.");
}
}
class Bird extends Animal {
public void fly() {
System.out.println("I can fly.");
}
}
class Duck extends Animal {
public void swim() {
System.out.println("I can swim.");
}
}
这样我们就能避免编写多次重复的 speak()
方法的代码。
正如上面打印机的例子,多态(Polymorphism)指的是,用子类代替父类调用子类的方法。仍然以上面的动物作为例子。这次我们给鸟和鸭子加上不同的叫声:
class Animal {
public void speak() {...}
}
class Bird extends Animal {
public void speak() {
System.out.println("Caw Caw") // 鸟叫
} ...
}
class Duck extends Animal {
public void speak() {
System.out.println("Quack Quack"); // 鸭叫
} ...
}
class AnimalKeeper {
private static void test(Animal animal) {
animal.speak();
}
public static void main(String[] args) {
Animal animal = new Animal(), bird = new Bird(), duck = new Duck();
test(animal); // I can speak.
test(bird); // Caw Caw.
test(duck); // Quack Quack.
}
}
我们在测试函数中生成了3个 Animal
类,但是这3个对象却产生了3种不同的现象。这里的2个子类,Bird
和 Duck
类都重写(Override)了父类中的方法,通过继承的方式能够从 Animal
类生成这两个类,这样就通过父类引用子类,调用了子类中的方法。有的人可能会说,那你这个没什么必要,我可以通过直接使用子类一样能调用子类的方法:
class AnimalKeeper {
private testBird(Bird bird) {
bird.speak();
}
private testDuck(Duck duck) {
duck.speak();
}
public static void main(String[] args) {
Bird bird = new Bird();
Duck duck = new Duck();
testBird(bird); // Caw Caw.
testDuck(duck); // Quack Quack.
}
}
确实这样可以调用到具体的子类。但是如果这个时候来了一个新的需求需要给狗也加一个类似的方法,或者来了更多的需求需要加更多的动物。那么我们就需要往这个类中加入很多方法,不仅容易出错,也不美观。在Java
中为了保证子类重写了父类的方法,我们可以在子类的方法前加上@Override
来检查。
那么我们怎么理解多态呢?我觉得一种比较好的方式是将子类理解为父类加持有自己独特的东西。也就是说子类至少是父类,它能够完成父类的所有工作,如果在这个基础上子类重写了父类的方法或者持有自己独特的方法,那么它就可以做到父类做不到的事情,也就是完成了代码的扩展。另一方面,在使用方法时,我们还是要按照子类的类型去使用方法,因为方法是绑定在对象上的。并且子类向上转型为父类是一定可以成功的,我们可以说所有的鸭子都是动物,但反过来不一定成立,有的动物可以是狗或者其他。所以我们可以用父类来声明一个子类,是安全的。
总而言之,多态通过继承和重写的方式使用父类声明调用子类的方法,可以体现代码的复用性和可扩展性。
在回答了什么是面向对象的问题后,还需要回答一个问题:为什么我们要使用面向对象?
当今软件业,包括互联网行业飞速发展,业务也越来越庞杂。无论是商业软件还是开源社区都需要多人协作开发。这就需要分工,需要软件架构师对软件结构进行分层分模块。这样不同的人进行不同的工作,既能够加快软件开发的进度又能够发挥每个人的所长,提升开发效率。
如果不遵循面向对象的编程思想,可能会导致代码结构不合理。这样的代码维护性差,可能牵一发而动全身,导致程序员不能添加或者修改代码。并且这种情况会随着代码的增多变得更加严重。另外如果经手这个部分的程序员离职,由于新接收的程序员可能不是十分熟悉业务,将会花费大量的时间在理清这些错综复杂的逻辑上面,耽误开发进度。
面向对象具有的三大特性可以实现非常多而复杂的设计思路,同时它也是设计模式的基础。只有充分理解了面向对象,才能继续学习设计模式。在程序员的初级阶段,我们可能只需要对业务进行理解,编写逻辑代码。但是随着我们职业的发展,身上的担子也会越来越重,我们终会需要自己对复杂代码进行设计和开发。这就需要我们对面向对象特性、设计原则和设计模式有足够的了解。这些是沉淀了几十年的软件开发人员智慧的结晶,经过了无数次生产实践的锤炼,能够解决很多具体的问题。