程序员修炼之道-3

}};  这个例子还说明了像Java和C++这样的面向对象语言的一个重要问题。在可能的情况下,应该总是用访问器(accessor)函数读写对象的属性。这将使未来增加功能(比如缓存)变得更容易。无耐性的重复  每个项目都有时间压力——这是能够驱使我们中间最优秀的人走捷径的力量。需要与你写过的一个例程相似的例程?你会受到诱惑,去拷贝原来的代码,并做出一些改动。需要一个表示最大点数的值?如果我改动头文件,整个项目就得重新构建。也许我应该在这里使用直接的数字(literal number),这里,还有这里,需要一个与Java runtime中的某个类相似的类?源码在那里(你有使用许可),那么为什么不拷贝它、并做出你所需的改动呢?  如果你觉得受到诱惑,想一想古老的格言:“欲速则不达”。你现在也许可以节省几秒钟,但以后却可能损失几小时。想一想围绕着Y2K惨败的种种问题。其中许多问题是由开发者的懒惰造成的:他们没有参数化日期字段的尺寸,或是实现集中的日期服务库。  无耐性的重复是一种容易检测和处理的重复形式,但那需要你接受训练,并愿意为避免以后的痛苦而预先花一些时间。开发者之间的重复  另一方面,或许是最难检测和处理的重复发生在项目的不同开发者之间。整个功能集都可能在无意中被重复,而这些重复可能几年里都不会被发现,从而导致各种维护问题。我们亲耳听说过,美国某个州在对政府的计算机系统进行Y2K问题检查时,审计者发现有超出10,000个程序,每一个都有自己的社会保障号验证代码。  在高层,可以通过清晰的设计、强有力的技术项目领导(参见288页“注重实效的团队”一节中的内容)、以及在设计中进行得到了充分理解的责任划分,对这个问题加以处理。但是,在模块层,问题更加隐蔽。不能划入某个明显的责任区域的常用功能和数据可能会被实现许多次。  我们觉得,处理这个问题的最佳方式是鼓励开发者相互进行主动的交流。设置论坛,用以讨论常见问题(在过去的一些项目中,我们设置了私有的Usenet新闻组,用于让开发者交换意见,进行提问。这提供了一种不受打扰的交流方式——甚至跨越多个站点——同时又保留了所有言论的永久历史)。让某个团队成员担任项目资料管理员,其工作是促进知识的交流。在源码树中指定一个中央区域,用于存放实用例程和脚本。一定要阅读他人的源码与文档,不管是非正式的,还是进行代码复查。你不是在窥探——你是在向他们学习。而且要记住,访问是互惠的——不要因为别人钻研(乱钻?)你的代码而苦恼。提示12Make It Easy to Reuse让复用变得容易  你所要做的是营造一种环境,在其中要找到并复用已有的东西,比自己编写更容易。如果不容易,大家就不会去复用。而如果不进行复用,你们就会有重复知识的风险。相关内容:l 正交性,34页l 文本操纵,99页l 代码生成器,102页l 重构,184页l 注重实效的团队,224页l 无处不在的自动化,230页l 全都是写,248页8 正交性  如果你想要制作易于设计、构建、测试及扩展的系统,正交性是一个十分关键的概念,但是,正交性的概念很少被直接讲授,而常常是你学习的各种其他方法和技术的隐含特性。这是一个错误。一旦你学会了直接应用正交性原则,你将发现,你制作的系统的质量立刻就得到了提高。什么是正交性  “正交性”是从几何学中借来的术语。如果两条直线相交成直角,它们就是正交的,比如图中的坐标轴。用向量术语说,这两条直线互不依赖。沿着某一条直线移动,你投影到另一条直线上的位置不变。  在计算技术中,该术语用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。在设计良好的系统中,数据库代码与用户界面是正交的:你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。  在我们考察正交系统的好处之前,让我们先看一看非正交系统。非正交系统  你正乘坐直升机游览科罗拉多大峡谷,驾驶员——他显然犯了一个错误,在吃鱼,他的午餐——突然呻吟起来,晕了过去。幸运的是,他把你留在了离地面100英尺的地方。你推断,升降杆控制总升力,所以轻轻将其压低可以让直升机平缓降向地面。然而,当你这样做时,却发现生活并非那么简单。直升机的鼻子向下,开始向左盘旋下降。突然间你发现,你驾驶的这个系统,所有的控制输入都有次级效应。压低左手的操作杆,你需要补偿性地向后移动右手柄,并踩右踏板。但这些改变中的每一项都会再次影响所有其他的控制。突然间,你在用一个让人难以置信的复杂系统玩杂耍,其中每一项改变都会影响所有其他的输入。你的工作负担异常巨大:你的手脚在不停地移动,试图平衡所有交互影响的力量。  直升机的各个控制器断然不是正交的。正交的好处  如直升机的例子所阐明的,非正交系统的改变与控制更复杂是其固有的性质。当任何系统的各组件互相高度依赖时,就不再有局部修正(local fix)这样的事情。提示13Eliminate Effects Between Unrelated Things消除无关事物之间的影响  我们想要设计自足(self-contained)的组件:独立,具有单一、良好定义的目的(Yourdon和Constantine称之为内聚(cohesion)[YC86])。如果组件是相互隔离的,你就知道你能够改变其中之一,而不用担心其余组件。只要你不改变组件的外部接口,你就可以放心:你不会造成波及整个系统的问题。  如果你编写正交的系统,你得到两个主要好处:提高生产率与降低风险。提高生产率l 改动得以局部化,所以开发时间和测试时间得以降低。与编写单个的大块代码相比,编写多个相对较小的、自足的组件更为容易。你可以设计、编写简单的组件,对其进行单元测试,然后把它们忘掉——当你增加新代码时,无须不断改动已有的代码。l 正交的途径还能够促进复用。如果组件具有明确而具体的、良好定义的责任,就可以用其最初的实现者未曾想象过的方式,把它们与新组件组合在一起。l 如果你对正交的组件进行组合,生产率会有相当微妙的提高。假定某个组件做M件事情,而另一个组件做N件事情。如果它们是正交的,而你把它们组合在一起,结果就能做M x N件事情。但是,如果这两个组件是非正交的,它们就会重叠,结果能做的事情就更少。通过组合正交的组件,你的每一份努力都能得到更多的功能。降低风险  正交的途径能降低任何开发中固有的风险。l 有问题的代码区域被隔离开来。如果某个模块有毛病,它不大可能把病症扩散到系统的其余部分。要把它切掉,换成健康的新模块也更容易。l 所得系统更健壮。对特定区域做出小的改动与修正,你所导致的任何问题都将局限在该区域中。l 正交系统很可能能得到更好的测试,因为设计测试、并针对其组件运行测试更容易。l 你不会与特定的供应商、产品、或是平台紧绑在一起,因为与这些第三方组件的接口将被隔离在全部开发的较小部分中。让我们看一看在工作中应用正交原则的几种方式。项目团队  你是否注意到,有些项目团队很有效率,每个人都知道要做什么,并全力做出贡献,而另一些团队的成员却老是在争吵,而且好像无法避免互相妨碍?  这常常是一个正交性问题。如果团队的组织有许多重叠,各个成员就会对责任感到困惑。每一次改动都需要整个团队开一次会,因为他们中的任何一个人都可能受到影响。  怎样把团队划分为责任得到了良好定义的小组,并使重叠降至最低呢?没有简单的答案。这部分地取决于项目本身,以及你对可能变动的区域的分析。这还取决于你可以得到的人员。我们的偏好是从使基础设施与应用分离开始。每个主要的基础设施组件(数据库、通信接口、中间件层,等等)有自己的子团队。如果应用功能的划分显而易见,那就照此划分。然后我们考察我们现有的(或计划有的)人员,并对分组进行相应的调整。  你可以对项目团队的正交性进行非正式的衡量。只要看一看,在讨论每个所需改动时需要涉及多少人。人数越多,团队的正交性就越差。显然,正交的团队效率也更高(尽管如此,我们也鼓励子团队不断地相互交流)。设计  大多数开发者都熟知需要设计正交的系统,尽管他们可能会使用像模块化、基于组件、或是分层这样的术语描述该过程。系统应该由一组相互协作的模块组成,每个模块都实现不依赖于其他模块的功能。有时,这些组件被组织为多个层次,每层提供一级抽象。这种分层的途径是设计正交系统的强大方式。因为每层都只使用在其下面的层次提供的抽象,在改动底层实现、而又不影响其他代码方面,你拥有极大的灵活性。分层也降低了模块间依赖关系失控的风险。你将常常看到像下一页的图2.1这样的图表示的层次关系。  对于正交设计,有一种简单的测试方法。一旦设计好组件,问问你自己:如果我显著地改变某个特定功能背后的需求,有多少模块会受影响?在正交系统中,答案应图2.1 典型的层次图该是“一个”。移动GUI面板上的按钮,不应该要求改动数据库schema。增加语境敏感的帮助,也不应该改动记账子系统。  让我们考虑一个用于监视和控制供暖设备的复杂系统。原来的需求要求提供图形用户界面,但后来需求被改为要增加语音应答系统,用按键电话控制设备。在正交地设计的系统中,你只需要改变那些与用户界面有关联的模块,让它们对此加以处理:控制设备的底层逻辑保持不变。事实上,如果你仔细设计你的系统结构,你应该能够用同一个底层代码库支持这两种界面。157页的“它只是视图”将讨论怎样使用模型-视图-控制器(MVC)范型编写解耦的代码,该范型在这里的情况下也能很好地工作。  还要问问你自己,你的设计在多大程度上解除了与现实世界中的的变化的耦合?你在把电话号码当作顾客标识符吗?如果电话公司重新分配了区号,会怎么样?不要依赖你无法控制的事物属性。8 正交性(2)工具箱与库  在你引入第三方工具箱和库时,要注意保持系统的正交性。要明智地选择技术。  我们曾经参加过一个项目,在其中需要一段Java代码,既运行在本地的服务器机器上,又运行在远地的客户机器上。要把类按这样的方式分布,可以选用RMI或CORBA。如果用RMI实现类的远地访问,对类中的远地方法的每一次调用都可能会抛出异常;这意味着,一个幼稚的实现可能会要求我们,无论何时使用远地类,都要对异常进行处理。在这里,使用RMI显然不是正交的:调用远地类的代码应该不用知道这些类的位置。另一种方法——使用CORBA——就没有施加这样的限制:我们可以编写不知道我们类的位置的代码。  在引入某个工具箱时(甚或是来自你们团队其他成员的库),问问你自己,它是否会迫使你对代码进行不必要的改动。如果对象持久模型(object persistence scheme)是透明的,那么它就是正交的。如果它要求你以一种特殊的方式创建或访问对象,那么它就不是正交的。让这样的细节与代码隔离具有额外的好处:它使得你在以后更容易更换供应商。  Enterprise Java Beans(EJB)系统是正交性的一个有趣例子。在大多数面向事务的系统中,应用代码必须描述每个事务的开始与结束。在EJB中,该信息是作为元数据,在任何代码之外,以声明的方式表示的。同一应用代码不用修改,就可以运行在不同的EJB事务环境中。这很可能是将来许多环境的模型。  正交性的另一个有趣的变体是面向方面编程(Aspect-Oriented Programming,AOP),这是Xerox Parc的一个研究项目([KLM+97]与[URL 49])。AOP让你在一个地方表达本来会分散在源码各处的某种行为。例如,日志消息通常是在源码各处、通过显式地调用某个日志函数生成的。通过AOP,你把日志功能正交地实现到要进行日志记录的代码中。使用AOP的Java版本,你可以通过编写aspect、在进入类Fred的任何方法时写日志消息:aspect Trace {advise * Fred.*(..) {static before {Lite("-> Entering " + thisJoinPthodName);}}}  如果你把这个方面编织(weave)进你的代码,就会生成追踪消息。否则,你就不会看到任何消息。不管怎样,你原来的源码都没有变化。编码  每次你编写代码,都有降低应用正交性的风险。除非你不仅时刻监视你正在做的事情,也时刻监视应用的更大语境,否则,你就有可能无意中重复其他模块的功能,或是两次表示已有的知识。  你可以将若干技术用于维持正交性:l 让你的代码保持解耦。编写“羞怯”的代码——也就是不会没有必要地向其他模块暴露任何事情、也不依赖其他模块的实现的模块。试一试我们将在183页的“解耦与得墨忒耳法则”中讨论的得墨忒耳法则(Law of Demeter)[LH89]。如果你需要改变对象的状态,让这个对象替你去做。这样,你的代码就会保持与其他代码的实现的隔离,并增加你保持正交的机会。l 避免使用全局数据。每当你的代码引用全局数据时,它都把自己与共享该数据的其他组件绑在了一起。即使你只想对全局数据进行读取,也可能会带来麻烦(例如,如果你突然需要把代码改为多线程的)。一般而言,如果你把所需的任何语境(context)显式地传入模块,你的代码就会更易于理解和维护。在面向对象应用中,语境常常作为参数传给对象的构造器。换句话说,你可以创建含有语境的结构,并传递指向这些结构的引用。  《设计模式》[GHJV95]一书中的Singleton(单体)模式是确保特定类的对象只有一个实例的一种途径。许多人把这些singleton对象用作某种全局变量(特别是在除此而外不支持全局概念的语言中,比如Java)。使用singleton要小心——它们可能造成不必要的关联。l 避免编写相似的函数。你常常会遇到看起来全都很像的一组函数——它们也许在开始和结束处共享公共的代码,中间的算法却各有不同。重复的代码是结构问题的一种症状。要了解更好的实现,参见《设计模式》一书中的Strategy(策略)模式。  养成不断地批判对待自己的代码的习惯。寻找任何重新进行组织、以改善其结构和正交性的机会。这个过程叫做重构(refactoring),它非常重要,所以我们专门写了一节加以讨论(见“重构”,184页)测试  正交地设计和实现的系统也更易于测试,因为系统的各组件间的交互是形式化的和有限的,更多的系统测试可以在单个的模块级进行。这是好消息,因为与集成测试(integration testing)相比,模块级(或单元)测试要更容易规定和进行得多。事实上,我们建议让每个模块都拥有自己的、内建在代码中的单元测试,并让这些测试作为常规构建过程的一部分自动运行(参见“易于测试的代码”,189页)。  构建单元测试本身是对正交性的一项有趣测试。要构建和链接某个单元测试,都需要什么?只是为了编译或链接某个测试,你是否就必须把系统其余的很大一部分拽进来?如果是这样,你已经发现了一个没有很好地解除与系统其余部分耦合的模块。  修正bug也是评估整个系统的正交性的好时候。当你遇到问题时,评估修正的局部化程度。你是否只改动了一个模块,或者改动分散在整个系统的各个地方?当你做出改动时,它修正了所有问题,还是又神秘地出现了其他问题?这是开始运用自动化的好机会。如果你使用了源码控制系统(在阅读了86页的“源码控制”之后,你会使用的),当你在测试之后、把代码签回(check the code back)时,标记所做的bug修正。随后你可以运行月报,分析每个bug修正所影响的源文件数目的变化趋势。文档  也许会让人惊讶,正交性也适用于文档。其坐标轴是内容和表现形式。对于真正正交的文档,你应该能显著地改变外观,而不用改变内容。现代的字处理器提供了样式表和宏,能够对你有帮助(参见“全都是写”,248页)。认同正交性  正交性与27页介绍的DRY原则紧密相关。运用DRY原则,你是在寻求使系统中的重复降至最小;运用正交性原则,你可降低系统的各组件间的相互依赖。这样说也许有点笨拙,但如果你紧密结合DRY原则、运用正交性原则,你将会发现你开发的系统会变得更为灵活、更易于理解、并且更易于调试、测试和维护。  如果你参加了一个项目,大家都在不顾一切地做出改动,而每一处改动似乎都会造成别的东西出错,回想一下直升机的噩梦。项目很可能没有进行正交的设计和编码。是重构的时候了。  另外,如果你是直升机驾驶员,不要吃鱼……相关内容:l 重复的危害,26页l 源码控制,86页l 按合约设计,109页l 解耦与得墨忒耳法则,138页l 元程序设计,144页l 它只是视图,157页l 重构,184页l 易于测试的代码,189页l 邪恶的向导,198页l 注重实效的团队,224页l 全都是写,248页挑战l 考虑常在Windows系统上见到的面向GUI的大型工具和在shell提示下使用的短小、但却可以组合的命令行实用工具。哪一种更为正交,为什么?如果正好按其设计用途加以应用,哪一种更易于使用?哪一种更易于与其他工具组合、以满足新的要求?l C++支持多重继承,而Java允许类实现多重接口。使用这些设施对正交性有何影响?使用多重继承与使用多重接口的影响是否有不同?使用委托(delegation)与使用继承之间是否有不同?练习1. 你在编写一个叫做Split的类,其用途是把输入行拆分为字段。下面的两个Java类的型构(signature)中,哪一个是更为正交的设计?  (解答在279页)class Split1 {public Split1(InputStreamReader rdr) { ...public void readNextLine() throws IOException { ...public int numFields() { ...public String getField(int fieldNo) { ...}class Split2 {public Split2(String line) { ...public int numFields() { ...public String getField(int fieldNo) { ...}2. 非模态对话框或模态对话框,哪一个能带来更为正交的设计? (解答在279页)3. 过程语言与对象技术的情况又如何?哪一种能产生更为正交的系统? (解答在280页)9 可撤消性如果某个想法是你惟一的想法,再没有什么比这更危险的事情了。  ——Emil-Auguste Chartier, Propos sur la religion, 1938  工程师们喜欢问题有简单、单一的解决方案。与论述法国大革命的无数起因的一篇模糊、热烈的文章相比,允许你怀着极大的自信宣称x = 2的数学测验要让人觉得舒服得多。管理人员往往与工程师趣味相投:单一、容易的答案正好可以放在电子表格和项目计划中。  现实世界能够合作就好了!遗憾的是,今天x是2,明天也许就需要是5,下周则是3。没有什么永远不变——而如果你严重依赖某一事实,你几乎可以确定它将会变化。  要实现某种东西,总有不止一种方式,而且通常有不止一家供应商可以提供第三方产品。如果你参与的项目被短视的、认为只有一种实现方式的观念所牵绊,你也许就会遇到让人不悦的意外之事。许多项目团队会被迫在未来展现之时睁开眼睛:“但你说过我们要使用XYZ数据库!我们的项目已经完成了85%的编码工作。我们现在不能改变了!”程序员抗议道。“对不起,但我们公司决定进行标准化,改用PDQ数据库——所有项目。这超出了我的职权范围。我们必须重新编码。周末所有人都要加班,直到另行通知为止。”  变动不一定会这么严苛,甚至也不会这么迫在眉睫。但随着时间的流逝,随着你的项目取得进展,你也许会发现自己陷在无法立足的处境里。随着每一项关键决策的做出,项目团队受到越来越小的目标的约束——现实的更窄小的版本,选择的余地越来越小。  在许多关键决策做出之后,目标会变得如此之小,以至于如果它动一下,或是风改变方向,或是东京的蝴蝶扇动翅膀,你都会错过目标。而且你可能会偏出很远。  问题在于,关键决策不容易撤消。  一旦你决定使用这家供应商的数据库、那种架构模式、或是特定的部署模型(例如,客户-服务器 vs. 单机),除非付出极大的代价,否则你就将受制于一个无法撤消的动作进程(course of action)。可撤消性  我们让本书的许多话题相互配合,以制作灵活、有适应能力的软件。通过遵循它们的建议——特别是DRY原则(26页)、解耦(138页)以及元数据的使用(144页)——我们不必做出许多关键的、不可逆转的决策。这是一件好事情,因为我们并非总能在一开始就做出最好的决策。我们采用了某种技术,却发现我们雇不到足够的具有必需技能的人。我们刚刚选定某个第三方供应商,他们就被竞争者收购了。与我们开发软件的速度相比,需求、用户以及硬件变得更快。  假定在项目初期,你决定使用供应商A提供的关系数据库。过了很久,在性能测试过程中,你发现数据库简直太慢了,而供应商B提供的对象数据库更快。对于大多数传统项目,你不会有什么运气。大多数时候,对第三方产品的调用都缠绕在代码各处。但如果你真的已经把数据库的概念抽象出来——抽象到数据库只是把持久(persistence)作为服务提供出来的程度——你就会拥有“中流换马(change horses in midstream)”的灵活性。  与此类似,假定项目最初采用的是客户-服务器模型,但随即,在开发的后期,市场部门认为服务器对于某些客户过于昂贵,他们想要单机版。对你来说,那会有多困难?因为这只是一个部署问题,所以不应该要很多天。如果所需时间更长,那么你就没有考虑过可撤消性。另外一个方向甚至更有趣。如果需要以客户-服务器或n层方式部署你正在开发的单机产品,事情又会怎样?那也不应该很困难。  错误在于假定决策是浇铸在石头上的——同时还在于没有为可能出现的意外事件做准备。要把决策视为是写在沙滩上的,而不要把它们刻在石头上。大浪随时可能到来,把它们抹去。提示14There Are No Final Decisions不存在最终决策灵活的架构  有许多人会设法保持代码的灵活性,而你还需要考虑维持架构、部署及供应商集成等领域的灵活性。  像CORBA这样的技术可以帮助把项目的某些部分与开发语言或平台的变化隔离开来。Java在该平台上的性能不能满足要求?重新用C++编写客户代码,其他没有什么需要改变。用C++编写的规则引擎不够灵活?换到Smalltalk版本。采用CORBA架构,你只须改动替换的组件:其他组件应该不会受影响。  你正在开发UNIX软件?哪一种?你是否处理了所有可移植性问题?你正在为某个特定版本的Windows做开发?哪一种——3.1、95、98、NT、CE、或是2000?支持其他版本有多难?如果你让决策保持软和与柔韧,事情就完全不困难。如果在代码中有着糟糕的封装、高度耦合以及硬编码的逻辑或参数,事情也许就是不可能的。  不确定市场部门想怎样部署系统?预先考虑这个问题,你可以支持单机、客户-服务器、或n层模型——只需要改变配置文件。我们就写过一些这么做的程序。  通常,你可以把第三方产品隐藏在定义良好的抽象接口后面。事实上,在我们做过的任何项目中,我们都总能够这么做。但假定你无法那么彻底地隔离它,如果你必须大量地把某些语句分散在整个代码中,该怎么办?把该需求放入元数据,并且使用某种自动机制——比如Aspect(参见39页)或Perl——把必需的语句插入代码自身中。无论你使用的是何种机制,让它可撤消。如果某样东西是自动添加的,它也可以被自动去掉。  没有人知道未来会怎样,尤其是我们!所以要让你的代码学会“摇滚”:可以“摇”就“摇”,必须“滚”就“滚”。相关内容:l 解耦与得墨忒耳法则,138页l 元程序设计,144页l 它只是视图,157页挑战l 让我们通过“薛定谔的猫”学一点量子力学。假定在一个封闭的盒子里有一只猫,还有一个放射性粒子。这个粒子正好有50%的机会裂变成两个粒子。如果发生了裂变,猫就会被杀死;如果没有,猫就不会有事。那么,猫是死是活?根据薛定谔的理论,正确的答案是“都是”。每当有两种可能结果的亚核反应发生时,宇宙就会被克隆。在其中一个宇宙中,事件发生;在另一个宇宙中,事件不发生。猫在一个宇宙中是活的,在另一个宇宙中是死的。只有当你打开盒子,你才知道你在哪一个宇宙里。  怪不得为未来编码很困难。  但想一想,代码沿着与装满薛定谔的猫的盒子一样的路线演化:每一项决策都会导致不同版本的未来。你的代码能支持多少种可能的未来?哪一种未来更有可能发生?到时支持它们有多困难?  你敢打开盒子吗?10 曳光弹预备、开火、瞄准……  在黑暗中用机枪射击有两种方式。你可以找出目标的确切位置(射程、仰角及方位)。你可以确定环境状况(温度、湿度、气压、风,等等)。你可以确定你使用的弹药筒和子弹的精确规格,以及它们与你使用的机枪的交互作用。然后你可以用计算表或射击计算机计算枪管的确切方向及仰角。如果每一样东西都严格按照规定的方式工作,你的计算表正确无误,而且环境没有发生变化,你的子弹应该能落在距目标不远的地方。  或者,你可以使用曳光弹。  曳光弹与常规弹药交错着装在弹药带上。发射时,曳光弹中的磷点燃,在枪与它们击中的地方之间留下一条烟火般的踪迹。如果曳光弹击中目标,那么常规子弹也会击中目标。

上一章 下一章
目录
打赏
夜间
日间
设置
10
正序
倒序
程序员修炼之道
程序员修炼之道-2
程序员修炼之道-3
程序员修炼之道-4
程序员修炼之道-5
程序员修炼之道-6
程序员修炼之道-7
程序员修炼之道-8
程序员修炼之道-9
程序员修炼之道-10
需支付:0 金币
开通VIP小说免费看
金币购买
您的金币 0

分享给朋友

程序员修炼之道
程序员修炼之道
获月票 0
  • x 1
  • x 2
  • x 3
  • x 4
  • x 5
  • x 6
  • 爱心猫粮
    1金币
  • 南瓜喵
    10金币
  • 喵喵玩具
    50金币
  • 喵喵毛线
    88金币
  • 喵喵项圈
    100金币
  • 喵喵手纸
    200金币
  • 喵喵跑车
    520金币
  • 喵喵别墅
    1314金币
网站统计