标签 设计模式 下的文章

致敬:程序员成长路上的良师与经典著作

本文永久链接 – https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works

早上送孩子去幼儿园的路上,收到一个小伙伴的微信:

我这才意识到今天是教师节!为人师,自觉还不够格!但在这个特殊的日子,作为IT行业从业人员,我想向那些在计算机科学和编程领域给予我们启迪的“老师们”致敬。这些老师可能不是传统意义上站在讲台前的教育者,但他们通过自己的著作、思想和贡献,通过他们的智慧结晶,为我们指明了方向,为无数程序员的成长之路点亮了明灯。

这里我列举的作者与其著作也都是我个人从大学开始至今在计算机编程学习和实践之路上受到深刻影响的重要参考资料。这些书籍不仅丰富了我的知识,也激发了我对编程的热情和探索精神。每一位作者的独特视角和深入浅出的讲解,都让我在理解复杂概念时受益匪浅。希望也能引起大家的共鸣。

注:计算机领域巨匠甚多,笔者见识有限,不能一一列举,这里仅列出我亲自读过且对我影响深远的作者及其代表作品,至于像唐纳德·克努斯和他的巨著《计算机程序设计艺术》等,由于我并未拜读过,这里也就没有列出。

注:书中的图书封面图片可能并非该书最新版的封面,而是笔者购买时的版本的封面图片。

2. 编程语言

2.1 C语言/Go语言领域

2.1.1 Dennis Ritchie

大一的时候学校开设了C语言编程课,指定谭浩强老师的《C程序设计(第二版)》作为随课教材,当时我特意到大学书店花了银子买了本,并奉为皋臬。

直到我看到清华出版的影印版《C程序设计语言(第二版)》,才发现自己天真了,这本才是真正的“圣经”!

Dennis Ritchie,被誉为”C语言之父”,1983年图灵奖得主(与Ken Thompson同年获得)。他不仅创造了C语言,还与Ken Thompson一起开发了UNIX操作系统。刚刚过去的9月9日是其诞辰纪念日,MIT CSAIL在X上发文纪念了这位计算机先驱和现代编程语言奠基人:

他与Brian Kernighan合著的《The C Programming Language》被亲切地称为“K&R C”,是学习C语言的必读经典,书籍不厚,它以简洁明了的语言介绍了C语言的核心概念(遵循当时的ANSI C89/C90标准),影响了几代程序员。

2.1.2 Brian Kernighan

说完K&R中的R,我们再来说K。K指的是Brian Kernighan,他也是Bell实验室UNIX开发团队的重要成员,是C语言的主要推广者之一,他也是AWK语言中的最后的那个K。和Dennis Ritchie等动不动就是语言之父不同,Kernighan以写作风格闻名。他的写作风格清晰易懂,使复杂的概念变得平易近人,并以一种易于理解和应用的方式呈现给读者。这使得与Dennis Ritchie合著的《C程序设计语言》不仅是C语言语言特性的权威指南,更是编程语言类书籍技术写作的典范,之后很多编程语言类的书籍都参考Kernighan的风格,至少也会先从一个“Hello, World”开始全书的讲解。

其与P.J.Plauger合著的《The Elements of Programming Style》也是程序员眼中的经典。

2015年,已经70高龄的Kernighan又和Go团队的Alan Donovan合著了Go语言编程书籍领域公认的圣经《The Go Programming Language》。这本书与K&R C的风格很相似,作者们以清晰简洁的语言,系统且全面地介绍了Go的语法特性和编程理念,并通过大量的实例展示了Go在实际项目中的应用。书中不仅覆盖了基础知识,还深入探讨了并发编程、unsafe编程等Go高级主题。

2.2 C++

2.2.1 Bjarne Stroustrup

Bjarne Stroustrup是C++语言之父,他从1979年开始,在C语言的基础上添加了面向对象编程等特性,从而创造了C++这门强大而灵活的通用编程语言。C++经过ISO标准化后,他也是C++标准委员会的创始成员,并从那时起一直是一名活跃成员。如今,他还担任负责处理语言扩展提案的小组(进化工作组)的主席。

Bjarne Stroustrup的著作也是我入门和深入C++的必读经典,其中《C++程序设计语言》被认为是C++语言的”圣经”。Stroustrup以语言之父的口吻在书中详细介绍了C++的语言特性、抽象机制、标准库与设计理念。它不仅是一本语言参考,更是理解C++哲学的重要资源。

