软件设计本质论 Essential Design —从链表设计说起

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情
               

软件设计本质论(Essential Design) 从链表设计说起

 

转载时请注明出处:http://blog.csdn.net/absurd/

 

大师说,软件设计不过是在适当的时候做出适当的决策罢了。对此我深以为然,好的设计就是做出了正确决策。然而,在多种互相竞争的因素下,要好做出正确的决策可不是件容易的事!本文以一个双向链表的设计为例,阐述一下软件设计为什么这样困难。

 

双向链表无疑是最简单的数据结构之一。即使没有系统的学习过《数据结构》的程序员,可能不知道AVL或者红黑(RB)树,但决不会不知道双向链表。他们会说双向链表是下面这样的数据结构:

       
           

Struct _DLIST

           

{

           

         struct _DLIST* prev;

           

         struct _DLIST* next;

           

         TYPE   value;

           

};

           

 

如果用形象一点的图形来表示,可以表示为:

 

再辅以文字说明:像链子一样一环套一环,除第一个结点外的所有链表结点都有一个指向前一个结点的指针,除最后一个结点外的所有链表结点有一个指向后一个结点的指针,所有链表结点还有一个用于保存数据的域。

 

看来双向链表很简单,似乎没有什么好说的,那么本文是不是应该到此结束呢?当然不是,尽管在真正做软件设计时,双向链表是一个基本组件,所以很少有人去考虑它。但是如果要实现一套容器/算法的基本程序库,对双向链表的考虑就要纳入设计范畴了,这正是本文要说的。

 

思想与语言无关,而设计是与语言相关的。在《说说动态语言》中曾说“不少设计模式,在动态语言里根本就是多此一举,一切来得直截了当,根本不用如此拐弯抹角。”。我想并非是这些设计的思想在动态语言中就没有了,相反可能这些思想太重要了,以至于在语言层面已经有了支持,程序员不需要付出任何努力就可以享受这些思想的好处,所以说思想是与语言无关的。同样,如果语言对这种思想有了支持,程序员在设计时就不必考虑了,在使用不同的语言,在设计时考虑的内容有所不同,所以说设计是与语言相关的。

 

在本系列的序言中,我曾经说过,我们提到的方法同样适用于所有语言,但我们是基于C语言来讲解的。双向链表库设计中,有的决策正是与语言有关的,下面我们来看看用C实现双向链表遇到的问题:

 

1.         专用还是通用。我们面临的第一个决策可能是:设计一个专用链表还是通用链表?从现有的代码来,两者都有大量的应用。按存在即合理的原则来看,它们都是合理的。专用还是通用,这是一个两难的选择,在做出选择之前弄清它们各自的特点是有必要的。特点也就决定了它们的适用条件,这可以帮助我们做出决策。

 

专用链表至少具有以下优点:

类型安全:既然是专用的,前面所说节点数据的TYPE是特定的,即类型是确定的。编译器可以进行类型检查,所以专用链表是类型安全的。

 

性能更高。专用链表往往是给实现者自己用的,无需要对外提供干净的接口,常常把对链表的操作和对链表中数据的操作揉合在一起,省去一些函数的调用,在性能上可能会有更好的表现。

 

实现简单。由于是给自己用的,可以只是实现暂时用得着的函数,其它函数完全不用理会。给自己用,很多情况都在控制范围内,一些边界条件没有必要考虑,实现进一步简化。

 

通用链表至少具有以下优点:

代码重用。既然是通用的,也就是说一次编写到处使用,这有效的避免了代码重复。同时由于使用的地方多,测试更加严谨,代码会越来越稳定。这正是设计通用链表最重要的原因。

 

便于测试。通用链表要求接口函数设计得比较合理,也比较全面。可以基于契约式设计,对它进行全面测试。另外,把对链表的操作和对链表中数据的操作分开了,对链表测试更加容易。

 

事实上,专用链表和通用链表各自的优点正是对方的缺点,这里对它们的缺点不再多说。至于如何选择,您老自己做主,只有你自己才知道你想要什么。

 

2.         存值还是存指针。双向链表归根结底只是一个容器,我们的目的是用它存放我们数据。那么,我们应该在里面存放指向数据的指针,还是存放数据本身呢?为了做出这个决定,我们先研究一下两种方式各自的特点:

 

存放指向数据的指针的特点:

性能更高。在链表中获取、插入或删除数据,不需要拷贝数据本身,速度会更快。

 

