其他分享
首页 > 其他分享> > 从面向对象解读设计思想

从面向对象解读设计思想

作者:互联网

从面向对象解读设计思想

作者:哲思

时间: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