我是从高教影印版的《The C++ Programming Language (Special Edition)》开始看这本书的,与当时手里的钱能老师所著的《C++程序设计教程》相比,我感觉Stroustrup的The C++ Programming Language简直是在讲述一门新语言。

Stroustrup的另外一本书《The Design and Evolution of C++》是C++进阶的必读之作,国内版译为《C++语言的设计与演化》,这本书可以理解为Stroustrup设计C++背后的心路历程以及设计决策与语言机制:

Stroustrup的书虽好,但读起来有些难度,对初学者可能不那么友好,尤其是The C++ Programming Language,更像是一本C++语言的spec,缺少了像Kernighan那种春风化雨的阅读体验,所以我个人更喜欢下面这位C++大佬的作品。

注:Stroustrup这些年持续更新其作品,甚至还推出了《A Tour of C++》这样的更易读的小册子。

2.2.2 Stanley B. Lippman

Stanley B. Lippman是Stroustrup的同事,早年和Stroustrup一起在Bell实验室开发C++编译器,2001年,Lippman加入微软,成为Visual C++的架构师。他最为人所称道的是他的“一厚一薄”两本C++经典著作。

我们先说这本厚的,它就是C++大部头:《C++ Primer》,这本书分为C++基础、C++标准库、类设计者的工具和高级主题四个部分,非常适合C++初学者,同样其高级主题对于有经验的C++熟手也有很高的价值。

Lippman的另外一本薄书名为《Inside the C++ Object Model》,最初国内中译版《深度探索C++对象模型》由宝岛知名技术作家侯捷翻译,如今的很多新一代程序员可能已经不知道侯捷老师了,他如今依然活跃在C++高级培训的舞台上。

这本书属于C++进阶书籍,Lippman从C++编译器实现者的角度对C++的对象模型、继承和多态的实现机制(比如虚函数表、动态绑定等)等做了深入浅出的讲解,是C++走向高级阶段的必读之作。

不幸的是,Lippman已于2022年仙逝,我们再也看不到他亲自更新C++ Primer了。

2.2.3 Scott Meyers

如果你学过C++,但没有看过Effective C++系列,那我可以肯定你不是C++高手,Scott Meyers的《Effective C++》系列书籍是C++程序员通往高手境界的必读书籍:

这套C++丛书的特色就是以一条条C++准则为单元,每一条都扼要说明了一个可让你写出更好的C++程序代码的方法,并以特别设计过的例子详加讨论,这非常适合程序员的胃口。

2.3 Java

我在工作初期曾经系统学过Java,那时Java刚刚发布5.0,Spring也是方兴未艾。现在看来,没有Spring的Java是那么的纯粹!

学习纯Java,两本书足矣!下面我们就分别来看看这两本书和他们的作者。

2.3.1 Bruce Eckel

Bruce Eckel是著名的C++和Java作家,以其深入浅出的写作风格闻名。我没有将Eckel列到C++范畴,一是因为C++大神太多,二则是因为他的Thinking in Java似乎比他的Thinking in C++影响力更大。

这本书《Java编程思想》被誉为学习Java最全面的资源之一。Eckel以其特有的方式,深入浅出地解释了Java的核心概念和高级特性。书中的例子丰富而实用,帮助读者真正理解和掌握Java编程,并这本书只讲纯Java语法,并不涉及任何框架。读过的朋友,还记得书中那句“Everything is an object”吗!

2.3.2 Joshua Bloch

和Bruce Eckel是一个作家和培训师不同,Joshua Bloch领导了许多Java平台功能的设计和实现,包括Java Collections Framework、java.math包和断言机制等,对Java语言和库的发展做出了重要贡献。他曾在Sun Microsystems担任杰出工程师。2004年他离开Sun,成为Google首席Java架构师。

和Bloch为Java实现做出的贡献相比,他的书籍在Java界更是“家喻户晓”,他曾自己或与其他人合著过多本Java书籍,包括Java Puzzlers、Java Concurrency In Practice以及Effective Java。而最后的《Effective Java》更是成为了Java程序员几乎人手一本的神作:

这本书提供了编写高质量Java代码的最佳实践。Bloch基于自己丰富的经验,提出了许多实用的建议,涵盖了从基本的编程习惯到高级主题如并发和序列化,其中每条建议都值得大家细致琢磨品味。这本书帮助无数Java程序员提升了代码质量和效率。

3. 算法与数据结构

