ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

从面向对象解读设计思想

2022-03-11 12:02:30  阅读:188  来源: 互联网

标签:封装 思想 编程 解读 面向对象 抽象 fun 变化


从面向对象解读设计思想

作者:哲思

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有