从面向对象解读设计思想
作者:互联网
从面向对象解读设计思想
作者:哲思
时间:2021.8.30
邮箱:1464445232@qq.com
GitHub:zhe-si (哲思) (github.com)
前言
很早就想总结一下自己对面向对象的理解,借这次公开课梳理了一下思路,并在之后撰写成本文。
对于面向对象概念性的介绍与理解当前网上已经有很多了,但却很少有人能讲出怎样用好面向对象,也就是如何用面向对象的思想设计出好的程序。所以本文将侧重“设计”二字来讲述这个问题。
当然,本文只是我参照当下所学和做的项目产生的认识,可能随着见识的提升和技术的发展,推翻一些当下所写。但是,其中对设计的思考,想必是走向更高位置的必经之路。
注:本文举例所用的代码统一使用Kotlin,一种包含诸多高级特性、可代替Java并能够编译成诸多类型的产物、已经成为Android官方推荐的高级语言。
1.什么是面向对象
首先,给大家一个思考题。
小明是一个志存高远的程序员。一天,由于业务需要,他想要在原有数据类型Number的基础上拓展两个新的子数据类型A与B,但操作时需要统一使用父类型Number进行操作,同时需要支持调用顺序无关的相加(add)的方法(假设相加逻辑为
A.numA + B.numB
,相加结果始终为C类型)。小明的设计之魂涌上心头,打算不光要实现,还要实现一个更灵活、易拓展的设计,但没有什么好的思路,你能帮帮他吗?
1.1.面向对象的含义
从小明的问题回过头,我们开门见山的给出面向对象编程的定义:
面向对象编程就是将事物抽象成对象,针对对象所持有的数据和与之相关的行为进行编程。
想要了解这个概念,就不得不从老生常谈的编程范式的历史讲起。
当计算机世界初开的时候,世界上只有低级语言,即机器语言和汇编语言。这种语言,从计算机的角度,一步步告诉计算机它该先做什么,再做什么。而我们需要把我们实际的问题转化为计算机的基本模型:存储器、运算器、控制器、输入/输出设备,也就是把什么数据存起来,什么数据和什么数据取出来做运算。我们把这种编程方式叫做指令式编程。
后来,人们为了让编程语言更符合人的理解,所以将最能描述事物本质同时又足够抽象的数学概念引入其中,我们可以像解数学题一样定义变量、对变量相加减(此处的变量指用一个标识符代指一个数据)、甚至定义函数来表示一个通用操作过程。这样,我们就可以通过数学去描述现实事物,并将事物的发展转化为一步步的运算过程。我们把这种编程方式叫作过程式编程,也算指令式编程的一种延伸。
在编写程序的过程中,人们发现编程的本质就是处理数据,也就是数据和操作(对数据的处理)。而二者有着非常明显的对应关系,一组相关的数据,总是对应一组相关的操作。而这样的组合,便满足了我们生活中对于绝大多数事物(也就是对象)的描述。我们将现实中的事物对应程序中的对象,让程序的运行变成对象与对象间的交互。对象成为程序中的基本单元,将一类对象相关的数据和数据对应的操作封装到一起作为类,而对象则是该类的一个具体实例,这便是面向对象编程。
编程的发展史便是不断抽象来让编程符合人的认知和事物的本质。包括之后出现的函数式编程、响应式编程,都是如此。但之后的编程范式都没有完全逃脱面向对象的思想,同时都是在一些具体场景下的产物。世界是由事物组成的,这已经符合了我们对世界基本的认知,这也是面向对象一直经久不衰的原因。
1.2.面向对象的三大特征
这里要首先强调一个概念:类型。面向对象将一切看成对象,通过类去描述对象,这里的类,在程序中,就是类型。我们将一类对象定义为一种类型,并在类型中声明属性和方法(这些都是该类型的特征)。可以说,面向对象编程,从计算机角度来说,就是面向类型编程!
接下来,我们将细说面向对象的概念。而面向对象的三大特征则是对其概念最好的描述:封装、继承、多态。
三者可以说从三个层面对面向对象进行了描述。封装是面向对象最基本的表现,继承是面向对象最核心的行为,多态是面向对象最重要的能力。
1.2.1.封装
封装:将不需要外部看到的数据和对应方法放到类内,外部不可见,只暴露外部需要看到的数据和方法。
这是面向对象的初衷和最基本的表现,将相关的数据放到一起,将数据对应的方法放到一起,实现了高内聚。
同时,进行信息隐藏,将内部数据和逻辑隐藏到类的内部,只让外部看到这个类的外部表现对应的数据和操作,实现了低耦合。
举个经典的例子:
狗
属性 行为 名字、颜色、尾巴长短 吃饭、叫、尾巴长不长
class Dog(
val name: String,
val color: String,
private val tailLength: Double
) {
private val description: String
get() = "${color}色、尾巴${tailLength}厘米的狗${name}"
fun eat() {
println(description + "正在吃饭")
}
fun shout() {
println(description + "正在叫:汪汪汪!")
}
fun isTailLong(): Boolean {
return tailLength > 15
}
}
fun main() {
// 一个狗的实例对象
val dog1 = Dog("dog1", "黑白", 12.5)
// 狗暴露出的外部信息与行为
println(dog1.name)
println(dog1.color)
dog1.eat()
dog1.shout()
println(dog1.isTailLong())
}
在这里狗的属性与行为都被封装到Dog
类中。
当前场景下外界不需要了解狗的尾巴具体是多长,所以将尾巴具体长度的信息隐藏,而暴露判断尾巴长不长的方法。同时对内部实现所需的狗的自我描述description
也进行隐藏,只能通过对外暴露的行为间接访问。
这样,外部可以通过Dog来访问狗的各种外在信息与行为,同时也看不到内部具体的实现。
本例中,封装的是一个实体类,将一个实体相关的数据和方法放到一个类中。但如果只是这样,实现的方法有很多种,称不上使用了面对对象,因为现实事物都有一个很重要的描述方式:依据特征去分类。
1.2.2.继承
继承:依据相关类的共有特征进行层级分类,具体类包含抽象类(它的上一层分类)的特征,二者是一种“is-a”的关系。
这是面向对象最核心的行为与标志。子类继承父类,表示子类“is-a”父类,子类从父类得到子类共有的方法,并进行个性化实现与拓展,是一种父类别下的具体类别,有着父类包含的特征,也可以拥有自己独有的特征。而父类是一组相关子类共同特征的集合,可以从抽象层面代指子类。
比如以下的例子,
狗、猩猩、猫、兔子,都是(is-a)动物,“动物”是那些具体动物的上一级分类(当然,这里还可以说它们都是哺乳动物,这分类的依据,需要根据需求和实际情况而定),包含了具体动物在“动物”这个抽象层面的共同特征。
动物
属性 行为 名字、颜色 吃饭、叫
abstract class Animal(
val name: String,
val color: String
) {
// description是通用的内部特征,但会随着不同的实例而变化,所以将会改变的子类个性化特征描述otherDescription与子类型名typeName抽象出来,让子类实现
protected abstract val otherDescription: String
protected abstract val typeName: String
protected val description: String
get() = "${color}色${otherDescription}的${typeName}${name}"
abstract fun eat()
abstract fun shout()
}
于是,我们将狗的抽象特征提取到动物抽象类中,
class Dog(
name: String,
color: String,
private val tailLength: Double
): Animal(name, color) {
override val otherDescription = "、尾巴${tailLength}厘米"
override val typeName = "狗"
override fun eat() {
println(description + "正在吃饭")
}
override fun shout() {
println(description + "正在叫:汪汪汪!")
}
fun isTailLong(): Boolean {
return tailLength > 15
}
}
并引入新的动物类别:猩猩。它也是动物的一种,包含动物的特征。
class Orangutan(
name: String,
color: String,
): Animal(name, color) {
override val otherDescription = ""
override val typeName = "猩猩"
override fun eat() {
println(description + "正在吃饭")
}
override fun shout() {
println(description + "正在叫:嗷嗷~!")
}
}
我们虽然提取了狗和猩猩的抽象特征“动物”,但当我们直接需要狗或者猩猩对象时,二者的外在表现没有任何区别。我们可以调用它们的抽象特征和特有特征。
// main()中
// 当我们需要狗的时候,直接实例化一只狗,可以调用它的抽象特征(如:name、eat等)以及特有特征(isTailLong)
println("**************** 1 *******************")
val dog1 = Dog("dog1", "黑白", 12.5)
println(dog1.name)
println(dog1.color)
dog1.eat()
dog1.shout()
println(dog1.isTailLong())
// 需要猩猩也是同理
println("**************** 2 *******************")
val orangutan1 = Orangutan("orangutan1", "黑")
println(orangutan1.name)
orangutan1.eat()
orangutan1.shout()
// 输出
**************** 1 *******************
dog1
黑白
黑白色、尾巴12.5厘米的狗dog1正在吃饭
黑白色、尾巴12.5厘米的狗dog1正在叫:汪汪汪!
false
**************** 2 *******************
orangutan1
黑色的猩猩orangutan1正在吃饭
黑色的猩猩orangutan1正在叫:嗷嗷~!
但是当我们只需要关注动物的抽象特征、不关心具体动物的特有特征时,可以用“动物”这个抽象类别去统一代指和对待。从抽象层面,狗、猩猩都是动物。
// 当我们只需要所有的动物,不需要区分是狗还是猩猩,则可以用父类去统一代指具体类,并调用其抽象的共有特征(但这些抽象特征的具体表现不同)
println("**************** 3 *******************")
val animals = listOf(dog1, orangutan1, Dog("dog2", "白", 15.2), Orangutan("orangutan2", "棕"))
for (animal in animals) {
println(animal.name)
println(animal.color)
animal.eat()
animal.shout()
println()
}
// 输出
**************** 3 *******************
dog1
黑白
黑白色、尾巴12.5厘米的狗dog1正在吃饭
黑白色、尾巴12.5厘米的狗dog1正在叫:汪汪汪!
orangutan1
黑
黑色的猩猩orangutan1正在吃饭
黑色的猩猩orangutan1正在叫:嗷嗷~!
dog2
白
白色、尾巴15.2厘米的狗dog2正在吃饭
白色、尾巴15.2厘米的狗dog2正在叫:汪汪汪!
orangutan2
棕
棕色的猩猩orangutan2正在吃饭
棕色的猩猩orangutan2正在叫:嗷嗷~!
1.2.3.多态
多态:相同的特征,在不同情况下有不同的表现。
这是面向对象最重要的能力,也是它灵活、易拓展和复用的原因。多态本身的内涵非常宽泛,有重载多态、子类型多态、参数多态、结构多态、行多态等。从面向对象角度,最常用的是子类型多态。但不管是那种多态,都符合以上的定义,都可以在调用相同的特征后产生不同的表现。
比如,重载多态,通过重载函数(函数本身即可理解为一种能力或特征,放到类中,即该类型的特征),调用时使用不同的参数(类别、个数)进而得到不同的表现。
class Number(val num: Int) {
fun add(number: Number): Int {
return num + number.num
}
fun add(number: Int): Int {
return number + num
}
}
fun main() {
val n1 = Number(5)
println(n1.add(6))
println(n1.add(Number(6)))
}
而子类型多态,在继承的例子中已有表现,Animal
父类指代不同的子类型(狗、猩猩)时,虽然一视同仁的调用了共有的特征animal.eat()
和animal.shout()
,但却产生了不同的表现,如狗的“汪汪”叫和猩猩的“嗷嗷”叫。
这(子类型多态)是通过定义具体子类型,并调用抽象父类型的共有特征实现的多态。父类型声明了一组类型的共有特征,但不一定直接实现,可以延迟到子类型去实现,进而基于子类型不同的实现方式产生不同的表现(多态)。而这种多态的实现方式,是基于继承实现的。
由于在讲面向对象,所以以下我们所说的多态都特指子类型多态,如果描述其他类型多态,会具体说明。
1.3.面向对象的思想
上面已经说过,三大特征是从三个层面去描述面向对象。封装从代码手段层面将相关的数据和对应的操作集中放到一起,让程序聚合成类和对象的基本单元;继承从核心行为层面,给予了类聚合相关特征、灵活分类的能力;多态从表现和结果层面,描述了基于这种分类所带来的好处,即可拓展性和可复用性。
好了,读到这里,想必大家对面对对象的基本概念和想法有了初步的理解,这些知识是当前网上比较“流行”的内容,也足够大家去面试或回答本科课堂的问题(甚至比较自信的说,算是比较透彻的了
标签:封装,思想,编程,解读,面向对象,抽象,fun,变化 来源: https://www.cnblogs.com/zhe-si/p/15993059.html