V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lanlanye
V2EX  ›  程序员

关于软件设计的一些问题

  •  1
     
  •   lanlanye ·
    laipz8200 · 2022-02-22 21:41:53 +08:00 · 3468 次点击
    这是一个创建于 765 天前的主题,其中的信息可能已经有所发展或是发生改变。

    不知道各位写代码时会不会经常觉得遇到以下问题:

    1. 某个听起来很自然的功能实现起来却需要绕很大一圈。
    2. 某个功能的实现不可避免的会影响到许多不相关的功能。
    3. 迭代到某个阶段,软件的响应越来越慢,逻辑越来越复杂,开发人员已经搞不懂自己在做什么了。

    ...

    以上只是我遇到许许多多问题中的一小部分,毕业快两年,我时常觉得自己写出的东西随着功能的增加离自己的设想越来越远,即使满足了业务需要也无法让自己满意。

    我认为开发难以进行的根源往往不是因为开发人员不会写业务代码,而是设计上出现了不合理的地方,有时是因为初期没有时间仔细考虑,有时是因为不可避免的需要与其他项目耦合,然而归根结底是开发者的设计能力不足,没能让软件保持低耦合与高内聚。

    近一个多月的时间,我试着通过各种渠道补充相关知识,从 数据库设计的一个小问题 到流行的架构模式和设计模式逐步学习,深感从 Python 开始开发一定程度上让我低估了面向对象设计的重要性。

    以下是我最近读过或正在读的书单(按时间顺序):

    1. 《数学之美》
    2. 《 SQL 反模式》
    3. 微软公开的 Azure 设计模式说明
    4. 《实现领域驱动设计》
    5. 《设计模式》
    6. 《领域驱动设计·精简版》
    7. 《领域驱动设计·软件核心复杂性应对之道》

    顺序上存在一些不合理的地方,比如《实现领域驱动设计》中提到了许多我难以理解的概念,这些概念是通过穿插着阅读后面几本书以及通过搜索引擎才逐渐能够理解的(以及我至今也没能把这本书彻底读完),这些书中也会引用其他人的著作,受限于时间我也没能逐个阅读,比如《分析模式》、《重构》等。

    可是直至目前我依然有很多疑问:印象里不久前还经常被人提起的“函数式编程”有什么优势?如果面向对象设计是一种“最佳实践”,为什么后来诞生的语言没有特别强调自己面向对象的特性?

    简言之,我隐约感觉自己遇到了瓶颈,我不确定这些疑惑是否会随着不断学习而得到解答,同时也很担心自己绕了远路钻了牛角尖,所以 希望站内的大佬们可以推荐一些相关书籍或者分享一些自己的学习经历、见解

    另外,我建了一个 QQ 群,希望 和我有同样困惑或相似目标 的人能够加入进来共同交流进步。这是一个尝试,我希望它是一个单纯的技术交流群,没有日常寒暄和无意义水群,不会因为过多的消息被人屏蔽,如果实在做不到我也可能解散掉它,群号 OTAyMzE0NDEz

    第 1 条附言  ·  2022-02-23 13:00:04 +08:00

    歪楼了,我希望与大家讨论的重点是 设计模式, 不仅是《设计模式》书中的内容,还有其他对现实问题抽象的方法,可能更贴近大家日常说的“架构”问题。

    44 条回复    2022-03-05 10:50:33 +08:00
    3dwelcome
        1
    3dwelcome  
       2022-02-22 22:19:50 +08:00
    设计软件就炒股一样。股票里 K 线图有个著名的随机游走策略,导致你不管做短线,中线,还是长线,输赢概率是相同的。微观操盘和宏观操盘上,体验是一致的。

    套用在软件开发也类似,从微观设计 API ,和宏观调用 API ,也需要保证开发体验是一致的,才能控制好全局。

    不能因为代码增多,就陷入局部细节。要不断的封装对象,隐藏细节,让代码流程保持在一定可供总量。往往代码量太多,又没有合理的封装,才是屎山的开端。

    至于“函数式编程的优势”,个人觉得就是能做到无状态编程,没内部状态维护,那么固定输入后,输出结果都是可预测的。能减少一些心智负担。
    qaqLjj
        2
    qaqLjj  
       2022-02-22 22:30:31 +08:00
    请问 ddd 最入门的文档是哪一篇啊,ddd 大名鼎鼎,想了解一下
    lanlanye
        3
    lanlanye  
    OP
       2022-02-22 22:40:27 +08:00
    @qaqLjj 我觉得应该是 InfoQ 这本 https://www.infoq.cn/minibook/domain-driven-design-quickly
    《实现领域驱动设计》中将这种称为「 DDD Lite 」或者「战术 DDD 」,因为它对战略设计的部分仅仅解释了一下概念,没有深入,看完后还是得去读原著,但能解释清楚概念已经非常有用了。
    3dwelcome
        4
    3dwelcome  
       2022-02-22 22:54:57 +08:00
    我以前发了一个去函数编程的方案。

    也就是用命令式指令,去替代传统函数的封装。类似一点 DSL 的定义。

    可惜最后被移到了水深火热专区,因为大家都不信,去了函数后还怎么写代码。

    这就是大家编程理念不同,微观上是函数,宏观上就自然升级到了指令。真是残念,我好多贴都去了水区,一去不复返。
    qaqLjj
        5
    qaqLjj  
       2022-02-22 22:56:01 +08:00
    @lanlanye 这本会不会太老了,我看是 07 年发布的,那时候我小学都没毕业
    liuhan907
        6
    liuhan907  
       2022-02-22 23:01:45 +08:00
    @3dwelcome
    指令和函数除了叫法不同,我很难想出还有什么区别啊。。。
    lanlanye
        7
    lanlanye  
    OP
       2022-02-22 23:05:03 +08:00
    @3dwelcome 网络上对函数式编程的解释似乎很杂,类似 Google 的 map+reduce 这种模式我还能理解它的实际意义,但其他场景下到底怎么用我是完全不知道,特别是最近看的书都强调面向对象设计,感觉它们根本没有互相替代的可能。

    @qaqLjj Eric Evans 的原著是 03 年的,不排除其中一些概念过时了的可能性,比如当时 web 还没有那么火,之后的移动互联网更是难以想象的,但后来者对这本书的解读感觉还不如原文,我觉得想要理解作者的思想,还是看原文比较好。(《 CSAPP 》出版于 2004 年,理论性的知识并不容易过时)
    3dwelcome
        8
    3dwelcome  
       2022-02-22 23:11:02 +08:00
    @liuhan907 我这里的指令类似于 LUA 脚本定义,每个指令都是负责控制一堆函数体。是函数集的上层抽象。

    函数里我一般不习惯牵涉业务,业务太多太杂,命名太辛苦,写一大堆函数没多大必要。而指令大部分都是能和业务实际挂钩的。

    可能没代码很难理解,指令可以看成是 onmessage 里的一个个接受命令体,没有自己的函数入口,但能正常调用别的函数。但很可惜那个详细说明的帖子,我自己也找不回了。
    liuhan907
        9
    liuhan907  
       2022-02-22 23:14:49 +08:00
    @3dwelcome
    但是函数难道不符合这个定义么?或者说,你认为的指令和函数本质上的区别是什么,我无法想象。函数本身已经就是对动作过程的描述,再增加一层抽象得到了什么好处呢?或者说,能否解释一下指令嵌套函数的模式和函数嵌套函数的模式有什么优势。
    3dwelcome
        10
    3dwelcome  
       2022-02-22 23:15:14 +08:00
    比如 Restful 的注册函数,正常写法,可能用户注册就是对应一个注册函数。

    而我就是一个 RPC 的注册指令,和外部路由绑定后,就直接处理了。
    3dwelcome
        11
    3dwelcome  
       2022-02-22 23:18:28 +08:00
    @liuhan907 "你认为的指令和函数本质上的区别是什么 再增加一层抽象得到了什么好处呢?"

    好处就是宏观写法和微观写法隔离开。

    你管理几百个函数也许没问题,在几千上万个函数里跳来跳去,还是很有压力的。

    我自己建立了一套指令网状体系,可以很快跳到自己需要修改业务的位置。
    Mithril
        12
    Mithril  
       2022-02-22 23:18:56 +08:00   ❤️ 2
    这就是架构师的价值了。
    说的是正经的架构师,不是那些满口新技术的 PPT 架构师。
    软件架构设计和软件需求本身就是互相制约的,什么样的需求就导致会有什么样的架构设计和技术选型,同时这个架构设计又会反过来制约需求的发展。
    所以一个好的产品,需要有足够强力和有经验的架构师和 PM 团队来一起界定需求和架构要互相妥协成一个什么样子,然后后续的开发和需求都围绕着这个共识来做。
    这就是 DDD 的第一步了。
    taowen
        13
    taowen  
       2022-02-22 23:19:08 +08:00
    https://zhuanlan.zhihu.com/p/356202989 代码写得不好,不要总觉得是自己抽象得不好
    giiiiiithub
        14
    giiiiiithub  
       2022-02-22 23:21:06 +08:00
    DDD 就很玄,不如 fp 思想来的实在。
    Macolor21
        15
    Macolor21  
       2022-02-22 23:32:10 +08:00
    @3dwelcome #8
    @3dwelcome #10

    没搞清楚你想说的是啥,Scala 函数式编程,有个 Scala 框架叫 Akka 。

    基本思想类似于一个 Actor 是一堆函数的集合,如 计算 Actor {
    .onMessage(Initilize.class, handleInitilize)
    .onMessage(Plus.class, handlePlus)
    .onMessage(Minus.class, handleMinus)
    }

    想要调用函数就是往 Actor 发一个消息,指令。如: computeActor.tell( new Plus(1) ); 就会执行 handlePlus 里面的逻辑

    Actor 不会 new 一个 Actor 类的对象出来,而是 invoke 这个函数。
    3dwelcome
        16
    3dwelcome  
       2022-02-22 23:37:55 +08:00
    @Macolor21 以上古的 win32 编程举例吧。

    一个 message ,就是一个 WM_USERREG 注册函数,加两个未命名的 wParam 和 lParam 。

    没有函数体,不用给函数起名字。只需要 WM_USERREG 和若干个未命名参数,就可以执行一段用户注册的逻辑代码,并顺利返回结果。

    反正就是这个意思。
    lanlanye
        17
    lanlanye  
    OP
       2022-02-22 23:39:14 +08:00
    @Mithril 我也是这么想的,只是我们小公司没有正经架构师,所以总是遇到问题。

    @taowen 还好,我们产品会参考我的意见,不会出现你说的情况。

    @giiiiiithub 确实很玄,但 FP 开始时轻松,随着发展心智负担越来越大(主要是需要保证无状态),还是说有什么成体系的方法论推荐吗?
    giiiiiithub
        18
    giiiiiithub  
       2022-02-22 23:58:04 +08:00
    @lanlanye FP 也不用入邪教,就对写清晰好测的函数,从小到大构建系统整体或者局部有帮助就行了。 我之前看的是《 functional programming in scala 》以及杂七杂八文章。比如所谓的无状态,其实是把状态推到其他地方隔离起来了,这样关键性的一部分函数就可以是纯的了。
    secondwtq
        19
    secondwtq  
       2022-02-22 23:59:44 +08:00
    我认为所谓的函数式编程,纯也好不纯也罢,主要的现实意义在于其组合性上
    https://v2ex.com/t/807880#r_10977046
    mascteen
        20
    mascteen  
       2022-02-23 00:22:35 +08:00 via Android
    推荐看看范畴论在语言中的运用
    liuhan907
        21
    liuhan907  
       2022-02-23 01:28:45 +08:00
    @3dwelcome
    那听起来不就是 erlang 的 actor 模型的弱化版,何况大规模下几万个指令和几万个函数的理解成本没有什么区别。或者说你觉得 win32 的那套消息机制会比现在的异步等待或者协程更容易理解?我反正不这么觉得。
    Leviathann
        22
    Leviathann  
       2022-02-23 01:44:43 +08:00
    面向对象在上世纪末是很时髦的东西,很多语言都会说自己“一切都是对象”就跟现在很多语言说自己“函数是一等公民”一样

    我觉得这波函数式的火热 react 占了很大的功劳
    声明式的 UI 开发告诉大家,过去大家认为没有继承(基于 class 的面向对象区别其他范式最核心的特性)就很难开发 UI ,实际上靠组合也可以做到
    然后大数据方面 scala 也炒作( hype )过一段时间
    xuanbg
        23
    xuanbg  
       2022-02-23 08:19:15 +08:00
    楼主说的这些问题大多是数据模型的问题,数据模型的设计问题里面尤其突出的是业务数据和基础数据不分家的问题。说到底还是抽象不够的问题。
    3dwelcome
        24
    3dwelcome  
       2022-02-23 10:36:14 +08:00
    @liuhan907 "何况大规模下几万个指令和几万个函数的理解成本没有什么区别。"

    设计指令的目的,都是为了在不同维度下,让开发体验保证一致。

    拿股票 K 线举例,你短线操作会观测到的细节,对于长线波段是没什么价值的,把细节抹去或者隐藏,才能让你更专注到更大的盘面,始终保持掌控力。

    你封装函数,封装对象,同样也是为了隐藏细节。这帖子就是为了讨论宏观把控力,协程这些真的都是很枝末细节的部分。

    海量函数能不能被很好的管理,要看开发者的经验。但指令模式,一定是可以用代码方式,科学维护管理的。
    liuhan907
        25
    liuhan907  
       2022-02-23 11:11:04 +08:00 via Android
    @3dwelcome
    为什么你会认为指令比函数更容易维护?
    FrankHB
        26
    FrankHB  
       2022-02-23 11:36:00 +08:00   ❤️ 2
    因为这是显然的倒退。

    函数(function) 的另一个同义词是“功能”,本来就有更高层次的抽象的意象在里面。所以编程语言发展过程中会把过程(procedure) 自觉合并到“函数”里,而不管数学上原本的定义是怎么样的。

    (另一个原因是数学上的定义本来就不怎么样,混乱程度参考 en.wikipedia.org/wiki/History_of_the_function_concept 词条——看,都需要单独整个条目出来了。)

    所以你所谓去函数,会被一般用户理解为去除功能上的模块化抽象。

    另一方面,对有点兴趣关心不同语言设计的用户,你说的函数具体是什么,都是个问题。但是既然你用“命令”代替“函数”,那我就当作最常见高级语言里允许带变量名构成参数的那种。

    而这样你就走进死胡同里了:你在对抗最常规的高级语言普遍需求——抽象。

    学术一点讲,虽然我数落数学定义,但是现代意义上的真正算跟高级语言捆绑在一起的“函数”,其构造器的学名(来自λ演算)就叫“λ抽象”。这货的重要性在于,它是所有日常通用编程语言中叫“函数定义”的东西在操作语义描述中的原型,不管语言的设计者是不是意识到了这点。

    为什么命名为抽象?因为它提供了把变量绑定作为形式参数的现代意义上的“函数”的普遍功能,而变量名允许用户自行随意决定而原则上不改变程序的语义稳定性(α-转换),或者说,允许定义函数的作者以决定变量名的方式夹带私货,隐藏实现。配合抽象的应用(函数调用)的结果能被作为抽象的操作数(函数参数),以此能组合出最基本的接口和实现分离提供微观上的可维护性——这是实用意义上最基本的可编程性的一部分。

    因此即便原版λ抽象也不支持副作用(以之后几十年的时髦话来说,纯函数式),拿来给人编程,在很大程度上就足够比任何不提供抽象的语言好用。

    相比之下,大多数传统数学家(不算整数理逻辑之类的)习惯的那种所谓的函数更适合叫“映射”,严格定义(例如通过笛卡儿积或者数学关系)上只是一种更一般意义上的函数的没多少可编程性的实现方式罢了。

    完全没有限制的抽象可能会被滥用,所以也被一些用户批判,比如 FORTRAN 和 BNF 之父 J. Backus 。他在 1977 年图灵奖演说中发牢骚并提出要以所谓的 FLP(function level programming) 解决当前语言的哲学问题。这个方案的核心就是消灭抽象——带变量的函数,让所有函数回归到没有变量而通过组合的更接近传统数学上容易排除私货而更容易推理的玩意儿,所谓 point-free style ( point 就是参数名)。

    但这显然没有工程意义上成功到哪去。甚至学术上也不咋地,因为计算模型上撑死就只是μ-递归函数这套,实在没多少新东西。这番推广倒是便宜了一字之差的 FP(function programming) ——而后者是显然接受甚至鼓励λ抽象的。

    FLP 失败的根本原因是,禁止用户使用函数参数就是“不好用”。(考虑基本没什么人用,具体现代例子还真不太好举例,比如想想只准管道不准 xargs 之类的 shell ……)要克服 FLP 风格的抽象困难特别是不容易表达常规递归算法描述的解的问题,用户程序基本得像早期 Lisp 语言一样要发明动态的环境(实际通常不具备像λ演算一样用替换实现“调用”的条件,至少非纯函数不大行)甚至词法闭包去重新实现抽象;考虑到实现λ抽象是λ演算的语义上的工作量的 50%以上(这保守了,实际一般起码 80%,如果考虑类型系统等其它特性经常对函数定义有逻辑依赖的话),这四舍五入基本上就是自己实现一个新语言的解释器了。而 FLP 语言又不是汇编这样能容易让硬件支持来换取存在感的语言,在这里还不如直接从实现栈里一脚踹开,站茅坑不拉屎在实用上是完全不合格的。

    再者,强调 point-free 也不是什么新的东西,组合子逻辑在 1950 年代早就整出来了——但也就是更早(不晚于 1940 年代)的λ演算上抠掉自由变量的缩水版整理出的(虽然组合子本身的历史可以追溯到 1920 年代)。而使用体验呢?除了纯函数式那套,比汇编还汇编(连寄存器都没)。甚至实际可用的组合子逻辑实现如 unlambda ,为了写出和“传统”语言功能等价的代码的一般方法,还得指望用户自己先构造λ抽象然后人肉编译去掉其中的具名参数。另一种路线是 APL——函数命名炸了(自行查找代码怎么写的)。

    这样的语言,能被多数高级语言用户接受才有鬼了。

    这里的缩水版函数(组合子),与其说是大道至简的 basic building block ,倒不如说只是一种(通过抽象这种构造器引入的)健全版函数的丐版,甚至都未必有传统数学上的映射实用(映射的描述是起码可以选配抽象的,只是形式上很不严格);毕竟计算模型上考虑语义而不仅仅是表面的语法,和组合子同等重要甚至更根本的东西多了去了。

    而所谓的“命令”,不说是不是被抽象养刁了的编程语言用户接受(大部分还是非纯函数式语言的抽象,组合子都没法这样扩展),连学术上引起的兴趣都远不如组合子。

    考虑历史,“命令”本质上是什么呢?其实就是因为一开始实现困难,而不那么强调组合的低级语言中间产物——命令或者说指令,早期基本就是机器操作助记符的同义词。之后即便在 FORTRAN 之类的高级语言中推广,但是增加的功能也相当抠,说白了是另一种风格的丐版函数。和组合子的丐版不同在于,它没 point-free 那么极端地拒绝使用不能作为函数组合的参数,但也没一般编程语言强调允许递归地把结果作为实际参数。结果就算不像组合子在高级语言可编程性上那么反人类(还得人肉编译才能写出多数编码),就是鸡肋中的战斗机。

    至于为什么讲到 DSL 看上去更容易接受“去函数”?因为 DSL 的目标用户通常没有经过基础的编程语言训练——不但没编程语言理论(大多数程序员其实也都没有),而且缺乏任何一种支持不丐版函数的通用编程语言的学习,所以理解不了“函数”的现代内涵,会把“函数”的理解自觉停留在中学阶段那种漏洞百出的玩意儿上,更加想象不出“函数”能用到怎么灵活的程度。这样,“命令”这种看上去无关的强大的“实用”货色,才会容易被接受。

    (其实内部鄙视链也是有的,比如因为解决不了 funarg 问题等实现原因而无法支持一等函数的类 ALGOL 语言和拐弯用“方法”支持自由函数的所谓面向对象语言的用户,就经常在所谓“函数式”语言的用户的火力范围之内。)

    对任何有点通用目的编程语言使用经验的用户来说,对不那么丐版的函数(包括“过程”)有点体验就会食髓知味,自然回不去被强迫使用“命令”了。(这也是为什么 shell 不受很多程序员待见的原因之一;虽然讲道理 shell 的函数没那么丐,还有个原因是不容易完全写对,维护起来太作妖了。)

    所以你的“微观上是函数,宏观上就自然升级到了指令”根本是外行人^n (n>=2) 的无稽之谈。

    (就不说 CPU 指令到底是不是微观的问题了,反正大部分用户都不会直接碰这个去编程。)
    FrankHB
        27
    FrankHB  
       2022-02-23 11:38:11 +08:00   ❤️ 1
    淦,上面的 at 怎么掉了……26L Re: @3dwelcome .
    3dwelcome
        28
    3dwelcome  
       2022-02-23 11:50:27 +08:00
    @liuhan907 "为什么你会认为指令比函数更容易维护?"

    你要管理一整套基于函数的语法树,构建 AST 是相对复杂的。而程序化去提取指令,要简单太多。

    你可能没接触过项目遗留屎山,就是函数和代码量多到,你怎么都改不过来。有时候并不是谁好谁坏的问题,是你看问题的角度不同,舍取的问题。

    宏观对我来说,就是把函数细节抹掉,把精力省下来,留到更有意义的事情上。
    3dwelcome
        29
    3dwelcome  
       2022-02-23 11:53:53 +08:00
    @FrankHB 我们说的都不是一回事,你是说哪一种代码实现方法更好,可我说的是整体设计方向。

    一个是低维,一个是高维,两者没办法放一起对比的。

    你细节 API 代码只要花时间,最后怎么都能写好。但是管理海量代码,是需要一点技巧和模式的。
    FrankHB
        30
    FrankHB  
       2022-02-23 11:58:33 +08:00   ❤️ 1
    当然这里大部分问题不是 PL 问题能解决的,而来自工程需求的本质困难。光是需求理解的复杂性( NPL:喵?)指望自动解决就差不多是在指望发明强 AI 。
    但还是有点相关的:建议参考 Actor model 和 Scheme 的早期历史。
    铜币不够先不展开了。
    3dwelcome
        31
    3dwelcome  
       2022-02-23 12:23:48 +08:00
    K 线是一维的,解释不了宏观和微观,我就用二维图片来代替。

    一张堆满细节的超高清图片,在普通的 1080p 显示器上全图显示,细节是自动被抹去的,所谓的下采样。

    细节一直存在,但是你看不见,你也不在乎,代码缩放也是这个道理。

    放到最大,函数能看见。缩到全图,就自动退化成一个点了。这时候所谓函数名,函数参数都都不重要,用指令包裹起来,我只要发布指令和命令,软件能正常运作,就足够了。
    lanlanye
        32
    lanlanye  
    OP
       2022-02-23 12:52:42 +08:00
    @Leviathann 实际上我也不是很认可 Java 这种强迫用户使用类定义操作的方式(据说后来的许多框架本质上都让用户 绕过了这个限制),但如我题中所说,我是从写 Python 的习惯出发解决问题,最后发现使用 OO 的思想对问题建模是很有必要的,或者本质上来说对问题进行合理抽象是必要的,这里是否应该认为 OO 和 FP 只是不同的抽象工具,其目的是相同的?

    @xuanbg 数据模型反映的是业务模型,只有设计出合理的业务模型,才能设计出合适的数据模型来,我觉得这其实是同一个问题。

    @FrankHB 感谢科普,虽然和我的问题关系不大,但是了解到了很多新知识。

    @3dwelcome 我觉得你们对 “函数” 和 “命令” 的理解可能存在出入,讲的不是一件事情。
    charlie21
        33
    charlie21  
       2022-02-23 13:32:55 +08:00
    项目管理三要素:time, cost, scope
    一个(设计 / 操作 / 安排)必须匹配你的时间估算和工期计算

    抽象度过高的优点是 “满足了更大 scope ” 的情况,缺点是 如果实际上没有那么大的 scope 那么这是对工程资源的浪费

    何时抽象等于浪费?就是此时
    何时抽象不等于浪费?重构的时候,重新考虑所有问题,重新抽象

    那么一个显而易见的结果是
    1 第一版的代码总是很烂。这没有关系,这反而是好设计,是匹配当时工期计算的设计
    2 第二版的代码里会出现各种设计模式 / 抽象等级比第一版高

    但根据地球人类的一些经验,“第二版代码” 往往是不会出现的,是胎死腹中的。理由是:重构需要人力,而重构结果仅仅是和第一版代码(能运行)的运行效果一样。那么还重构什么呢?只有那些 long live 的项目,才有 “好心人” 来运用抽象能力给出第二版代码。这样的项目占总体项目的比例可能不到 10% ,而正是这些人总结出了设计模式这回事:写给参与 long live 项目的人 如何去重构代码。其他人:1 心安理得围绕抽象程度很低的第一版代码做事 2 去给那些愿意给第二版代码付钱的人做事,只有这些愿意付钱的人会让第二版代码不胎死腹中

    it's nothing wrong to just let the shit die.
    charlie21
        34
    charlie21  
       2022-02-23 13:39:10 +08:00   ❤️ 1
    当然可能你会又说到 “程序员的成长” 了。不,你没有成长。对公司项目,拿钱办事走人,就这么简单。这个行业不允许你成长,公司不需要你的成长,没什么好成长的。有时候自己刻意运用一些精妙技巧写出的一个非常小的 demo 都比那些公司项目里用大白话写的垃圾的价值高,这就是事实。你的成长需要成本,公司不会为你的成长付费。你不要指望在公司项目里成长,你做梦。
    liuhan907
        35
    liuhan907  
       2022-02-24 13:45:06 +08:00
    @3dwelcome
    我实在很难赞同你对於指令的看法,函数和指令本来就是一回事,都是对过程的抽象。指令可以屏蔽细节,函数当然就也可以做到。我很难理解为何你会觉得指令是比函数更好的抽象。这根本就是换汤不换药。
    3dwelcome
        36
    3dwelcome  
       2022-02-24 14:14:01 +08:00
    @liuhan907 你是小兵的时候,“函数”就是具体执行任务的方式。

    你当领导的时候,你自然就不需要关心具体实现细节,只需要给别人发布模糊“指令”即可。

    我还是那句话,不同的维度,看到的世界不一样。两者并不能拿来横向对比,孰优孰劣。

    好领导就不应该死抠软件实现细节,应该把控大局。
    3dwelcome
        37
    3dwelcome  
       2022-02-24 14:18:41 +08:00
    还有我一直觉得软件开发人员,是需要分层的。

    分写 API 和调用 API 的人,不分层很难把软件架构真正做好。普通人不可能把控每个细节,特别是项目里,很多函数不是你写的时候。

    一个庞大工程的总代码量,是你无法控制的,分层却是可控的。
    RickyC
        38
    RickyC  
       2022-02-24 17:04:39 +08:00
    这没一大段叫人怎样阅读?
    应该第一句话就说重点。让人知道你问题的点是什么。
    v2exblog
        39
    v2exblog  
       2022-02-24 17:58:21 +08:00
    最近遇到了相同的问题,请教楼主,CRUD 的时候怎么对业务建模。如果实践的话,CRUD 和 API 接口怎么分层,跪求指导
    Gota
        40
    Gota  
       2022-02-24 21:40:48 +08:00   ❤️ 1
    我过年时发过一帖,推荐有助于突破瓶颈期的书籍,可以参考:
    https://www.v2ex.com/t/831291

    也可以看看 HackNews 上相关的讨论:
    https://news.ycombinator.com/item?id=30228261

    至于如何保持对程序的掌控力,我觉得重点在于精通模块化设计。
    而面向对象或是函数式编程只是实现模块化的手段。

    这方面推荐读读看 《 UNIX 编程艺术》,我简要提一下其中与模块化设计相关的点。

    1. 设计良好的程序一定要做到 “机制” 和 “策略" 分离
    2. “机制” 偏底层,设计时要做到:1 )相互正交; 2 )稳定不变
    3. “策略” 偏上层,保持灵活,有时甚至可以交给用户自己实现
    4. “机制” 和 “策略” 之间用一层尽可能薄的胶合层连接

    至于具体如何做到上面四点,就到书中找答案吧。

    最后关于编程范式,我的习惯是:
    1. 偏上层的业务操作特别是 UI ,面向对象
    2. 偏底层的机制,面向过程
    3. 需要做流式处理,就上函数式 (比如后端数据管道,或是前端的事件流 )
    FrankHB
        41
    FrankHB  
       2022-03-04 13:16:43 +08:00   ❤️ 1
    @3dwelcome 路走窄了。

    海量 API ?海量用户和需求面前算什么事儿?

    这里有个背景:用户和开发者从来没有绝对的界限。(真跑题:就因为这,我同时站在 RMS 和 Steve Jobs 的对立面。)
    很多用户和开发者的角色已经重合。开发者水平够,需求响应就能几乎任意高效。开发技术演进使“水平够”的门槛不断降低。到一定程度,OP 的部分因需求理解困难的问题会通过这种耍无赖的方式解决(剩下是怎么都不太可能圆满解决的)。
    长期看这是种 dssq:最终用户迟早会跨越信息不对等,变成某种意义上的程序员 /产品经理,自己动手才能保持具体需求上的话语权;跟文盲起码先得脱盲才可能融入主流社会一样。
    (无法融入?最好祈祷需求真被谁代表了而能蹭点现有产品。有消费能力的,会被当作代表资本话语权的实体的提款机,用完就扔,还没本事阻止一些开发者耍流氓。这是现在进行时。)
    能预见的阻止 API 发挥价值最终的问题就是领域特定逻辑太麻烦,非用户的开发者不够熟悉。以后要管海量 API ,大不了锅扔给同时是开发者的内行用户就是了。

    API 就不是什么细节,而是所有接口中最具有代表性的。相比其它种类的接口,API 受需求变化和外部随机干扰更小,开发者最能主动自主变更设计应对需求变化,演化过程也最易自动化。
    被认为是细节,说明现在技术发展很初级罢了,不够提供足以让用户信任的大规模可靠接口。
    “一个是低维……两者没办法放一起对比的。”——这正好是 API 提供的抽象比起其它接口更加擅长专门治疗的不服。
    生产力足够,UI 甚至 ABI 都能被吸收作为 API 提供,反过来不行。
    设计 DSL 的工作也能是设计 API 的子集(调包侠拿工具搭积木;实现能更自动化),当然现在科技不够(最直接地,spec 绕不开自然语言这种自动化之敌)。

    另外,让没被 API 吸收的其它接口设计工作有存在感,会越来越细碎和专业化,占据通用领域的开发者过大比例的资源而和生产方式不搭;之后,只有最终用户才能准确理解和快速响应依赖非通用 API 设计方案满足的需求。
    传统开发者更适合作为专业顾问或者维护基础设施。这种正反馈加速生产力分工的分化和再融合。最后,用户会自发把需自动化照顾效率的其它接口标准化为 API 。

    函数是 API 设计中能被普遍复用的单元,有资格成为未来所有能复用的接口的必备标准件。没有任何已知备胎有相提并论或超越这个候选的可能。
    市场认知成熟到一定程度,会普遍发现让一个备胎具有替代函数的功能,比从一个成熟的基于函数抽象的接口实现中砍掉功能而定制化(取得丐版)的代价大得太多。不管架构和生产力分工如何演化,这部分变化余地不大,未来也不太可能有变数。
    当然,完全无关的古典接口设计方式也会保留,毕竟有不同层次的边缘需求。就像文字被发明之后,画图仍然被作为一种承载信息的手段。但是,健全的用户很容易理解不同手段适用性差距如何。

    于是要抽象掉什么,总是会被逐渐成为开发者的用户个别关心。解决方案的演化只会让用户越来越有权决定打算自己动手处理还是让自动化手段替代。
    只考虑“宏观”问题的一揽子解决方案的市场占比会越来越小,因为这同样是在排除用户的话语权;同时,这类方案的设计者因为提供粒度有限、表面成本较高(同样主要来自信息不对等)等原因,比其它生产部门经常更容易因为能自动化机制代替而被用户优化掉。
    原则上,你需要解答的“宏观”问题,之后会成为只有(没能力干预实际生产活动的)领导关心的奢侈问题。

    “你要管理一整套基于函数的语法树,构建 AST 是相对复杂的。而程序化去提取指令,要简单太多。”
    你对复杂的认知和主流从业者还不大一样:当年这类选型不用 AST 主要因为实现环境限制(主存不够大),而不是你嫌弃的复杂。
    构建 AST 对工业语言基本小菜一碟,反正横竖省不掉 parse ,就输出 IR 格式不一样,分析树前的工作量没多少区别。而且真要计较,不用 AST 避免所谓的复杂性,整体经常适得其反。强行不用 AST 去实现一些变换(同时照顾时间性能),比起能用 AST 的复杂程度可能超过一个数量级。(所谓纯 FP 语言等鼓吹声明式编程风格的实现出来的代码看上去可能好那么点,但代价是输出结果以外各方面不可预测。)
    要去除所谓的复杂在一般只会是不成熟的优化。现在业界早就接受先天不足的丐版方案在需求整合面前无能的事实了。
    比如说 MSVC 的 cl 因为没有 AST 数十年拖着不实现 2-phase name lookup 最终几乎整个干掉了重写这种,算复杂么。
    没 AST 的玩具设计注定无能应对一些常见需求,拖越久成本越高,只配当作一些非专业用户的日常作业。趁早干掉是出路,要么等死。
    要找到除极端资源限制理由外适合不用 AST 去处理 DSL 又具有显著现实意义的案例还更难。AST 对应递归语法,不支持的(对应 AST 退化成列表,不用正经的也无关紧要)要么类似原始的汇编这种特别低级,要么像 INI 这样普遍缺乏灵活性。(极端例子,用程序提取 CSV ,听着还不如让用户拿个靠谱的命令行文本编辑器直接处理方便……编辑器还能带脚本支持,命令式函数式的都有,都不用设计方案。)要扭曲成这样,DSL 一般仅是不成熟的历史包袱;若没有现成稳定实现而要花资源维护,这种 DSL 继续值得在设计方案中存在都是个问题。
    而所谓“基于函数的语法树”的说法暴露出你不懂如何度量这里的复杂性,也不清楚一般实现。AST 节点不管是不是函数的定义和 /或外部表示,原则上没区别。相反,Lisp 这样几乎什么东西都能当 S-表达式把所有子表达式的组合硬当函数应用(不严格准确,Lisp 函数一般是指过程,然而还有特殊形式;不过更高级的语义模型上可以统一成函数),反而比典型的命令式语言要区分处理各种语句、声明(可能算语句,如 C++;也可能不算,如 C )、表达式之类更多各种乱七八糟的不同 AST 节点简单得多了。而且,要都实现成 AST 解释器,不算 GC 的 Lisp (如 PreScheme )一般也比类 ALGOL 语言还简单点(没简单那么多的理由是类 ALGOL 语言的语义要求可能比较氵,可以成心不支持一等函数)。
    函数实现的复杂性来自活动记录管理机制支持,如 GC 和闭包,这些相对 AST 完全是“离线”的,一般要在自由存储区分配;但就算设计降级用丐版函数,只要不丐到组合子的层次、类似硬限制成用 AST 上在线变量替换代替调用的λ演算,或者 shell 外部命令那种整体外包踢皮球给操作系统的奇技淫巧,一样要管理离线资源。这不是实现者来应付,就是当用户用到递归算法时自己重新造轮子(如栈),整体复杂性不会少。强上这种设计,只是便宜了连栈都不会实现却又愣是不想用有健全函数的语言所以干脆把所有表达递归算法的需求都当作不存在的用户(其他用户可能会嫌弃功能太少不像高级语言);基于以上分工演化趋势,这种用户不但不会被视为专业开发者,在非专业用户中也会被边缘化(像只会写自己名字的撑死当半文盲,不会被文化人视作同类一样)。

    为何你会倒置复杂性?看上去是因为不熟这些基础。这些知识只是早期看着很高冷,会随开发技能的普及成为常识而不是专业人员的护城河。一如写代码因为专业门槛限制,一开始只是极少数专业研究人员的特权;现在呢?
    即便以今天的科普程度看,对命令式语言入门的程序员来说,使用 AST 首先也就是空间换时间的粗浅应用,跟数据结构的入门课程就差不远。缺乏这些基本直觉,说明在试图掌握这类比较基础的知识时训练得很不够或效果怎么都不够好,可能更适合具体应用领域方面的工作。

    “你可能没接触过项目遗留屎山……”
    如何应对“项目遗留屎山”,或者说如何高效清理垃圾+背锅,是另一个专业领域的问题。一股脑儿扔给负责设计和实现的人员,这是不务正业。
    FrankHB
        42
    FrankHB  
       2022-03-04 13:17:10 +08:00   ❤️ 2
    @lanlanye

    模式(pattern)这话题不大有趣。背后想要复用设计的需求是真实的,然而基本无解(了解清楚模式是什么不能帮你省多少事)。
    模式在此指工程过程中(被假定)可复用的一些实践。真正容易的部分已经被编码到程序中涵盖了,所以这里说的特指无法通过编程手段自动化复用的部分。

    模式有个共同点:有名字。这提供“行话”,具体意义经常通过局限性(不能做什么,什么不算是模式的实例)体现。

    按规模模式通常分为三类。

    最易局部实现的有时叫代码模式,更常叫做惯用法(idiom)。因为和具体语言特性相关这种经常被视为实现手段而非“设计”模式,但其实不确切,因为一些惯用法(如 RAII )很明确地会影响并传染到跨模块的 API 上,而这至少算得上是一种详细设计。叫成“代码模式”也不甚对路。
    惯用法的意义在于无法被自动隐含到其它代码中隐式复用,要设计和实现人员长点心记得什么时候适合用,但每个实例又不都一样。

    最大的一类是架构模式,涉及到系统整体的宏观设计却又依赖具体项目的经验而具有显著性。讨论架构模式通常较困难,它关注的问题非常大,很多解法和涉众利益相关。除了可用资源,架构模式的选型瓶颈还有涉众的偏好(或者说,眼界),我不认为缺乏具体项目背景的情况下有多少讨论余地。
    架构模式的意义体现在它是一类可复用问题解法抽象层次的上限(其实只要你敢想,问题的规模就没有上界)。只要一个问题的规模似乎足够大,遇事不决就有倒腾架构的余地。但还是那句话,什么问题真能算得上架构问题,得看涉众偏好,有些模式也有模糊性(比如 MVC 能算架构模式也能算设计模式)。一般更小规模的模式能确切解决的问题,都不算架构模式问题;所以说叫“上限”。

    现实不允许你什么都赖架构,又有些不是记几个惯用法就能解决的。这类问题的套路性一般夹在需求和实现之间的阶段中体现;这种高不成低不就的模式,就被称为所谓的设计模式。然而,它要解决的有多少算设计得打问号。虽然不像惯用法高度依赖具体语言特性,设计模式通常依赖范型(paradigm)更甚于问题域。
    例如,OOP 的设计模式和 FP 的设计模式经常不通用,而且对面看来都是冗余的。所谓设计模式跟惯用法没那么大的区别,无非是依赖一个或者几个语言还是依赖一类或者几类语言,中间没有明确界限。

    设计模式中经典的 OOP 的部分,跟 OOD 其实没什么关联(虽然实现滥用类后可能强行对应得上)。后者重点是建模,而 OOP 所谓设计只是如何选择声称支持 OOP 的语言提供的特性,跟建模难以对应。
    语言设计倒是会提供一些直接的对应。比如说“类”,经常是语言而非模式提供。也有例外,如 C 的标准输入 /输出流用 FILE*其实就是一种基于类的 OOP 实现,但似乎没谁把这种实现方式叫模式,连惯用法都不算。
    于是,我不认为类似的所谓模式有多少讨论的意义——既不能帮助明确问题域的边界,又难以涵盖现实存在的(不把模式当回事的用户创造的而不会特意取名)具体语用的惯例,作为单独的复用基础欠缺现实意义。

    设计模式还引出一大堆现实问题。
    第零,滥用。这个之后讲。
    第一,取名不见得有正面作用。
    如很多开发者会把单态(monostate)混淆成单例(singleton),以至于不看实际怎么写的单例就判断不出说的是哪个。虽然在此两者经常有足够大的共性使混淆被容忍,但是要想提防混淆,还不如直接说满足什么需求来得清楚。模式提供名字的作用就整体不那么靠谱了。
    第二,随意强调即便没被滥用,也容易造成刻板印象下的误导。
    一些 OOP 设计模式跟 OOP 也没什么关系。
    比如所谓工厂方法(factory method) ,其实只是对一些类 Simula 的 OOP 语言的类的构造器无法多态的变通,而像 C++的解法 make_xxx 根本用的就是参数多态(parametric polymorphism)而不是 OO 特供的包含多态(inclusion polymorphism),一点都不 OO 好吧。
    如果说这不是惯用法,反而有些怪。但不同语言用不同多态都能实现,都不见得算 OO ,放到惯用法里是不是又太屈尊了?就有点尬。
    第三,可能被语言的演进自然淘汰。
    像 C++17 出来 deduction guide 后很多 make_xxx 就下岗了,因为直接声明对象也不用写参数类型,相当于构造函数隐含了参数多态,那何必多此一举 make_xxx ?
    (不过因为一些理论上的问题,不管能不能有 C++17 用,我都避免 deduction guide 。)
    这倒是从反面体现了一些设计模式的积极作用:揭示日常依赖使用这些模式的语言设计的不足,给语言补充特性指明一个方向。

    再说滥用设计模式。
    多数工程中这也不算是头等问题,因为味儿(如代码中的废话(boilerplate))明显,一眼看得出来,敢不敢砍掉看执行力。更大的问题是阻碍整体语用的理解,进一步阻碍对语言设计、范型偏好乃至选型策略等一系列问题的正常认知。
    OOP 的设计模式啰嗦得太明显所以问题还不普遍( GoF 的书用 C++,然后设计模式长期被 C++用户普遍嫌弃,倒是语言演化一贯后进的 Java 用户更吃这一套才给发扬光大),至少没到 OOP 用户写点程序就想到模式的地步。
    这个问题更多体现在某些 FP 语言用户上。

    典型的反面教材是 Haskell 的 monad I/O 。这里为实现个 do-notation 的语法糖只是个惯用法,但 API 非得这样依赖 monad 的设计就硬生生地强行搞成了“设计”模式。以至于有用户说 I/O 必 monad ,有点纠正不过来了。
    其实 monad 无非是个类似 OOP 的 interface ,顶用的核心原因,甚至都不是 interface 内部能 typecheck 的东西,而是更上层的 monad law 这样的约定(或者说公理(axiom))。然鹅,更基本的其实是 applicative functor……好了,大多数用户到这里就表示投降:不要念经,还是死记硬背 monad 吧。
    这里甚至还没说到杯具的表象:用了所谓的 monadic interface ,相当于写的随便到处 CPS(continuation passing style)变换过的程序,于是 Haskell 用户正常情况下是没资格有用其它大多语言的 direct style 表达 I/O 操作的本事的。(其实也有不 monad 的古董,但是用起来体验嘛……)
    而杯具的实质是啥?是因为 Haskell 用户很多都是 PFP(purely functional programming)教徒,认为无条件默认拒绝副作用是好的,于是为了有效 wrap 必须具有副作用的 I/O primitive 又想藏起来这些味道不对的东西,就想方设法搞出了 monadic interface 的风格来分离再做成语法糖。
    但为何拒绝副作用就好?怕是十个用户九个半都答不对,撑死嘟囔些 referential transparency 就不错了。(然鹅这种解释是错的,PFP 不是支持 RT 的必要条件,OOP 用户都知道还能隐藏内部状态呢……并且,RT 的实际含义比 99%以上的 PFP 语言用户理解的微妙得多,有兴趣补课的搜 Referential transparency, definiteness and unfoldability )。
    真正明白点实质的会知道非得这样 monad 才写得顺,在于 Haskell 的 PFP+默认 lazy eval 的设计(不信改掉这些规则试试)。为什么 lazy 是好的?因为 lazy 能 declarative 。为什么 declarative 是好的?……然后怕是又有不少于八成的概率如蜜传如蜜,绕回到 lazy 就是好的这种同义反复上。
    也不怪这些用户。实质问题的起源是 Haskell 的一些设计者认为 PFP+默认 lazy 能让 equational theory 会足够强,而默认允许 equational reasoning 能便于程序优化啦 blah blah……至于实际情况呢?反正看着 GHC 一坨 thunk 我就积点口德懒得吐槽了。虽然还是特别想骂骂 seq magic 这种又当又立的。
    至于啥叫 equational theory 又如何判断强度呢……算了,超纲太多,自己找 paper 吧,我也弃疗了。。。

    讲到这里,就能看出光一个设计模式就能蕴含多少咖喱味的屎了。
    使用 OOP 模式的语言也有设计的必要性稀里糊涂这问题,然而好巧不巧至少 Java 非常不擅长缩短屎味废话,并且 OOP 的一些 calculi 都比较拉胯在学术界也没 FP 的 model 那么有存在感,所以 OOP 的设计模式(的屎味溢出)还是挺受控的……大部分工业界写 OOP 代码的都会闻得到点味道,可能忍耐的阈值不太一样。

    以上主要围绕设计模式和惯用法的边界展开批判。设计模式和架构模式之间也有暧昧之处,主要例子是 MVC 。
    先前提过架构模式通常会关系到不同的涉众。这里建议 MVC 被当作设计模式还是架构模式,一般主要看各个部分组件预期给谁写。如果设计 MVC 框架,打算具体的 M 、V 、C 实例的设计和实现分包给非框架作者的其他用户,这就算架构范畴。否则,如果都自己搞定,可认为这就是关于设计的。这种设计模式倒是真靠近一点实在的设计。
    但是我还是比较倾向于把这类模式处理成解决架构问题的套路。如果不打算分包出去,其实不用那么纠结组件边界。事实上,主流客户端 GUI 框架声称用到 MVC 或者变体的,这里都不那么规矩,都普遍进行了偷懒而不是提供符合架构要求的职责的组件(好像就 Swing 比较规整一点)。倒是 MVVM 因为一开始就是 WPF 这种先整出来的,边界还算严格。(但 MVVM 是不是适合这样的需求就另说了。)

    所以,整体建议是:惯用法和架构模式看看无妨,当背景知识长见识,被误导风险不大;至于设计模式,不想进 PLT 的坑就别靠太近了,免得积累不够,嘲讽不到位,还被设计模式厨二(虽然也许当事人甚至不知道这叫设计模式)挖坑反埋了。
    (我能在一些坑里跳,是因为有积累能对着一堆鸟语写的 spec 实时脑补出一些 formal model 出来,提前看清楚一些问题,比别人多出来的一些时间就能搜到更多黑历史文献了。不建议一般用户复制这种低效的做法。)
    DDD 偏架构,但主要给管理者看,多数人也不见得有条件实施。
    如果想要从头开始体验抽象,复习 SICP 。先区分清楚抽象掉的能是什么,再考虑建模方法。
    层次不一样具体操作不一样,要求也不一样,不要想着上中下三路一口气通吃。
    搞 PL 的应该关心的是怎么让编程语言更顶用而不是逼着外行用户不得不放出什么设计模式之类的妖魔鬼怪给语言功能缺陷擦屁股。
    工业界负责落实实现的普通用户应该致力达到一个合格水准:看到什么需要设计的东西,就要想着尽量用项目允许的语言趁手写出来(或者保持有底气造反推翻不恰当的选型决策),不看任何所谓的设计模式,也要做到能随手发明出来怎么实现,不在详细设计上阻塞。至于命名,不是想要找别人的废话批判一番的就算了,别添乱了。
    精力过剩就找 leader 讨论需求和针对问题域的真正的设计,不要花大把时间纠结在如何给实现方法擦屁股的伪设计上,除非你致力于成为屁股理论专家。

    最后跑点题,所谓语法(syntax)也是一坨 formal pattern 。现在 PL 界看来折腾语法过头,特别是搞 type theory 的整个拿语法当方法论整,而对语义注意经常不足,有滥用模式的传统艺能味儿了(即便这 pattern 不需要人去抄)。和一些设计模式的鼓吹者之间有类似共通问题:希望整出一些能摆脱 case by case analysis 而允许机械地复用的东西偷懒,却忽视了和实际需求的联系。
    3dwelcome
        43
    3dwelcome  
       2022-03-04 15:57:35 +08:00
    楼上也太能写了,当然对于函数我个人还是持保留意见。

    就和这个世界生物多样性一样,看起来早履虫比人差了很多,但万物进化,皆有存在的理由。

    代码也是如此,指令是函数的退化形态,退化就一定落后嘛,也不一定的。适合场景就好,函数是写不完的,代码退化意味你可以把精力省下来,用到别的地方。

    以前有句名言,keep it simple stupid ,代码写傻点没什么不好,太复杂是炫技,足够简单才是代码长寿的秘诀。与其写茫茫多的函数,堆代码,我更喜欢在项目里删代码。
    lanlanye
        44
    lanlanye  
    OP
       2022-03-05 10:50:33 +08:00 via iPhone
    @v2exblog 分层问题简单来说就是使用分层架构,我接触到的分层架构主要有两种,一种规定每层只能访问自己下面一层,另一种规定每层都可以访问下方所有层,我觉得两种都行。
    CRUD 就是跟着数据模型走,设计好数据库,本质上就是开发一个界面好看的数据库管理工具。
    如果你的业务很复杂,无法用这种简单的方式实现,则可以参考 DDD 中的方式对领域进行分析。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   952 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 21:51 · PVG 05:51 · LAX 14:51 · JFK 17:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.