编写可复用的服务端软件系统应该注意的五个重要细节

编写可复用的服务端软件系统应该注意的五个重要细节

作为程序员,我们往往希望自己写的代码能被最大程度的重用,但是我们依然能看到有很多“被重复发明的轮子”,其原因往往只是一个简单细节没有考虑到位。所以我就希望能总结一些这些容易被忽视的细节:

1 安装部署方面的细节

1.关于安装

很多软件进程、库的安装都比较繁琐,比如那些从源代码编译的软件,或者需要依赖很多第三方库的软件库,都会让使用者望而生畏。正确的做法应该是,把下载下来的压缩包,解压开就直接可以运行或者使用。例子有Eclipse软件。要做到这点,需要对于整体软件的依赖环境进行统一规划,否则自己都搞不清楚整体依赖状况,很难做到在压缩包里面去准备这些东西。现在最流行的Docker也是一个很好的解决方案。

2.关于配置文件位置

很多软件强大的配置软件让人印象深刻,比如Apache,MySQL,PHP这类,但是,他们都有一个共有的特点,就是都自带提供了默认配置。很多时候我们自己写的软件,常常会忘记在压缩包里面准备一份常用的配置文件,或者需要用户拷贝某个配置文件到某个指定目录才会生效。这样的配置文件操作往往让用户失去耐心。正确的做法应该是,在压缩包目录中,就带有了默认配置文件,解压开就直接应用这个配置文件。

3.多进程系统

作为分布式软件系统,往往都是多进程的。而用户去配置和启动多个进程,往往又是最复杂的。因为各个进程之间的关系需要在配置文件中正确登记,然后以正确的参数启动这些进程。有时候这种事情就连熟练的开发人员都一筹莫展。正确的做法是,提供一个可以在单机上直接运行的版本,作为默认的运行配置。这样用户只需要简单的启动操作,就可以完成功能的试用,然后在这个环境下,逐步尝试搬迁一些进程到其他服务器上。

4.集群系统的配置参数

我们往往喜欢把服务器IP列表作为配置文件放在系统中,但是这对于使用者来说是一个沉重的负担,因为如果写错了这些配置,往往只是会发现工作不正常,而不知道什么原因。对于部署这些系统的运维人员,在每次加入和退出机器的时候,修改这些IP也是个繁琐的工作。正确的做法应该是,用一个IP或域名来代表整个集群,而这个IP或域名的机器上运行集群的目录服务;集群中其他服务器,通过代码自动获得IP地址,上报给这个目录服务进程,从而获得整个集群的地址配置。这样复杂的集群配置就变得高度自动化,只需要配置一个中心节点地址即可。

5.错误提示

在陌生的软件启动出错,或者库初始化出错的时候,我们常常看不懂,甚至找不到出错信息。又或者完全不知道该如果处理。我们自己写程序也往往在出错的时候,就简单的描述出错的情况拉倒。但是,正确的做法应该是,对于启动、初始化错误,除了写日志外,应该在stderr打印一些文字,这些文字应该提出修正错误的可行方案。比如某个配置需要如何填写,某个进程需要先启动,或者使用某个发布包的脚本来修复。

6.运行环境

我们的软件在启动和初始化的时候,常常默认了环境是我们的预设环境,甚至是开发环境。比如我们会默认服务器安装了expect shell或者python shell。或者默认我们需要的进程已经启动,比如MySQL和Redis。或者某个共享内存已经建立好。这些依赖常常在不熟悉的使用者手上,难以自己去完全准备好。所以正确的做法应该是,尽量在启动脚本或者初始化过程中,尝试自己修复这些问题,比如自己去建立共享内存,启动所需的进程,安装需要的工具。这显然会增加很多开发量,但是这会大大提高产品的易用性。

2 关于配置参数

01

我见过很多系统,因为一个看起来不起眼的参数没配对就死活启动不了,或者修改了某个参数就运行失败。而这些复杂的参数对于我来说毫无意义,大部分都是一些基于实现的性能参数和预分配资源参数,这会让我非常恼火,在试用阶段就放弃使用。正确的做法是,为每个参数都配上默认参数。比如IP地址是取本地eth0网卡的地址,或者直接就是127.0.0.1。

02

我们往往系统于编写SHELL脚本来管理和控制我们的linux服务器系统。这会提供非常好的灵活些。但是有些系统的参数,必须要修改配置文件中的一个项目,这就让我们写的脚本异常复杂,需要用到sed或者其他一些文件处理程序来“生成”配置文件。正确的做法是,让命令行参数可以覆盖任何一个配置项目。这样我们就能简单的使用脚本来做任何事情。