程序员,永远绕不开算法与数据结构。在算法与数据结构领域,Donald E. Knuth无疑是祖师爷级别的,他写的多卷本大部头的“计算机程序设计艺术”被多少人买回后顶礼膜拜,却不曾拆封拜读:)。

更多人和我一样,喜欢更为实用的,能看懂的书籍资料。

3.1 Robert Sedgewick

首先我们来看Sedgewick和Wayne合著的作品:《算法(第4版)》。

Robert Sedgewick是Donald E. Knuth的学生,名门之后,从1985年开始一直担任普林斯顿大学计算机科学系教授,曾任该系主任。很多耳熟能详的数据结构和算法都是Sedgewick发明的,比如红黑树、三元搜索树等。他基于课程讲义编写的这本“算法”,以清晰的讲解和丰富的Java实现而闻名。该书不仅介绍了经典数据结构和算法,还着重讨论了算法在实际问题中的应用。书中包含了大量的图示和代码,使得复杂的算法概念变得易于理解。这本书适合从入门到进阶的各个阶段的读者,是算法学习的必备参考。不过你不要想一下吃透这本书,很多算法非常深奥,可以将其作为案头的参考书,常看常新。

Sedgewick曾出版过多本算法书籍,有C实现的,有C++实现的,大家可以根据自己需要选择不同的实现版本。

3.2 Thomas H. CormenCharles E. Leiserson

提到算法,就不能不提到另外一部大部头的经典著作《算法导论

这部作品的英文版有上千页,可谓是算法领域的“百科全书”,这本书由 达特茅斯学院计算机科学系教授Thomas H. Cormen、麻省理工学院计算机科学与电气工程系教授Charles E. Leiserson等四人共同完成。这本书既全面又严谨,因此啃起来非常有难度,我在大学时期就买了该书的高教出版社的影印版,至今过去了十余年,我也没有完成全书的阅读:(。

在国内数据结构领域不得不说的另外一本教材是清华大学出版社出版的、由严蔚敏和吴伟民两位老师合著的《数据结构(C语言版)》,因很多高效将其作为考研指定教材,因此这本书的市占率很高,大家可以结合前面两个外版教材一起学习,效果可能更佳。下图是当年我购买时的版本样式:

4. 软件工程与编程思想

从大学毕业,入职工作后,软件工程知识必不可少,下面这些经典著作可以帮助大家快速融入工程领域。

4.1 Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides

这四位博士都是国际公认的面向对象软件领域的专家。他们在1994年合著的开创性的书籍《设计模式:可复用面向对象软件的基础》成为了开发人员在工程领域的必读之作,其影响力之广泛在整个IT领域都能排在TOP10。

这本书定义并系统化了软件设计中的常见模式,为面向对象设计提供了一套通用词汇和最佳实践。书中详细描述了23种设计模式,并通过实例说明了它们的应用场景。这本书不仅影响了无数程序员的设计思想,也为软件工程领域提供了宝贵的参考。这四位博士的工作对软件设计模式的研究和应用产生了深远的影响。

4.2 Steve McConnell

Steve McConnell是软件工程实践领域的权威专家,他的著作有不少,包括《Code Complete》、《Rapid Development 》和《Software Estimation》等,都对提高代码质量和开发效率有着重要影响。而其中影响力最大的莫过于《代码大全(第2版)》:

这是一本软件构建实践的百科全书,它涵盖了从变量命名到软件架构的各个方面。McConnell以丰富的经验和洞察力,提供了大量实用的编程技巧和最佳实践。这本书不仅适合新手学习,也是有经验的程序员提升技能的重要资源。并且,书中所讲的各种技巧和实践几乎与编程语言无关,无论你擅长哪种语言,都能从中获益!

4.3 Robert C. Martin(Uncle Bob)

Robert C. Martin,昵称”Uncle Bob”,是敏捷开发运动的重要推动者,也是软件工艺的倡导者。他的著作颇多,包括敏捷软件开发、敏捷整洁之道、代码整洁之道、匠艺整洁之道等。最近刚刚上市的《函数式设计》也出自Bob大叔之手。

在他的诸多作品中,《敏捷软件开发:原则、模式与实践》对我的影响更为深刻。

在这本书中,Martin详细阐述了敏捷开发的核心原则(SOLID原则),并通过大量的案例研究和设计模式,展示了如何在实践中应用这些原则。这本书不仅介绍了技术层面的最佳实践,还深入探讨了敏捷开发对团队协作和项目管理的影响。

4.4 Andrew HuntDavid Thomas

Hunt和Thomas是两位经验丰富的软件开发者,他们的著作强调了持续学习和改进在程序员职业生涯中的重要性。他们共同开创了Pragmatic Programmer的概念,并通过其大作:《程序员修炼之道:从小工到专家》为开发人员讲述具体实践的方法:

这本书强调了在软件开发中保持务实态度的重要性。作者们通过一系列小贴士和练习,涵盖了从个人责任到知识投资等多个方面,帮助程序员不断提升自己的技能和职业素养。

4.5 Frederick P. Brooks Jr.

谈到软件工程,我们不能忘记一个人,他就是Frederick P. Brooks Jr.。Brooks是一位美国计算机架构师、软件工程师和计算机科学家,以管理IBMSystem/360系列大型机和OS/360的开发而闻名。他在其开创性著作《人月神话》中坦率地写下了这些开发和项目管理经历,对后续的软件工程领域产生了深远的影响:

这本软件工程的经典之作挑战了许多关于软件开发的传统观念。Brooks通过自己在IBM的经历,深入探讨了大型软件项目管理中的各种问题。尽管首次出版已经过去多年,但书中关于团队沟通、项目规划和概念完整性等方面的见解至今仍然适用,是每个软件项目管理者入门必读的著作。

5. 计算机系统

最后,我们看一下计算机系统领域,我将系统编程、网络编程、编译器、数据库、操作系统统统放到这个领域一起说明了,排名不分先后:)。