更容易造成内存碎片。因为存放的是指针,数据和链表结点在两块不同的内存上,要分两次分配。链表本身就容易造成内存碎片了,若存放数据指针让内存分配次数加倍,这会加剧内存碎片倾向。

 

数据的生命周期难以管理。要保证数据的生命周期大于等于链表的生命期,否则会造成野指针问题。数据由链表释放还是由调用者释放呢?通常用链表释放比较简单一些,这需要调用者提供一个释放函数。也可以由链表提供一个遍历函数,由调用者遍历所有结点,在遍历时释放它们。

      

       存放数据本身的特点:

       效率较低。因为涉及到数据的拷贝,所以效率较低。

 

数据拷贝复杂。如果数据是基本数据类型或者普通结构,问题不大,但如果结构中包含了指其数据的指针,怎么办?这时数据要提供深拷贝函数。

 

       同样,至于如何选择,要看情况和你当时的心情。

 

3.         要封装还是要性能。

封装的目的是想把变化隔离开来,也就是说如果链表的实现变化了,不会影响链表的使用者。链表的实现比较简单,你可能会认为它不会有什么变化。这可很难说,比如,现在多核CPU风行,大家都在打多线程的主意,假设你想把链表改为线程安全的,那麻烦就来了。

 

要达到线程安全的目的,要对所有对链表的操作进行同步处理,也就是要加锁。如果你的运气比较好,调用者都通过链表函数来访问链表,加锁操作在链表函数内部实现,这对调用者没有什么影响。但是如果调用者直接访问了链表的数据成员,这种改造一定会影响调用者。

 

封装隔离了变化,特别是对于那些易变的实现来说,封装会带来极大价值。当然,凡事有利必有弊,封装也会带来一点点性能上的损失。在大多数情况下,这种损失微不足道,而在一些关键的地方,也可能成性能的瓶颈。

 

至于选择封装还是性能,要看具体情况而定。也可以采用一种折中的方法:通过宏或者inline函数去访问数据成员。

 

4.         是否支持遍历,支持何种遍历。

很多人认为遍历链表很简单,不就是用一个foreach函数把链表中的元素都访问一遍吗?没错,如果是只读遍历(即不增加/删除链表的结点),那确实如此。但实际情况并非那样简单,考虑下面几种情况(为了便于说明,我们把遍历函数称为foreach,访问元素的函数称为visit)

 

visit函数的上下文(context)。遍历链表的主要目的是要对链表中的元素进行操作,而对元素操作并非是孤立的,可能要依赖于某个上下文(context)。比如,要把链表中的元素写入文件,此时文件句柄就是一个上下文(context)visit函数中如何得到这个上下文呢,这是foreach函数要考虑的。

 

遍历时增加/删除链表的结点。要做到这一点并不是件容易的事,通常visit拿到的只是数据本身,它对数据在链表中的位置一无所知,所以它还需要额外的信息。即使它有了需要的信息,仍然不是那么简单,foreach里面还要做特殊处理,以防止引用被删除的结点。

 

遍历的终止条件。多数情况下,我们可能希望遍历链表中的所有结点。而有时当我们找到了需要的结点时,我们可能希望中止遍历,避免不必要的时间浪费。这是foreach要考虑的,当然实现很简单,可以根据visit的返回值来决定继续还是中止。

 

由此可见,遍历并非那么简单,至于是否要实现遍历,实现到何种程度,完全看你需要而定。如果你不嫌麻烦,可以使用后面的介绍的迭代器,它使遍历的实现大大简化。

 

5.         谁来管理内存。

链表是最容易产生内存碎片的容器,它往往有大量的结点,每个结点都占一块内存,每个内存块的大小很小。链表的优点是插入和删除操作非常迅捷,调用者既然选择了链表,也味着调用者可能频繁的做插入和删除操作,这种操作伴随着频繁分配/释放小块内存,所以会带内存碎片问题。

 

要对付内存碎片,通常采用专用的内存分配算法,比如固定大小分配。这种分配算法简单,问题是应该由谁来实现呢?由链表来实现吗?如果是,那么如果在平衡二叉树或者哈希表中要用到,是不是也要自己实现一套呢?所以,显然不应该由链表使用,链表只是使用者。

 

当然,如果底层的内存管理算法做得非常好,你可以不必考虑这一点。

 

6.         算法与容器是否分开。

