深度思考:到底什么是抽象?

简介: 不要为了抽象而抽象

到底什么是抽象?

前言:

不要为了抽象而抽象。 ——《Tony Bai · Go 语言第一课》

1 回顾软件设计的六大原则

一、单一职责原则(SRP: Single responsibility principle)

二、开放封闭原则(OCP: Open Closed Principle)

三、里氏替换原则 ( LSP: Liskov Substitution Principle)

四、接口隔离原则( ISP: Interface Segregation Principle)

五、依赖倒置原则( DIP: Dependence Inversion Principle)

六、迪米特原则(Law of Demeter)

解释:

  • 单一职责原则:

    一个类/接口/方法只负责一项职责,并且有且只有一个需要被改变的理由。

  • 开放封闭原则:

    一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

  • 里式替换原则:

    子类可以替换父类,即子类可以扩展父类的功能,但是不能改变父类原有的功能。

  • 接口隔离原则:

    用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口,一个类对一个类的依赖应该建立在最小的接口上。

  • 依赖倒置原则:

    高层模块不应该依赖底层模块,二者都应该依赖其抽象。

  • 迪米特原则:

    一个对象应该对其他对象保持最少的了解,又叫最少知道原则。

2 抽象引入

在依赖倒置原则中有一个非常常见但又让你理解起来很模糊的名词——抽象

我们看下维基百科对抽象的解释:

抽象就是把一个问题或模型,以不同规则或方法所得出的不同的解(求解方法和解本身即抽象层),这些不同的解可以组合并还原成问题或模型的本身。

抽象的意义是可以忽略不是求解过程中必需的。例如要用计算机程序)去模拟“人”,在描述了人的动作(饮食、思考、移动等)符合设计要求后(如可完整表达“人”在坐下时候的动作),其他“人”的细节都可以忽略,以集中设计需要的功能,并减低程序的复杂度。

在程序设计中,高级语言是对机器指令序列的抽象。高级语言的出现,使得程序的编写得以简化,极大提高了程序的编写效率。随着软件技术的发展,组件技术进一步提升了程序抽象的级别。

另一种可取的替代方法是设计一种语言机制,允许程序师在需要的时候构建自己的抽象方法。一个通用的机制是使用过程(procedure)。通过分离过程的定义和规则,编程语言包含了两种重要的抽象方法:参数化抽象(abstraction by parameterization)和规范化抽象(abstraction by specification)。

程序设计中,抽象类别包括下列4类:

1.过程抽象:能够引入一些新的操作;

2.资料抽象:能够引入新的资料对象类型;

3.反复运算抽象:能够反复运算遍历在集合中的元素,而不必显示如何获得元素的细节;

4.类型层次:能够从多个单独的资料类型中抽象成几组相关的类型。

3 软件设计中的抽象举例

3.1 业务场景

在平时生活中我们每个人在加入一个新的群体前都会进行自我介绍,以此来让人们认识自己,比如一名老师会这样说:“大家好,我是一名老师,我叫XXX”,一名运动员会说:“大家好,我叫XXX,我的职业是一名运动员”等等,但是生活中总是不乏有有趣的人出现,比如突然来了一位程序员自我介绍:“Hello World,我叫XXX,来自中国,是一名Programmer”。

由上述我们可以得知,每个人的自我介绍一般都会先问好,然后介绍自己的职业,自己的姓名等等,于是我们可以将每个人的自我介绍进行提取和抽象,在大多数情况下每个人都可以依赖这个抽象,并且在自己有别出心裁的自我介绍时还可以将抽象具体化成自己的自我介绍。

因此,到这里我们就可以总结出可以抽象的部分问好、职业,(因为每个人都会先问好,并且很多人也都可能从事同一个职业,但是名字的抽象度并不高):

  • 问好
  • 职业

3.2 代码实现

提供抽象:
// Say 抽象接口
type Say interface {
    // SayHello 抽象方法
    SayHello() string
}

// PeopleSayHello 利用反射调用接口
func PeopleSayHello(dest interface{}) (string, error) {
    var sayHello string
    if dest == nil {
        return "dest is nil", nil
    }
    value := reflect.ValueOf(dest)
    if value.Kind() == reflect.Ptr && value.IsNil() {
        value = reflect.New(value.Type().Elem())
    }
    modelType := reflect.Indirect(value).Type()
    if modelType.Kind() == reflect.Interface {
        modelType = reflect.Indirect(reflect.ValueOf(dest)).Elem().Type()
    }
    modelValue := reflect.New(modelType)
    sayHello = DefaultSayHello(modelType.String())
    if say, ok := modelValue.Interface().(Say); ok {
        sayHello = say.SayHello()
    }
    return sayHello, nil
}

// DefaultSayHello 抽象的默认实现
func DefaultSayHello(str string) string {
    split := strings.Split(str, ".")
    return "Hello,I am a " + split[len(split)-1]
}
使用抽象:
// Teacher 教师
type Teacher struct {
    Name string
    Age  int
}

// Student 学生
type Student struct {
    Name string
    No   string
    Age  int
}

// Sportsman 运动员
type Sportsman struct {
    Name string
    Age  int
    Like string
}


func (stu Student) SayHello() string {
    return "Hello,I am a Student and my name is Zs."
}