5.1 Randal E.Bryant

Randal Bryant是一位美国计算机科学家和学者,因其在形式验证数字硬件和软件方面的研究而闻名。Bryant自1984年以来一直在卡内基梅隆大学任教。2004年至2014年,他担任卡内基梅隆大学计算机科学学院(SCS)院长。他长期从事本科生和研究生计算机系统方面课程教学近40年。他和David O’Hallaron教授一起在卡内基梅隆大学开设了15-213课程“计算机系统导论”,其《深入理解计算机系统》便是以这门课的讲义为基础撰写而成的:

这本书涵盖了计算机系统的多个层面,包括硬件、操作系统、编程语言和网络等,使读者对计算机的整体架构有深入的理解。对于计算机专业入门的学生而言,这本书是必读的教材,国内尚没有类似的教材能望其项背!当年如果早早能看到这本教材该多好啊!

5.2 W. Richard Stevens

Richard Stevens是UNIX和网络编程领域的权威专家,也是我顶礼膜拜的大神,他的著作对系统级编程产生了深远的影响。在我工作后的若干年内,Stevens的作品是我理解Unix/Linux系统编程的必备参考,并全部购买收藏,随时翻阅。更神奇的是,他的每一部作品都是上乘之作,看下面的豆瓣评分:

-《UNIX环境高级编程》

这本书被誉为UNIX编程的”圣经”。Stevens深入浅出地解释了UNIX系统调用和库函数的使用,涵盖了文件I/O、进程控制、信号处理、线程等关键主题。这本书不仅是学习UNIX/Linux系统编程的必备参考,也为理解操作系统内部工作原理提供了宝贵的见解。

-《UNIX网络编程》(卷1:套接字联网API,卷2:进程间通信)

相对于Unix环境高级编程的全面和总括,这套书深入具体领域,重点覆盖了UNIX环境下的网络编程和进程间通信技术。第一卷重点讲解了TCP/IP协议族和套接字编程,第二卷则专注于UNIX系统上的各种IPC(进程间通信)机制。这套书不仅提供了详细的技术讲解,还包含了大量的实例代码,是网络编程学习和实践的必备参考。

-《TCP/IP详解》系列

这套书深入浅出地解释了TCP/IP协议族的工作原理,从协议的基本概念到复杂的实现细节,为读者呈现了一幅完整的TCP/IP知识图谱。这套书不仅适合网络程序员阅读,也是理解现代互联网技术基础的重要资源。

对于Stevens的这些书,虽然年代已久,但对如今的后端/系统程序员依然有极大的参考价值,建议大家必读。

5.3 Alfred V. Aho, Monica S. Lam, Ravi Sethi和Jeffrey D. Ullman

以Alfred V. Aho为代表的这几位作者都是编译器理论和实现的权威专家,他们的著作被誉为编译原理领域的”圣经”。Alfred V. Aho同时也是AWK语言中的那个”A”,他还著有《计算机算法的设计与分析》。当然“龙书”是其在学术领域著作的最卓越代表,学编译原理的同学建议人手一本。