链表只是一个容器,它的目的是方便我们对容器里的数据元素操作。对数据元素的操作即算法,是与容器合二为一,还是独立存在呢?我们当然会选择后者,原因是容器中的元素是不确定的,操作它们的算法也是不确定的。如果这里算法与链表放在一起,每次增加或者修改算法,都要修改链表,变化没有隔离开来。

 

算法与链表分开了,但算法可能要调用链表的函数,才能存取或者遍历链表中的元素,也就是说算法与链表的耦合仍然很紧密。这些算法本来可能也适用于哈希表或者二叉树的,现在与链表的耦合起来了,我们不得不为其它容器各写一套。

 

要彻底分离算法与容器,我们希望一种不暴露容器的实现,又能对容器用元素进行操作的方法。这就是迭代器模式,迭代器并非是C++的专利,在C语言里也可以使用。这里我们介绍一下C语言实现迭代器的方法(下面代码仅作演示所用,未经验证)

定义抽象的迭代器:

       
           

struct _iterator;

           

typedef struct _iterator iterator;

           

 

           

typedef BOOL  (iterator_prev)(iterator* iter);

           

typedef BOOL  (iterator_next)(iterator* iter);

           

typedef BOOL  (iterator_advance)(iterator* iter, int offset);

           

typedef BOOL  (iterator_is_valid)(iterator* iter);

           

typedef void* (iterator_get_data)(iterator* iter);

           

typedef void  (iterator_destroy)(iterator* iter);

           

 

           

struct _iterator

           

{

           

    iterator_prev      prev;

           

    iterator_next      next;

           

    iterator_advance   advance;

           

    iterator_get_data  get_data;

           

    iterator_is_valid  is_valid;

           

    iterator_destroy   destroy;

           

 

           

    char priv[1];

           

};

           

 

           

实现具体的迭代器:

       
           

typedef struct _ListIterData

           

{

           

    List*     list;

           

    ListNode* current;

           

}ListIterData;

           

 

           

iterator* list_begin(List* list)

           

{

           

    ListIterData* data = NULL;

           

    iterator* iter = (iterator*)malloc(sizeof(iterator) + sizeof(ListIterData));

           

 

           

    iter->prev     = list_iter_prev;

           

    iter->next     = list_iter_next;

           

    iter->advance  = list_iter_advance;

           

    iter->get_data = list_iter_get_data;

           

    iter->is_valid = list_iter_is_valid;

           

    iter->destroy  = list_iter_destroy;

           

 

           

    data = (ListIterData*)iter->priv;

           

    data->list    = list;

           

    data->current = list->first;

           

 

           

    return iter;

           

}

           

使用迭代器:

       
           

iterator* iter = list_begin(list);

           

 

           

for(; iter->is_valid(iter); iter->next(iter))

           

{

           

    data = iter->get_data(iter);

           

    ...

           

}

           

 

           

iter->destroy(iter);

           

 

7.         抽象还是硬编码。

每种容器都有自己的优缺点。链表的优点是插入/删除比较快捷,因它只需要修改结点的指针,而不需要移动数据。缺点是排序不方便,它不能采用像快速排序或堆排序这样的高级排序方法,查找也不能采用像二分(折半)查找那样的高级查找方法。而数组恰恰相反,插入/删除非常慢,而排序和查找非常方便。

 

同一个软件,在有的条件下,可能主要是对数据进行插入/删除,很少去查找,这时用链表比较合适。在另外一种条件下,很少对数据进行插入/删除,多数情况下是查找,这时用数组比较合适。我们能不能动态的切换容器,自适应的各种情况呢?

 

当然可以,那就是抽象一个容器接口。所有容器都实现这个接口,调用者使用抽象的容器接口,而不是具体的容器。

 

这样抽象的后果是,实现复杂了,同时限制了容器的功能。因为容器接口只能提供所有容器都能实现的函数,不能支持各种容器的专用函数了。

 

下面我们看一下用C语言如何实现(下面代码仅作演示所用,未经验证)::

定义抽象的容器:

       
           

struct _container;

           

typedef struct _container container;

           

 

           

typedef iterator* (container_begin)(container* thiz);

           

typedef BOOL (container_insert)(container* thiz, void* data);

           

typedef BOOL (container_erase)(container* thiz, void* data);

           

typedef BOOL (container_remove)(container* thiz, void* data);

           

typedef void (container_destroy)(container* thiz);

           

 

           

struct _container

           

