写代码这件事,迈入第七个年头才有了一些心得(第三章 抽象下篇)
本篇是抽象的下篇,围绕案例展开。
一、抽象概念
眼见的自然形式会蒙蔽真实的本质,简化事物的形象,才能显露艺术的真实面目 -- [荷兰]蒙德里安
抽象是一种思维,也是一种艺术,它是现实世界与机器语言之间的桥梁。
抽象就是在特定的环境,特定的视角,特定的维度,对事物进行分析、提炼、精简、归纳、推演、概括等行为,从具体到抽象,探索事物的规律和本质。
规避细节,提炼共性, 推演归集......
抽象思维是编程中最为重要的思维,充斥着每一天的生活。
二、抽象案例
浅析表单的低代码实现
元数据是对数据的描述,元数据是对数据本身的抽象;而低代码是对代码的进一步抽象,也是站在更高的视角看问题。
下面对表单进行分析,逐步理解抽象的过程。如下所示:
原始表单 | 分析表单 |
---|---|
一个表单,由多个表单组件构成。(此处的组件,可以理解为表单中的文本框等)
对一个表单控件进行简单分析,理解它的组成要素,提取它的元素。(它非常像我们拿到原型去实现一样,都是将一个生动的画面,用代码进行抽象,然后去实现一样的过程)
原数 | 描述 |
---|---|
icon | 小图标 |
title | 名称,比如姓名,电话、邮箱 |
type | 类型:比如邮箱类型、比如电话类型 |
length | 长度 |
order | 顺序 |
此时把表单控件的 "骨架" 和"血肉" 进行分离,"血肉" 用占位符表示,那么表单控件的 "骨架" 都是相似的,不同的控件用不同的占位符表示。
bash
{
"icon":"${xx}$",
"title":"${xx}$",
"type":"${xx}$",
"length":${xx}$,
"require":${xx}$,
"order":${xx}$
}
由此,一个简单的表单就可以用像下面的 JSON 进行描述。
JSON
{
{
"icon":"icon...",
"title":"姓名",
"type":"name",
"length":120,
"require":false,
"order":1
},{
"icon":"icon...",
"title":"电话(必填)",
"type":"phone",
"length":120,
"require":true,
"order":2
},{
"icon":"icon...",
"title":"邮箱",
"type":"email",
"length":120,
"require":false,
"order":3
},{
"icon":"icon...",
"title":"地址",
"type":"address",
"length":120,
"require":false,
"order":4
}
}
比如再需要一个新表单,我会这样理解: 下图左边是样式,JSON 元数据描述如右边所示:
表单描述 | 控件描述 |
---|---|
于是,所有的表单都是可以用 JSON 进行描述的。再用一张表来管理所有表单描述:
表单Id | 类型 |
---|---|
id | int |
schema | json |
JSON
{
{
"icon":"icon...",
"title":"姓名",
"type":"name",
"length":120,
"require":false,
"order":1
},{
"icon":"icon...",
"title":"电话(必填)",
"type":"phone",
"length":120,
"require":true,
"order":2
}
}
表单可以得到 schema 描述信息,那么是否通过 schema 也能得到表单呢?
前端通过拿到 JSON 数据,将控件遍历渲染就能够形成不同的表单,这就能实现相互转换了。
更进一步:
如果有一个地方能生成不同的 schema, 在另外一个地方进行渲染。 从而形成不同的表单了。
一个简单的表单低代码平台:
- 编辑态:拖拉拽形成不同的 schema 元数据
- 运行态:通过 schema 渲染形成不同的表单
一个简单的低代码表单实现想法就有了雏形。这是一种抽象的过程。
当然实际的低代码远比这个例子复杂的多,不过,在程序的世界里,一切皆可抽象!!!
接下来通过依赖倒置谈一谈抽象在软件工程中的应用,依赖倒置也是面向对象五大原则(SOLDI)中最重要的一个原则,同时也能体现如何进行抽象。
三、抽象-依赖倒置(DIP)
如果你一直写重复的代码,那么你可能体验不到编程的快乐。
依赖倒置,是我认为进行代码抽象最关键也是最重要的一个面向对象原则。 刚入门学的那会儿,被依赖倒置这个概念搞得云里雾里的,直到我开始理解并实践它,我才知道依赖倒置是那样的精髓。
理解依赖倒置之前,先理解依赖关系。
依赖关系
一般而言,依赖关系在最终的代码里体现为类构造方法、类方法等的传入参数。与关联关系相比,依赖关系除了临时"知道"对方外、还会"使用"对方的属性或方法。从这个角度讲,被依赖的对象的改变会导致依赖对象的改变。
举一个简单例子:
A 调用 B,那么 A 对 B 有依赖关系,正向依赖。
依赖倒置案例
高层模块不应该依赖底层具体实现,应该依赖抽象,模块之间的信息传递是通过抽象进行的。对应代码即接口类/抽象类不要依赖具体实现,否则也不能进行扩展。
在编写多年代码后才慢慢有体感,如下图所示,箭头的方法发生了变化。
还是案例结合吧,逐步分析依赖倒置的乐趣。以软件工程师阅读学习稀土掘金文章开始讲解吧
Java
public interface JuejinArticle {
}
// 软件工程师
public interface SoftwareEngineer {
void learn(JuejinArticle juejinArticle)
}
软件工程依赖稀土掘金文章进行学习,按照依赖这个思路出发,程序员学习就要依赖稀土掘金的文章了。 显然这是狭隘的理解; 更极端的理解,得先有了稀土掘金文章,软件工程师才进行学习。 但实际上,没有稀土掘金,软件工程也可以通过书籍文章进行学习,甚至软件工程也可以阅读任何文字进行学习。
而软件工程师学习的是知识,文章只是知识的一种承载形式。
软件工程师也不再依赖稀土掘金文章了,而是依赖知识。而稀土掘金文章不再被软件工程师所依赖。而稀土掘金文章却需要对知识进行实现,不然软件工程师也不需要稀土掘金文章,此时系统掘金文章是依赖知识,也是依赖于软件工程师的。
Java
public class JuejinArticle implements Knowledge {
}
// 软件工程师
public interface SoftwareEngineer {
void learn(Knowledge knowledge)
}
现在 A 调用 B, 但是却是 B 对 A 有依赖关系。 这就是简单的依赖倒置 DIP。
而依赖倒置,将控制权进行了转换。也从另外一个角度来看,我们的代码更加灵活,更加更具扩展性了。
Java
// csdn
public class CSDNArticle implements Knowledge {
........
}
// github
public class GithubArticle implements Knowledge {
......
}
可能软件工程师可能最终还是通过稀土掘金文章进行学习知识,但思维方式已经完全不同了。
就像在拿到原型实现功能逻辑一样,最后大家写出来的程序都能跑,甚至看不出什么价值差异点; 可能真正区别的是这些代码背后软件工程师们的思维方式。
再多啰嗦两个依赖倒置的两个场景。
场景扩展之 Spring#IOC
Spring IoC
(Inversion of Control
,控制反转),由Spring
来负责控制对象的生命周期和对象间的关系。即使用方掌控控制权。是 Spring 最核心的特性。
场景扩展之 API 和 SPI
SPI:控制反转,外部去实现我的定义,我定义标准。SPI 先定义后实现。
API:依赖调用,具体实现也是由外部实现。
都是接口调用,但是对接口的拥有和身份是截然不同的。一个是定义标准,一个是遵守标准。
依赖倒置小结
在程序中,通过直接依赖,还是依赖倒置并没有一个精准的判断,软件工程师对这些的理解可能也不一定相同,因此具体的实现也可能差别很大。是一种内功心法,随着变化,可能差异就会有一些变化。
很多时候不是拿着表就开始写crud; 不是说着面向对象的话,干着面向过程的活; 架构的设计会影响到软件的维护成本和生命周期。
到这里,抽象的理解到此结束,下一章,会谈一谈其他面向对象原则在写代码上这件事上的影响。
四、抽象总结
- 找共性,提炼本质,软件的实现过程就是抽象的过程。
- 框架不是细节,需要打磨,控制好粒度
- 软件的设计实现就是不断对已有事物进行分析,按照特定的思维进行归纳,通过这些归纳分析总结去应对事物变化的复杂性。
希望有一天你的代码像一件艺术品,被反复欣赏!
🌾 代码只是形式,抽象思维才是编程的灵魂。