这本书以其全面性和深度在编译器领域独树一帜。从词法分析、语法分析到代码优化,书中详细讲解了编译器设计的各个环节。虽然以理论为主,但书中也包含了大量的实例和练习,帮助读者将理论付诸实践。这本书不仅是编译器开发者的必读之作,对理解程序语言的设计和实现也有重要帮助。国内各大开设编译原理课程的重点高校也都将其作为第一教材。国内一些高校也编写了一些自己的教材,但与这本“龙书”相比,level还是差距很大。

5.4 Abraham Silberschatz

Avi Silberschatz是一位以色列计算机科学家和研究员,曾在bell实验室工作过,他因在计算机科学领域撰写了许多有影响力的著作而闻名,尤其是操作系统和数据库系统方面。其作品《数据库系统概念》《操作系统概念》被全世界的高校计算机专业所采用。

-《数据库系统概念》

本书由Abraham Silberschatz、 Henry F. Korth和S. Sudarshan合著,这三位作者都是数据库系统领域的专家,他们的著作被广泛用作大学教材和专业参考。这本书全面介绍了数据库系统的基本概念、设计原理和实现技术。从关系代数到事务处理,从查询优化到分布式数据库,书中涵盖了传统和现代数据库技术的各个方面。无论你是在校数据库专业的学生,还是从事数据库核心系统开发的工程师,亦或是数据库应用开发的程序员,本书都极具参考价值,可放置在案头随时查看。

-《操作系统概念》

本书由Abraham Silberschatz, Peter B. Galvin 和Greg Gagne合著,这几位作者都是操作系统理论和实践的专家,他们的著作在学术界和工业界都有广泛影响。

这本书以其全面性和深度成为了操作系统学习的重要参考。从进程管理到分布式系统,从内存管理到安全性,书中详细讨论了操作系统的各个方面。作者们不仅介绍了理论知识,还通过案例研究展示了这些概念在实际系统中的应用。这本书适合从入门到进阶的各个阶段的读者,是理解现代计算机操作系统工作原理的关键参考材料。

6. 小结

在教师节这个神圣的日子中,我们回顾了这些在计算机科学和编程领域做出杰出贡献的”老师们”。他们的智慧和洞见,通过这些经典著作,影响了几代程序员的成长,更是对我的程序员生涯提供了莫大的帮助。

这些大师们不仅仅传授了技术知识,更重要的是,他们塑造了我们思考问题和解决问题的方式。从C语言到Go,从算法到软件工程,从操作系统、编译原理到网络编程等,这些著作涵盖了计算机科学的方方面面,构建了现代软件开发的知识体系。

作为程序员,我们应该心怀感激,因为我们站在了这些巨人的肩膀上。同时,我们也要记住,学习是一个终身的过程。技术在不断进步,新的挑战不断出现,但这些经典著作中蕴含的智慧将永远指引我们前进的方向。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

聊聊Go与依赖注入

本文永久链接 – https://tonybai.com/2023/09/28/dependency-injection-with-go

如果你读过Robert C. Martin《敏捷软件开发:原则、模式与实践》(书的封皮见下图),那么你一定知道经典的SOLID设计原则中的“D”:依赖倒置原则(Dependency Inversion Principle, DIP)。

依赖倒置原则是面向对象设计中的基本原则之一,它阐述了高层模块和低层模块的依赖关系应该倒置(如下图),也就是:

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象

依赖倒置原则实际上就是对控制反转(Inversion of Control,IoC)这一概念的阐述,而依赖注入(Dependency Injection)是实现控制反转的一种机制。所以可以说,依赖倒置原则是设计级的指导思想,它提出了正确的依赖关系;而依赖注入是实现级的具体设计模式,它将组件的依赖关系控制权移到了外部,实现了组件之间的解耦,是对依赖倒置原则的一种实现手段。

依赖注入可以帮助你开发出松耦合的代码,松耦合使代码更易于维护

《Go语言包设计指南》一文中,我们提到过:在Go中,耦合发生在包这一层次。而在Go代码层面最低的耦合是接口耦合。在Go中,接口的实现是隐式的,即a包实现b包中定义的接口时是不需要显式导入b包的,我们可以在c包中完成对a包与b包的组装,这样c包依赖a包和b包,但a包与b包之间没有任何耦合。那么负责组装a包与b包的c包能否在代码层面消除掉对a和b的依赖呢?这个就很难了。不过我们可以使用依赖注入技术来消除在代码层面手动基于依赖进行初始化或创建时的复杂性,在中大型的程序中,依赖注入的优点更能得到体现。