03

当我们在用IDE,或者在开发机上开发时,对于编译和运行测试的环境来说,配置文件和命令行参数都一个不太方便的选择。我们可能要在某个机器上共享一些配置,或者要编写多进程并行的环境。正确的做法是,可以让环境变量来覆盖配置文件的任何一个项目,而命令行则可以覆盖环境变量的设定。这样我们就能拥有绝对零活的环境配置工具。

04

如果我们在开发一个库,而代码里面有很多需要输入的“常量”或者“配置项”,我们就要在初始化参数和配置文件中做选择。正确的做法应该是,根据这些输入“数据”所在的代码层次来决定。这些输入“数据”如果是这块代码的主要功能相关的,就应该成为函数中的参数,否则应该成为配置项。比如对于一个读取INI格式文件的库来说,文件路径应该是参数;而对于一个网络服务器来说,INI文件路径就应该是一个配置项。

3 关于日志的使用

01

我们写程序,在碰到检查出严重问题的时候,都会打印日志,但是往往忘了打印stderr,这样会造成用户不能立刻发现问题。所以我们应该在fatal级别以上的信息都同步输出一些到stderr中。

02

我们对于日志目录的设置,往往五花八门,有的是喜欢放到/var/log下面,有的则需要用户去配置。但是最容易找到的,是在软件的安装目录下的log/目录。这对于可重用的软件库尤为重要,我们往往因为无法简单的获得软件的安装目录,就放弃写到相对路径的log/下面。实际上我们甚至可以用/proc系统,或者getcwd,或者使用启动脚本来获得。

03

我们的Error和Warning日志往往代表了某种运行时的问题,而这些问题正是用户最关心的问题——运行状态。因此我们要在出现问题的时候,仔细规划这些输出,不要把一些无关紧要的事情也输出出来。而是最好应该把和系统的输入所产生的错误以Error, Warning输出。这样用户就能明确的知道问题的原因。

4 关于使用方法和接口函数API

01

我们提供的可重用方法,是通信协议,还是API库?这永远是个可以争论的问题,但是有一条是可以确定的:通信协议方便在不同OS和语言间使用,而API更方便具体的语言开发。我更倾向于首先提供API,因为这可以让用户自己对可重用代码的进程情况和内存情况做自定义的设置,这可以大大提高代码可以组合出的新功能。而API库可以提供比通信协议更多的语言特性,比如回调、继承、工厂等等。

02

对于可重用代码的使用方法,实际上有很多“流派”。但最糟糕的是没有“流派”,使用方法完全需要根据例程或者手册来用。这在纯C的函数库中是最常见的。比如文件操作,你需要先获得一个句柄,然后用这个句柄读、写,等等。JAVA对象的操作方法一般比较确定,就是调用构造器或工厂方法,获得一个对象,然后就可以自由的调用此对象的任何方法。一切调用顺序完全由各种“获得对象”的方法来规定,只要研究一下API手册就可以明白应该如何用。OC对象则稍微比JAVA复杂一点,需要用 alloc+init的双层调用来构建对象,以init()函数作为标准的工厂方法来设计使用顺序。相对的,很多C++的库也遵循这个原则,先初始化,然后调用Init()方法,差别之处在于你还需要想办法自己管理所有的对象和指针,因为大部分的函数都是用输出参数的方式返回结果。因此,不管怎样,应该让API在对象构建上有一个默认的规则,同时以构建方法API来规范用户的使用,这样可以显著降低用户的学习成本。

03

错误处理是影响可重用代码的一个重要细节。一般来说我们有三种手段对付错误:一是通过返回错误码,二是抛出异常,三是制造一次崩溃。对于JAVA或者其他虚拟机语言来说,异常是首选手段,因为可以记录调用栈,可以方便的从异常中恢复过来,同时不丢失异常信息。而对于C++程序来说,异常几乎没有什么用处,而使用错误码,则可能强迫所有调用带错误码的函数,都也返回错误码,而且需要每层调用都检查错误码,代码中遍布了各种if(ret == -1)之类的代码,影响可读性的同时,往往这种错误处理还没啥用处,因为最后不是往上继续返回这个错误码,就是打一行日志拉倒。所以在因为用户错误使用函数(参数、功能)的时候,还不如制造一次崩溃,这样可以让gdb来显示整个调用栈,从而提示用户的代码在何处错用了这个库。