{

           

    container_begin   begin;

           

    container_insert  insert;

           

    container_erase   erase;

           

    container_remove  remove;

           

    container_destroy destroy;

           

 

           

    char priv[1];

           

};

           

 

实现具体的容器:

           

typedef struct _ListData

           

{

           

    ListNode* first;

           

}ListData;

           

 

           

container* list_container_create(void)

           

{

           

    ListData* data  = NULL;

           

    container* thiz = (container*)malloc(sizeof(container) + sizeof(ListData));

           

 

           

    thiz->begin   = list_begin;

           

    thiz->insert  = list_insert;

           

    thiz->erase   = list_erase;

           

    thiz->remove  = list_remove;

           

    thiz->destroy = list_destroy;

           

 

           

    data = (ListData*)thiz->priv;

           

    data->first = NULL;

           

 

           

    return thiz;

           

}

           

/////////////////////////////////////////////////////

           

typedef struct _VectorData

           

{

           

    VectorNode* first;

           

}VectorData;

           

 

           

container* vector_container_create(void)

           

{

           

    VectorData* data  = NULL;

           

    container* thiz = (container*)malloc(sizeof(container) + sizeof(VectorData));

           

 

           

    thiz->begin   = vector_begin;

           

<RTL设计的艺术> verilog实现单链表遍历 假设存在这样的单链表链表每个节点为16bit,其中bit14为1标明该节点是否为根节点,如果是根节点则bit 13-0用于存储根节点对应数据;如果bit14为0则标明该节点是中间节点,bit 阅读详情

相关推荐

软件设计本质论--面向对象.pdf

软件设计本质论--面向对象,详细的讲解,容易理解

软件设计的基本原理

1.模块化 模块是程序中数据说明、可执行语句等程序对象的集合,或者是单独命名和编址的元素。在软件体系结构中,模块是可组合、可分解和可更换的单元。 模块化是指解决一个复杂问题时自顶向下逐层把软件系统分成若干模块的过程。每个模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体,完成整个系统所要求的功能。 开发一个大而复杂软件系统,将它进行适当的分解,不但可降低其复杂性,还可减少开发工作量,从而降低开发成本,提高软件生产率。这是模块划分的依据。 2.抽象 抽象是认...

迷麟的博客 5178

软件设计本质论(Essential Design) —从链表设计说起

软件设计本质论(Essential Design) —从链表设计说起 转载时请注明出处:http://blog.csdn.net/absurd/ 大师说,软件设计不过是在适当的时候做出适当的决策罢了。对此我深以为然,好的设计就是做出了正确决策。然而,在多种互相竞争的因素下,要好做出正确的决策可不是件容易的事!本文以一个双向链表设计为例,阐述一下软件设计为什么这样困难。 双

国产开源 GUI 引擎 AWTK 7839

软件即抽象

本文关键字:抽象是软件的本质设计是编程的本质 首先,什么是编程,这或许要先问,什么是软件,因为具体编程就是一种“在某平台下,使用某语言,针对解决某个需求进行实现,某个问题进行解决,由程序团队完成最终递交给客户并维护的整个过程,产生的结果就叫软件”,人们只注意到了作为结果的软件,但其实这里提到了很多实体和对象 ----- 整个软件和软件开发,它是一种寄涉众多的人类工程,每一个都不可分别而论。 具体到工业层次的软件开发和应用上,人们最终追求的始终是如何控制软件的生产周期,降低成本。更灵活地处理新出现的需求和软

minlearn's techrepos 1246

01 | 软件设计到底是什么?

今天,我们学习了软件设计到底是什么,它应该包括“模型”和“规范”两部分:模型,是一个软件的骨架,是一个软件之所以是这个软件的核心。模型的粒度可大可小。我们所说的“高内聚、低耦合”指的就是对模型的要求,一个好的模型可以有效地隐藏细节,让开发者易于理解。模型是分层的,可以不断地叠加,基于一个基础的模型去构建上一层的模型,计算机世界就是这样一点点构建出来的。规范,就是限定了什么样的需求应该以怎样的方式去完成。它对于维系软件长期演化至关重要。关于规范,常见的两种问题是:一个项目缺乏显式的、统一的规范;

qq_37756660的博客 2100

软件设计本质论 Essential Design —序

软件设计本质论 Essential Design —序

jggyff的博客 391

软件设计本质论(Essential Design) —序

软件设计本质论(Essential Design) —序 转载时请注明出处:http://blog.csdn.net/absurd/ 标题可能让人觉得有些自负,这并非出于我的本意。为了避免冒犯一些挑剔的网友,对此我先做一点说明。本质这个词历来与哲学的关系比较紧密,本质与现象对应,于是本质似乎都是一些极端抽象的东西。而近年来出版的一些书籍,如《COM本质论》、《.NET本质论第1卷:

国产开源 GUI 引擎 AWTK 6636

开发者指南:如何为cracking-the-coding-interview贡献新算法解法?PR工作流与测试数据集规范全解析

本文为开发者提供 cracking-the-coding-interview 项目的完整贡献指南:从本地一键构建,到 C++/Python 双语言解法的文件命名规范,再到单元测试用例注册与测试数据集生成规范,帮助你写出能被 CI 一次跑绿的高质量 PR。 ## 一、为什么这个项目值得贡献?📚 这个项目收录了《Cracking the Coding Interview》第 6 版全部题目的 *

gitblog_00129的博客 758

Linux终端纯C实现植物大战僵尸:从ASCII艺术到游戏引擎的编程实践

在计算机科学领域,理解程序如何驱动复杂交互系统是核心基础。通过C语言实现经典游戏,能够深入掌握数据结构、算法和系统编程原理。这种实践不仅巩固链表、指针、内存管理等基础知识,还能学习终端控制、非阻塞输入和实时渲染等底层技术。其技术价值在于剥离图形库依赖,迫使开发者聚焦游戏循环、状态管理和碰撞检测等本质逻辑,是理论到工程落地的绝佳桥梁。应用场景包括教学演示、嵌入式系统原型开发和极客项目创作。本文以Linux终端纯C实现的植物大战僵尸为例,详细拆解了如何使用ncurses库进行终端图形化,以及如何通过实体-组件系

adr5970的博客 402

CPU 缓存 | 原理、映射、一致性

……

u013669912的博客 349

软件开发的本质之一

语言、框架只是工具,不要太在乎用什么工具,应该在乎如何用好工具,如何使用工具。

m0_37359507的博客 1258

什么是设计设计本质是什么

设计的首要 因素: 发现 存在问题 设计都带有缺陷 我刚进大学时,韩国教授问我们,为什么要做设计。那时候的我心里说「因为我喜欢设计」。教授说设计为了赚钱。那时候的我接受了这个想法。 后来,我越做设计,就越发现设计不是为了赚钱。 设计可以让更多生命得到温饱,让生态进入合理的循环; 设计提高社会和程序的运行效率,节省人们更多时间,间接延长了人们的寿

chenhua1028的专栏 7972

设计本质是什么?

Future Simple的创始人兼CEOUzi Shmilovici写了一篇文章,里面谈到了设计应该关注的核心问题,我们编译如下,供各位参考。 出色的设计很好,但是用户体验最重要。Path 2.0的设计就很出色,但从作者自己的感受来说,Path实际并没有没有什么用户,其本人称自己仅用过两次,周围使用Path的人也是寥寥(当然他的感受不一定就代表所有人)。这个...

daibalusu 273

软件设计本质论(Essential Design) —白话面向对象

软件设计本质论(Essential Design) —白话面向对象 转载时请注明出处:http://blog.csdn.net/absurd/ 不同的人在谈面向对象编程(OOP)时所指的含义并不相同。有人认为任何采用图形界面的应用程序都是面向对象的。有人把它作为术语来描述一种特别的进程间通信机制。还有人使用这个词汇是另有深义的,他们其实是想说:“来啊,买我的产品吧!”我一般不提OO

国产开源 GUI 引擎 AWTK 8121

软件设计本质论—白话面向对象

不同的人在谈面向对象编程(OOP)时所指的含义并不相同。有人认为任何采用图形界面的应用程序都是面向对象的。有人把它作为术语来描述一种特别的进程间通信机制。还有人使用这个词汇是另有深义的,他们其实是想说:“来啊,买我的产品吧!”我一般不提OOP,但只要提到,我的意思是指使用继承和动态绑定的编程方式。 --《C++沉思录》   《C++沉思录》说的是十几年前的事了,现在大家对面向对象的回答已经是众口一

jjyjjyjjy的专栏 629
上一篇: X Window研究笔记 2
下一篇: 用inotify监视文件/目录变化
黎明的鹿
博客等级 码龄8年 18粉丝 226原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值