在这篇文章中,我们就来聊聊Go中依赖注入可以解决的问题,并初步认识一下两个在Go社区认可度较高的Go依赖注入框架。

1. 手动注入

我们先建立一个符合DIP原则的例子,其依赖关系如下图:

这里有三个“模块”,从高到低分别为Service、BussinessLogic和DatabaseAccess。Service是一个接口,其实现ServiceImpl依赖BussinessLogic接口。Business是BussinessLogic的实现,它还依赖DatabaseAccess接口。Database则是DatabaseAccess接口的实现。

围绕这一示例,我们分别用手动组装和依赖注入框架演示一下如何实现注入,先来看一下手动组装与注入。

下面是示例的项目结构布局:

./manual
└── demo/
    ├── Makefile
    ├── business/
    │   └── business.go
    ├── database/
    │   └── database.go
    ├── go.mod
    ├── main.go
    └── service/
        └── service.go

manual/demo目录下的service、business和database包下面包含了导出的接口与其具体实现的定义。这里将这些包的代码列出来,这些代码在后续应用依赖注入工具的示例中也是保持不变的:

// dependency-injection-examples/manual/demo/service/service.go

package service

import "demo/business"

// Service interface
type Service interface {
    HandleRequest() string
}

// ServiceImpl struct
type ServiceImpl struct {
    logic business.BusinessLogic
}

// Constructor
func NewService(logic business.BusinessLogic) *ServiceImpl {
    return &ServiceImpl{logic: logic}
}

// Implement HandleRequest()
func (s ServiceImpl) HandleRequest() string {
    return "Handled request: " + s.logic.ProcessData()
}

// dependency-injection-examples/manual/demo/business/business.go

package business

import (
    "demo/database"
)

// BusinessLogic interface
type BusinessLogic interface {
    ProcessData() string
}

// Business struct
type Business struct {
    db database.DatabaseAccess
}

// Constructor
func NewBusiness(db database.DatabaseAccess) *Business {
    return &Business{db: db}
}

// Implement ProcessData()
func (b Business) ProcessData() string {
    return "Business logic processed " + b.db.GetData()
}

// dependency-injection-examples/manual/demo/database/database.go

package database

// DatabaseAccess interface
type DatabaseAccess interface {
    GetData() string
}

// Database struct
type Database struct{}

func NewDatabase() *Database {
    return &Database{}
}

// Implement GetData()
func (db Database) GetData() string {
    return "Data from database"
}

service.Service是直面client的接口。于是在main函数中,我们实例化一个Service的实现并传给Client,后者调用Service的HandleRequest方法触发全流程。service.NewService的调用依赖一个实现了business.BusinessLogic接口的实例,我们在调用NewService之前还需要调用business.NewBusiness创建一个实现了business.BusinessLogic接口的实例;business.NewBusiness的调用依赖一个实现了database.DatabaseAccess接口的实例,我们在调用NewBusiness之前需要调用database.NewDatabase创建一个实现了database.DatabaseAccess接口的实例。

这就是手工组装的现实:我们要记住“模块”间的依赖关系,并手动创建对应实例以满足这种依赖。下面是main函数的代码:

// dependency-injection-examples/manual/demo/main.go

package main

import (
    "demo/business"
    "demo/database"
    "demo/service"
    "fmt"
)

// Client struct
type Client struct {
    service service.Service
}

// Constructor
func NewClient(service service.Service) *Client {
    return &Client{service: service}
}

// Call service
func (c Client) MakeRequest() string {
    return "Client request: " + c.service.HandleRequest()
}

func main() {
    // make dependency injection manually
    db := database.NewDatabase()
    busi := business.NewBusiness(db)
    svc := service.NewService(busi)
    client := NewClient(svc)

    fmt.Println(client.MakeRequest())
}

编译运行上述示例的结果如下:

$cd dependency-injection-examples/manual/demo
$make
$./demo
Client request: Handled request: Business logic processed Data from database

这种为了满足依赖而进行的手工实例创建的行为,在一些小型或演示型程序中还可以自诩为straightforward,但在拥有上百个包的大型程序中,这种为了组装而进行的创建行为就会因多点发生、依赖众多而显现出“复杂性”和难于维护。为了保持代码的松耦合还要降低组装创建行为的复杂度,依赖注入工具被引入,并且往往代码库越庞大,引入DI的好处就越发明显。松耦合带来的好处并不总是立竿见影,但随着时间的推移,随着代码库复杂性的增加,这些好处就会变得显而易见。