04

名字空间的使用。名字空间虽然无法根本控制用户对预设代码的违反约定的调用,但是还是一个 有力的提示,比如二层空间(name1::name2::InnerClass)的代码是不希望被调用的。而且名字空间还可以用来标志软件的模块,这对用户也是一种 有益的提示。所以我们不要仅仅把名字空间看成是解决名字冲突的手段,而应该从对用户的教育角度上来使用它。

05

Doxygen文档。我们应该努力的维护这个文档,在所有的文档中,最需要投入精力的就是这些和源代码一起生成的注释(文档)。如果养成了随时修改这些注释的习惯,我们就无需额外去花时间整理API手册。当然这个文档的用处,也受本身API设计的限制。如果你的代码中充斥着大堆大堆没有规划过的函数,就算这些API文档再完善,也没有人能学会如何使用你的代码。正确的做法应该是先规划出供重用的接口,然后重点维护他们的注释,以便生成精悍有用的API手册。

5 软件架构

01

遵循分层原则,能得到灵活而且强大的接口。这需要我们的代码耦合是严格符合规范的:不跨层调用,不逆层调用,尽量重用自己的底层代码。要做到这些,需要反复考虑用户可能使用的场景,以及每一层,每一个模块的封装性。有时候权衡这些,比写功能实现更加折磨人。

02

对数据做抽象,建立结构模型。而不是使用指针+长度的buffer来解决一切。写C出身的人会自然的认为“一切都是数字”,所以只有数字的格式和数字内容的差别。而这种用法是难以提供清晰易学的接口的,因为人类天然的思维是对象式的,类型式的。否则C语言只提供一种char类型就可以了,反正所有的功能都可以用数字来自己完成。

03

为每个子模块都建立测试代码。这能让你检查自己模块的依赖性,如果测试代码太难编译成功或者难以在不同环境下运行,就证明了有易用性的问题需要解决。这是一种内省的开发方式,除了能减少BUG以外,还能切实以身作则的提供易用性,因为作者就是第一个使用它的人。

原文发布于微信公众号 - 韩大(handa1740168)

原文发表时间:2015-12-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户1191492的专栏

JClouds的命令行界面

我已经使用JCloud(一种面向Java支持多种云的工具集)一年了。到目前为止,我已经在很多领域广泛地使用了JCloud,特别是在Fuse Eco...

2429
来自专栏云计算

JClouds的命令行界面

我已经使用jclouds一年多了,也一直为它的进步做贡献。目前为止,我已经在很多领域广泛地使用它,特别是在 Fuse Ecosystem 。总之,它是一个特别棒...

1827
来自专栏大数据和云计算技术

mesos开发指南

1 frameworks开发指南 这个文档中,我们称Mesos的应用为”frameworks”。 In this document we refer to Me...

36615
来自专栏微信公众号:Java团长

版本号命名指南

从上可以看出,不同的软件版本号风格各异,随着系统的规模越大,依赖的软件越多,如果这些软件没有遵循一套规范的命名风格,容易造成 Dependency Hell。所...

451
来自专栏我是攻城师

图形数据库之Neo4j学习(一)

3145
来自专栏CodeSheep的技术分享

利用K8S技术栈打造个人私有云(连载之:K8S环境理解和练手)

在前文中我们已经搭建好了K8S集群,接下来就来讲述一下K8S的一些重要的概念和知识,并搞两个例子在集群中来实际练手感受一把!

44814
来自专栏王亚昌的专栏

采用共享内存或文件映射的方式保存用户数据

    举个例子,假如一个网站提供给用户8种特权服务,用户可以选择性的开通其中一个或多个,而用户一般的操作行为是查看自己的特权以及查看好友的特权。这类数据的特点...

732
来自专栏蓝天

Linux下可以替换运行中的程序么?

今天被朋友问及“Linux下可以替换运行中的程序么?”,以前依稀记得Linux下是可以的(而Windows就不让),于是随口答道“OK”。结果朋友发来一个执行结...

852
来自专栏zingpLiu

【实战小项目】python开发自动化运维工具--批量操作主机

有很多开源自动化运维工具都很好用如ansible/salt stack等,完全不用重复造轮子。只不过,很多运维同学学习Python之后,苦于没小项目训练,本篇演...

1012
来自专栏开源优测

AutoLine开源平台升级新增自定义关键字支持

852

扫码关注云+社区