func (tea Teacher) SayHello() string {
    return "Hello,I am a Teacher and my name is Ls."
}
运行:
func main() {
    teacherHello, err := PeopleSayHello(Teacher{})
    studentHello, err := PeopleSayHello(Student{})
    sportsmanHello, err := PeopleSayHello(Sportsman{})
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(teacherHello)
    fmt.Println(studentHello)
    fmt.Println(sportsmanHello)
}

<img src=" title="image-20220331142523472">

解释:

在整个代码中,我们使用接口定义了抽象,使用接口的方法定义了抽象方法,并且通过反射能够让抽象更加的通用,进而形成抽象层,将人们习惯的方式进行统一,但是通过提供抽象的具体化让每个角色能使用自己的具体方法。

4 探究反射和抽象

"反射是框架设计的灵魂"

我们都知道架构的设计高度依赖于反射这个特性,可以说反射是框架的灵魂,究其原因,反射是因为有以下这个概念:

反射用于观察并修改程序在运行时的行为。一个反射导向的程序组件可以监测一个范围内的代码执行情况,可以根据获取的目标对象信息及与此相关的范围修改自身。这可通过在运行时动态分配程序代码实现。

反射主要用途就是使给定的程序,动态地适应不同的运行情况。利用面向对象建模中的多态(多态性)也可以简化编写分别适用于多种不同情形的功能代码,但是反射可以解决多态(多态性)并不适用的更普遍情形,从而更大程度地避免硬编码(即把代码的细节“写死”,缺乏灵活性)的代码风格。

反射也是元编程的一个关键策略。

反射本身很抽象,反射让架构更加抽象。

反射本身是抽象的,我认为反射的抽象在于他能在有条件的情况下取出运行时的对象状态,进而作出判定或修改。

反射的最大优势在于动态地适应不同的运行情况,进而解决多态,比如在上面的代码中,无论实例化的结构体类型是Teacher、Student还是Sportsman,反射构成的方法都可以当做interface{}类型进行处理,进而能让方法更加的抽象化,这种方式在架构设计和编码中也非常的常见,如Java的JDBC抽象、Spring IOC抽象等等。

5 更高级的抽象

对于计算机领域中,我认为更高级的抽象是数学

德国数学家Hermann Weyl曾说:“数学抽象中最关键的一步让我们忘记这些符号所表示的对象,有许多操作可以应用于这些符号,而根本不必考虑他们到底代表着什么。”数学离不开抽象,架构的建模过程同样离不开抽象。

对于数学和软件架构的关系,因为本人目前的阅历也是较为浅薄,所以在此先一笔带过,待日后经验更加丰富,思想更加有深度时再回来补充。

6 总结

文章的开头前言就写出了一句话:“不要为了抽象而抽象”

确实是这样的,盲目的抽象只会让系统过度设计,而我们要做的就是懂得适度权衡。

为什么要抽象?这是一个值得思考的问题,而我认为抽象最大的好处就是:

  • 能够最大限度的减少重复
  • 基于抽象可以灵活的扩展

所以只有当我们的设计能够达到可抽象的要求是才适合使用抽象,而本来的简单和精巧的设计不一定非要进行抽象。

以上就是个人对抽象的全部理解,欢迎读者留言~

参考文章:

https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E5%8C%96_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8)

https://blog.csdn.net/qq_26900081/article/details/88053385

https://blog.csdn.net/u014681799/article/details/113888079

https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%B0%84%E5%BC%8F%E7%BC%96%E7%A8%8B

相关文章
|
7月前
|
Java Go 数据库
深度思考:到底什么是面向接口编程?
深度思考:到底什么是面向接口编程?
115 0
|
7月前
|
架构师 测试技术 Linux
嵌入式软件架构中抽象层设计方法
嵌入式软件架构中抽象层设计方法
244 0
|
4月前
|
uml
建模底层逻辑问题之在建模时,对现实进行抽象该如何操作
建模底层逻辑问题之在建模时,对现实进行抽象该如何操作
|
5月前
|
存储 Java 程序员
【c++】继承深度解剖
【c++】继承深度解剖
41 1
|
5月前
|
开发者
领域驱动设计问题之为什么防腐层允许随时挖掘隐式概念
领域驱动设计问题之为什么防腐层允许随时挖掘隐式概念
|
6月前
|
架构师 持续交付 微服务
探索软件架构设计的深层逻辑
【6月更文挑战第5天】在数字化浪潮中,软件架构设计如同搭建一座虚拟的巴别塔,它不仅需要承载技术的重量,还要预见未来的需求。本文将通过我的个人经验,探讨如何在变化莫测的技术海洋中,寻找到稳固的架构基石,以及如何让这座塔楼灵活地适应不断变化的环境。
53 1
|
7月前
|
SQL 设计模式 Java
【软件工程底层逻辑系列】建模的底层逻辑
在本文中,给出建模的底层逻辑:用图形逻辑地表达现实业务的抽象,通过一些大家通识的技术案例讲述建模的过程。
75004 3
|
设计模式 测试技术 持续交付
深入抽象和动态建模(1)
深入抽象和动态建模
237 0
深入抽象和动态建模(1)
|
设计模式 自然语言处理 前端开发
深入抽象和动态建模(2)
深入抽象和动态建模
深入抽象和动态建模(2)
【逻辑思维训练 一】金字塔思维概述
【逻辑思维训练 一】金字塔思维概述
134 0