注:大家不要进入这样的误区:“采用依赖注入工具的代码就一定是符合DIP原则的松耦合的代码”。至少在Go中,不符合DIP原则的代码(比如没有建立接口抽象)也可以使用依赖注入工具来进行依赖的创建和模块间的组装。

Go社区(尤其是一些大厂)提供了一些Go依赖注入工具,比如:Google wireuber Fxfacebook inject等。这些工具大致可分为两类,一类是利用代码生成技术的编译期依赖注入,另一类则是利用反射技术的运行时依赖注入

下面我们分别以编译器依赖注入的Google wire和运行时依赖注入的uber fx为例来看看如何通过依赖注入工具来完成依赖模块的组装(assembly)。

注:facebook的inject已经public archived;google wire目前的开发也不是很active,wire团队给出的理由是要保持wire足够简单并认为从v0.3.0开始,wire已经是功能特性完备的了,目前不接受新feature,仅接受bug报告和修复的补丁pr。只有uber的fx还处于非常积极的开发状态,uber宣称fx是经过uber生产验证的:uber几乎所有的Go服务都是建立在Fx基础之上的。

2. google/wire:编译期的依赖注入

wire是由Google Go Cloud开发包团队于2018年下旬开源的Go编译期依赖注入工具,与uber fx、facebook的inject等使用反射在运行时注入不同的是,wire灵感来自Java的Dagger 2,使用的是代码生成技术,而不是反射或服务定位器(service locator)技术。

相较于运行时依赖注入,编译期间注入的最大好处就是生成的依赖注入和组装的代码是对你可见的,没有任何背后的“魔法”。这便于在编译期捕捉到注入过程的错误,也便于代码的调试。

此外,wire团队认为编译期注入可以避免依赖膨胀。Wire生成的代码只会导入所需的依赖项,因此,你的二进制文件不会有未使用的导入。运行时依赖项注入在运行时之前无法识别未使用的依赖项。

下面我们就用wire注入来改造一下上面的示例。

注:安装wire命令为go install github.com/google/wire/cmd/wire@latest 。

相对于manual那个示例,我们在main包下面增加一个新文件wire.go:

// dependency-injection-examples/wire/demo/wire.go

//go:build wireinject
// +build wireinject

package main

// wire.go

import (
    "demo/business"
    "demo/database"
    "demo/service"

    "github.com/google/wire"
)

func InitializeService() service.Service {
    wire.Build(service.NewService,
        wire.Bind(new(service.Service), new(*service.ServiceImpl)),
        business.NewBusiness,
        wire.Bind(new(business.BusinessLogic), new(*business.Business)),
        database.NewDatabase,
        wire.Bind(new(database.DatabaseAccess), new(*database.Database)),
    )
    return nil
}

我们看到wire.go中提供了一个InitializeService函数,用于为main函数中的Client实例提供一个service.Service接口的具体实现。但是在这个函数中我们并没有像manual中那样手工调用NewService等来创建实例,我们仅仅是将各个“模块”Service、BussinessLogic以及DatabaseAccess的实例的创建函数传给了wire.Build函数。另外我们看到wire.go这个源文件使用了build tag,这个文件仅仅是用于代码生成,并不会参与到最终的代码编译过程中,这也是InitializeService函数的返回值随意设置为nil的原因,这个nil在代码生成过程中会被忽略并替换掉。

注:为什么要使用wire.Bind?我们示例中的各个模块的NewXXX函数接受的参数都为接口类型,返回的都是具体的类型实例,这符合Go的惯例。但如果不使用wire.Bind,wire将无法知道NewXXX依赖的接口类型参数该如何创建!通过wire.Bind告诉wire某个接口类型参数,比如service.Service,可由创建如*service.ServiceImpl的类型替代。关于Binding Interfaces的具体介绍,可以参考wire官方文档。

接下来,我们就可以通过wire命令生成代码,完成注入过程:

$cd dependency-injection-examples/wire/demo
$wire
wire: demo: wrote /Users/tonybai/Go/src/github.com/bigwhite/experiments/dependency-injection-examples/wire/demo/wire_gen.go

wire工具基于wire.go生成了wire_gen.go文件,在该示例中,wire_gen.go的内容如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
    "demo/business"
    "demo/database"
    "demo/service"
)

// Injectors from wire.go:

func InitializeService() service.Service {
    databaseDatabase := database.NewDatabase()
    businessBusiness := business.NewBusiness(databaseDatabase)
    serviceImpl := service.NewService(businessBusiness)
    return serviceImpl
}

看一下wire生成的代码,和我们在manual中手动组装的代码基本是一样的。基于这份代码,我们调整一下main函数,主要是去掉手动组装的过程,改为直接调用InitializeService:

// dependency-injection-examples/wire/demo/main.go

func main() {
    // make dependency injection by code generated by wire
    svc := InitializeService()
    client := NewClient(svc)
    fmt.Println(client.MakeRequest())
}

运行一下wire注入这个demo,其结果与manual demo是一致的:

$cd dependency-injection-examples/wire/demo
$make
$./demo
Client request: Handled request: Business logic processed Data from database

关于wire,这里仅是作了“浅尝辄止”的介绍。要想深入了解wire的功能特性,可以阅读Wire tutorialWire User Guide

接下来,我们再来看看如何使用uber/fx来实现依赖注入。

3. uber/fx:运行时的依赖注入

如果我没记错的话,uber应该是先开源的dig,再有的fx。dig是基于反射的依赖注入工具包,而fx则是由dig支撑的依赖注入框架。对应普通Go开发者而言,直接使用fx就对了。

下面是使用fx实现上面示例依赖注入的代码,我们只需要改造一下main.go:

// dependency-injection-examples/fx/demo/main.go

func main() {
    app := fx.New(
        fx.Provide(
            fx.Annotate(
                service.NewService,
                fx.As(new(service.Service)),
            ),
        ),
        fx.Provide(
            fx.Annotate(
                business.NewBusiness,
                fx.As(new(business.BusinessLogic)),
            ),
        ),
        fx.Provide(
            fx.Annotate(
                database.NewDatabase,
                fx.As(new(database.DatabaseAccess)),
            ),
        ),

        fx.Invoke(func(svc service.Service) {
            client := NewClient(svc)
            fmt.Println(client.MakeRequest())
        }),
        fx.NopLogger, // no fx log output
    )

    app.Run()
}

我们在main函数中,使用fx.Provide注册了所有依赖类型的实例的构造方法(NewXXX),然后将我们要执行的代码放入一个匿名函数,并传给fx.Invoke。当我们运行程序时,fx会在内存中构建对象调用依赖图,并使用Provide中注册的类型实例的构造方法构造实例,完成依赖注入和代码组装,然后运行传给Invoke的函数。

在向fx.Provide传递NewXXX时,我们使用了fx.Annotate,其目的与在wire示例中使用wire.Bind一样,即将一个类型实例转换为接口类型,以满足参数为接口类型的NewXXX的依赖所需。关于fx.Annotate的详细说明,可参考fx的官方文档。

上述使用fx示例还有两处要提及一下,一个是使用fx.NopLogger关闭fx框架自身的日志输出;另外一个则是上述示例run起来后并不会自动退出,只有当按下ctrl+c后,程序才会因收到系统退出信号而退出!

对比fx和wire,你可能也发现了这样一点:fx将很多工作放到了“背后隐蔽处”,如果你不了解fx框架的运行机理,你很难使用好fx框架;而wire生成的代码就是编译到程序中的代码,没有额外的“魔法”。

当然fx不仅提供了Provide、Annotate、Invoke,其他一些功能特性大家可以自行到官方文档阅读并理解使用。

4. 小结

依赖注入常用来解决软件模块之间高度耦合的问题。传统的程序设计中,一个模块直接new或者静态调用另一个模块,这使得模块之间产生了强耦合。依赖注入将模块创建和注入的控制权移交给外部,由外部动态地将某个实现类实例注入到需要它的模块中。这样实现了模块之间的松耦合。

如果你来自Java等面向对象编程语言的群体,你对依赖注入肯定不陌生。

但是在Go社区,我觉得依赖注入并非惯用法。Go社区很多人崇尚“You often don’t need frameworks in Go”这样的信条。但凡引入一个框架,都会带来学习和理解上的额外负担,Go依赖注入框架亦是如此。

究竟是否使用依赖注入,完全取决于你在开发过程中的权衡和取舍。

如果你决定使用依赖注入,wire和fx都是你可选择的框架。就目前情况来看,fx是目前开发最active、历经生产考验最多的Go依赖注入框架,不过要想用好fx,必须深入理解fx的运行机制和底层原理,这又会带来一定的学习负担。

本文涉及的Go源码,可以在这里下载。

5. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats