0%

前言

人们为电脑编程超过80年了,但是很奇怪的是很少有人谈论怎么去设计这些程序或者什么样的程序是好程序。有讨论关于软件开发过程,如敏捷开发和开发工具诸如debuggers,版本控制系统,代码覆盖工具。同时还有软件扩展分析比如面向对象变成和功能变成,设计模式和算法。所有这些讨论是有价值的,但是软件设计的核心问题仍没有触及。David Parnas的分类论文“On the Criteria to be userd in Decomposing Systems into Modules” 发表于1971,但是软件设计在这之后的45年的进步仍没有超越这个论文。

计算机科学最基础问题是‘问题分解’:怎么把一个复杂的问题拆解成碎片,以至于可以独立去解决。问题拆解是编程者每天面临的主要问题。然而和这里说的不同的是,我没有在任何大学发现有问题拆解这门核心课程。我们重复地教面向对象编程,但是不教软件设计。

此外,这对编程者的产出量和产出质量有巨大变化,但是我们很少尝试去理解什么使编程者变地更好或者在课堂上教这些技巧。我和一些我认为伟大的程序员朋友交流过,但是大部分人很难说出给他们优势的关键技术。很多人认为软件编程技巧是一个先天的东西,很难教会。然而,有充足的科学证据表明理解表现在很多领域和高质量的练习更有关系而不是先天能力。(比如发表在Geoff Colvin
的‘天才被高估了’)

这些问题困惑我多年。我开始怀疑软件设计是否能后天学习,并且我假设软件设计技巧是区分一般开发者和伟大开发者的因素。我最后决定回答这些问题的唯一办法是尝试去开设移门软件设计的课程。结果就有了 CS190的斯坦福大学课程。在这门课上,我加强了软件设计原则的阐述。这门课有点类似传统英语写作课程。在英语课上,同学用写草稿,得到反馈,然后再写一遍来提升这样一个迭代过程来提高。在 CS190上,同学将从草稿中开发大量的碎片化的软件。然后我们通过广泛的代码审查来识别设计问题,然后同学修改他们的工程。这样同学可以看到软件设计原则是如何改进提升他们的代码质量的。

我现在已经开设这门软件设计课程3次了,这本书是根据这三次的课堂上提出的设计原则写的。原则高度抽象和哲学接壤(定义不存在的错误 Define errors out of existence),所以同学们很难理解也是正常的。通过写代码是同学最好的学习方式,通过犯错误,并用方法去修正错误可以看到他们进步的过程。

在这个阶段你可能会问:是什么使我认为我知道所有软件设计的问题呢?答案是我不知道,我学习编程时没有软件设计的课程,并且从没有一个导师教我软件设计原则。我学编程时,代码审查还不存在。我的软件设计方法来自我个人的写代码和看代码的经验。在我的职业生涯中我用多种语言写了大约250,000行代码。我参与了3个操作系统的设计,多文件和存储系统,基础工具例如debuggers,build systems和GUI工具集,一门脚本语言,写文本,画画,演讲和集成电路的交互式编辑器。经历这些使我有了处理大系统的经验和掌握了大量设计技术。此外,我读了很多别人写的代码,让我接触了各种方法,好的和坏的。

除这些经历外,我尝试去抽离共同特征,关于错误避免和技术使用。这本书是我经验的映射:每个描述的问题是我个人经历,每个建议和技巧是我在我写的代码中获得验证的。

我不希望这本书是软件设计的最终版:我很确定有很多技巧我忽略了,并且一些我的建议从长远看可能会是bad idea。然而,我希望这本书能使软件设计变成热门讨论的话题。把这本书中的经验和你的实际经验结合,去发现它是否能减少软件的复杂度。这本书代表我个人观点,所以有些人会不认同我的一些建议。如果你也是,尝试去理解为什么。我很乐意去听取你对于软件设计的其他观点。我希望这些交流可以使大学对于软件设计有更深的理解。我会持续修正这本书的内容。

与我交流关于这本书的内容可以用email:software-design-book@googlegroups.com

我很乐意听到关于这本书特别的反馈,比如bugs或者改善的建议,关于软件设计的一些想法和经验。最好的例子阐述了一个重要的设计原则并且用一两句就能解释清楚。你如果想看看其他人的建议,可以加入Google Group software-design-book。

如果因为一些原因Google Group software-design-book Google Group在未来停止了,搜索我的个人主页;你可以看到联系交流关于这本书的方式。请不要发送本书相关的内容到我个人email。

我建议你接收这本书的建议。整体目标是减少复杂性:这比其他原则和思想更重要。如果你尝试了一个方法但是没有减少复杂度,你就没必要保留他(但是,让我知道你的经历,我希望知道什么工作让你发生这种情况)。

很多人提供了批评或建议改善这本书的质量。以下人士对本书提供了帮助:。。。

第1章

引言

(都是关于复杂度)
软件开发是人类史上最纯洁的创造活动。编程者不会被诸如物理定律这些规则约束:我们可以创造令人激动的虚拟世界,是这个世界上不存在的世界。编程者不需要物理或协调技巧,比如棒球或者篮球。所有的编程者需要一个创造力的思想和组织你想法的能力。如果你能想象一个系统,你很可能用软件编程实现。

这意味着限制我们编写软件的能力在于理解我们创建的系统。随着一个软件需要越来越多的特征,它会变得复杂,有很多的依赖。同时,复杂度积累,使编程者在修改时越来越难关注到所有细节。这使得开发速度变慢并且导致bug,增加了开发成本。任何程序都避免不了复杂度的增加。程序越大,开发者越多,越难管理复杂度。

好的开发工具可以帮助我们处理复杂度,很多伟大的工具在过去的几十年被创造出来。但是仅仅使用工具有局限性。如果我们想让写程序更简单,以至于我们可以更廉价地创建更强大的系统,我们必须找到方法使写软件更简单。简单设计可以使我们在复杂度出现前构建强大的系统。

有两个通用方法解决复杂度,在本书后面会讨论。第一个方法是使代码更简单和明显。例如,复杂度可以通过使用特例或者稳定版本去减少。

另一个关于复杂度的方法是压缩它,这样编程者可以在一个不会一次性暴露所有复杂度系统工作。这个方法被称为模块化设计。在模块化设计中,一个软件系统被拆分为模块,比如在面向对象语言中的类。模块被设计成相互独立,这样编程者可以在不需要清楚了解模块内细节的情况下使用模块。

因为软件太有扩展性,软件设计是一个持续改进过程,这使得软件设计和物理系统设计不同,比如造房子,船,或者桥。然而,软件设计并不是一直这样。在大多数编程历史上,设计往往集中在项目的早起。这被称为瀑布模型,一个工程被拆分为离散的阶段,比如需求确定,设计,编码,测试和维护。在瀑布模型中,每个阶段在下个阶段开始前完成。在很多例子中,不同人负责不同阶段。整个系统设计一次,就在设计阶段。设计在这个阶段结束后被冻结,随之而来的阶段是去实现这个设计。

不幸的是,瀑布模型在软件设计中并没有很有效地实施。软件系统往往比物理系统更复杂。对于一个很大的软件系统,很难预测到所有的具体实现。随之而来的是起先的设计会出现很多问题。这些问题不容易发现直到实现阶段。瀑布模型很难相应这种情况(比如,设计中被调到另一个项目中)。这样,开发者开始对起先的设计打补丁,这使得问题越来越复杂。

第2章

复杂性的本质

这本书是关于怎么设计软件系统去减少复杂性。第一步是去理解敌人。具体来说就是“什么使复杂度”。你如何确定一个系统是不必要地复杂?什么导致了它复杂?这章会把问题提升一个高度,下一章会降低一个高度向你展示如何辨识复杂,用特定的特征。

辨识复杂度是设计的关键技巧。它让你在投入前发现问题,使你做出准确的判断。相比创造一个简单的设计,判断这个设计是否简单相对容易。一旦你发现系统太复杂,就可以指引你把它设计地简单。如果设计太复杂,尝试用不同方法使设计简单。长期以往,通过把复杂变简单,使你设计越来越顺手。

这章还展示一些框架的基础猜想。下一章会基于这章下一些定论。

2.1 复杂度定义

我用实践的方法定义复杂度。“复杂度是关于理解一个软件系统的结构容易与否和修改的难易程度。”复杂度有很多形式。例如,有可能很难理解一句代码的意思,实现一个小的改动很不容易,或者不清楚要修改哪部分来做出调整。如果软件系统很难被理解和修改,那么它很复杂。好理解,就简单。

你也可以认为复杂度是耗时和收益。在一个复杂的系统中,花费了很大时间去实现一个小改动。在简单系统中,大改动也可以很快地实现。

复杂度是开发者为了实现某个特定目标所要花费的精力。和整个大小,系统功能无关。人们往往用复杂去描述一个很大的系统,但在这本书中,如果这个系统很容易操作,那么它不复杂。当然,大部分大的系统很难学会,他们复合复杂的定义,但不是绝对。小的系统也可能复杂。

复杂度是由不同活动的共同部分定义的。如果一个系统只有一小部分复杂,但是那部分不需要修改,对于整体的复杂度影响就不大。用数学的方式来看可以这样描述:
C=求和(西格玛)CpTp
整个系统的复杂度C是由分部的复杂度p加成得到的。

复杂度对读者相比对作者更显而易见。如果你觉得代码简单但是其他人觉得复杂,那么它是复杂的。让其他人解释为什么他们觉得复杂很有必要。可能会有很有趣的事情遇到。你的工作是写不仅你自己觉得简单的代码,同时也让其他人工作更简单。

2.2 复杂度的特征

复杂度的表现通常有三个形式。下面会细说,每一个都会令开发复杂。

变化放大:

第一个是一个简单的改动需要改动很多不同地方的代码。例如,一个网页有很多页,每页都有一个banner展示背景色。在早期的很多网页,每个颜色都在每个页面单独声明。
Figure 2.1a
如果要修改,开发者需要改每一页。幸运的是,现在网页开发都会把这些属性声明一次。
Figure 2.1b
共享这个属性。一个好的设计有减少代码量的目标,集中处理使修改难度降低。

认知负荷:

第二个是认知负荷,意思是开发者需要额外学习多少去实现一个任务。高负荷意味着开发需要花大量时间学习,那么就有产生bug的高风险,因为他可能漏学一些重要的部分。例如,假如一个函数C分配了内存,返回一个指针指向那片内存,假设调用者会释放那片内存。这增加了认知负荷。如果开发者没有释放那片内存,就产生了内存泄漏。如果系统被设计成不需要关系释放内存,就减少了负荷。认知负荷在很多地方,例如API的方法,全局变量,模块间的依赖。

系统设计者有时假设复杂度可以被代码行数衡量。他们认为一个实现比另一个短,那么它就简单。很少的代码做出很大改变,那么变化就是容易的。然而,这些观点忽略了认知负荷的成本。我用过一些框架,调用代码非常简单,但是很难理解这些代码做了什么。
有时一个方法有更多行数的代码实际上更简单,因为它减少了认知负荷。

不知道不知道的情况:

第三个特征是需要哪些代码去实现一个任务,或者哪些必要信息必须携带去实现任务不明显。
Figure 2.1c
网站使用一个中间变量去实现横幅背景色,所以变得容易去更换。然而,一些网页用暗色阴影作为背景色来强调,在特定页有更深的颜色。如果背景色改了,强调的颜色也需要更改。不幸的是,开发者不喜欢实现这个,所以他们更新横幅背景而不更新强调色。即使开发者意识到这个问题,哪个页面用了强调色并不明显,所以开发者可能需要搜索每一页。

在复杂度的这三个表现中,最后一个是最严重的。有一个不知道的问题意味着有一些你需要去知道,但是你找不到它是什么,或者它是否有问题。直到出现bug前你都不知道怎么去改。修改应用很恼人,一个高认知负荷会增加修改成本,如果哪部分信息要阅读很明显,修改似乎是正确的。唯一了解全部的方法是读每一行代码,对于大系统来说几乎不可能。并且不高效,因为修改后的依赖问题并不会被声明。

一个重要的目标对于一个好的设计来说就是明显。这和高认知负荷和不知道不知道情况相反。在一个明显的系统中,开发者能够快速理解存在的代码是如何工作的,需要如何修改。一个明显的系统是开发者能够快速猜测需要做什么,不需要思考很久,并且对猜测很肯定。第18章讨论使代码更明显的技巧。

2.3 复杂度的认知

现在你明白了复杂度的高层特征和为什么复杂度使软件开发更复杂,下一步是理解什么导致复杂度的产生,这样就可以设计系统去避免问题。复杂度由两个事情产生:依赖和隐晦。
这部分讨论这些因素在一个高的层次,下一章在低的层次讨论。

出于这本书的目的,依赖存在于不能孤立和不能被理解的代码中,代码和其他代码有某种关联,如果代码修改了,依赖的代码也需要作出调整。例如Figure2.1a,背景色依赖存在于每一页。所有页需要相同背景色,所以一个页面的背景色改了,其他页也一定要改变。另一个例子是依赖存在于网络协议。具体来说有两部分独立代码控制发送和接收,但是它们都要遵守同一个协议,更改发送端的代码往往需要改动接受者的代码,反之亦然。方法的签名导致方法实现和代码之间的依赖。如果在一个方法增加参数,所有与这个方法有关的部分都需要调整来适应那个参数。

依赖是软件基础部分然而并不能完全消除。实际上,我们引入依赖作为软件开发的一部分。每次你写一个新类都会创建一个围绕这个类API的依赖。然而,软件设计的一个目标是减少依赖的数量,让依赖更简单和明显地表示出来。

考虑网页的例子。在早期网页每页的背景色都独立声明的情况下,每页都相互依赖。新的设计通过声明一个公用API去获得背景色。消除了每页的依赖,但是制造了新的依赖,即接收背景色的依赖。幸运的是,新依赖更明显。开发者可以通过搜索名称找到。更多的是,编译器帮助去管理API依赖。如果共享变量变了,引用老的名称的地方编译器会报错。新的网页设计将复杂依赖变成了简单明显的依赖。

第二个导致复杂性的因素是隐晦,隐晦发生在重要信息不明显。一个简单例子是一个变量名没有足够的描述信息,(如time)。获得它的唯一途径是搜索变量,看哪里用到。晦涩经常和依赖同时出现,依赖不明显。如果两个类中命名一样,没有其他信息,开发者很难分辨两者区别。

一般来说,依赖和晦涩导致了三大复杂度的因素。依赖导致修改更困难,增加了认知负荷。晦涩导致了不知道的不知道增多,同样增加了认知负荷。如果我们找到方法减少依赖和晦涩,我们就减少了软件开发的复杂度。

2.4 复杂度增加

复杂度并不是由单一的巨大错误产生的,它是由很多小错误积累而成的。一个简单的依赖和晦涩,它自己来说,并不足以影响整个系统。但是积少成多,数量足够多时会导致复杂度。最后,任何小的改动都会影响整个系统。

2.5 结论

复杂度来自依赖和晦涩的积累。复杂度增加了,导致修改难度增大,更大的认知负荷,随之而来更多的不确定。使后期增加一个新特性变得需要更多代码。此外,开发者消耗更多时间来保证改动是安全的,最差的是,他们并不能及时找到需要的信息。底线是复杂度导致修改存在的代码有很高的风险。

第3章

只写代码是不够的

(策略 vs 短程编程)
一个好软件设计最重要的元素之一是完成编程任务的思维。很多组织鼓励开发一个有预见性的系统。可以快速响应未来的变化。然而,你想要一个好的设计,你必须花大量时间用一个好的策略。这章会讨论为什么从长期看策略用于设计优于短程开发。

3.1 短程编程

大部分软件开发者用一种策略我称之为短程开发。在短程开发中,你主要关注实现一个功能或者修复一个bug。第一眼看很合理:有什么比写一段有效果的代码更重要呢?然而,短程编程不能提供一个好的系统设计。

短程开发的问题在于它很狭隘。你使用短程开发,为了快速完成一个任务。也许你有一个截止期限,所以计划一个好的未来并不是首选重要的事情。你没有花大量时间在寻找好的设计,你只是想快速解决问题。你想的是写一堆代码去实现功能,越快越好。

这就是导致系统变得复杂的原因。在早期章节的讨论中,复杂性是逐步递增的。不是一个特定的问题使系统变得复杂,而是整体的累加。如果使用短程编程,每一个编程任务都会增加这个复杂度。每个任务单独看似乎是解决问题的最短路径。然而,复杂性增加了,特别是每个人都用短程编程。
———

不久以后,部分复杂性会开始导致问题,你开始后悔早期的写法。但是你认为修改之前的思路比开发当前需求更麻烦。重构长期看有益,但是会影响你当前任务的完成。所以你还是使用补丁的方式去实现,绕过你遇到的问题。这使得系统更复杂,后期需要更多补丁。很快代码变得一团糟,需要花费数月去清理。没有一个项目可接收如此长的耗时,所以你继续使用短程编程策略。

如果你在一个大项目中工作很长时间,我相信你已经遇到过短程编程导致的问题。一旦你开始使用短程编程,就很难改变。

几乎每个软件开发组织有至少一人使用极端的短程编程:一个短程风暴。短程风暴是一个多产的编程者,写的代码比其他人快很多但是使用一些短程技巧。就轮实现任务来说,没人可以快过短程风暴。在一些组织,管理者把短程风暴视作英雄。然而,短程风暴会留下灾难性的破坏。他们很少考虑未来的扩展。并且其他开发者需要整理短程风暴留下的问题,让他们从表面看起来比短程风暴更慢了。

3.2 策略编程

变成一个好的软件设计者第一步是意识到只写代码是不够的。为了尽快实现你当前的需求而引入不必要的复杂性是没有必要的。重要的是系统长期的结构。大部分代码在一个系统中是为了扩展已经存在的代码,所以你重要的工作是促进这些扩展。因此,你不能把’写代码‘变成你的首要目标,尽管你的代码必须有用。你的首要目标必须是做一个好的设计,同时工作。这就是策略编程。

策略编程需要一个投资的观念模式,而不是快速实现当下需求,你必须花时间去设计系统。这些投资短期会减少你的进度,长期来看会加速整体的时间。

一些投资是积极主动的。例如,设计每个新的类是有必要花时间的。而不是实现出现在脑海的第一映像,尝试一组设计然后选择最干净的一个。尝试想象未来的扩展可以帮助你设计。写一个好的说明文档也是一个例子。

不管你考虑如何周到,避免不了遗漏的情况。过一段时间,这些错误会明显。当你发现一个设计问题,不要忽略它或者打不到。花一点时间去修正它。

3.3 花多少去投资?

所以正确的投资数量是多少?过度的投资,例如重新设计整个系统,是没有效的。设计思想是一点点出现的,从你的经验和以往的系统中获取。因此,最好的方法是花少量的投资在一个持续的项目中。我建议花10%-20%的时间在投资。这点程度不会影响你整个计划,但是足够多去设计好的模板。你早期的项目会多这10%-20%的开发时间。但是会使软件未来更好,未来每个项目可以节省10%-20%的时间。

3.4 开始行动和投资

在一些环境有很多因素阻止使用策略编程。早期有强大的外界压力要项目快速实现。在这些阶段,前面说的10%-20%时间是不能接受的。导致很多项目开始就是短程编程,花少量时间在设计,甚至一些问题也不改了。他们的理念是,如果项目成功了,会有足够的钱雇佣更多的人维护修复这些问题。

如果你在的公司是这样的发展方向,你要意识到一旦代码变得和意大利面一样,几乎不可能修复。你需要花费大量的开发时间去维护这个产品。随之而来的问题让后期版本更慢上线。

Facebook 是一个早期使用短程开发的例子。在早期的时候,Facebook的公司价值观是”Move fast and bread things“。 他们鼓励员工快速实现功能。当Facebook逐渐成为成功的公司时,他们发现代码越来越糟糕,变得不稳定和难以修复,他们意识到整个系统都已经不稳定。最后,Facebook更改了公司价值观”Move fast with solid infrastructure“,去鼓励员工做好的设计。

公正地说,Facebook早期的代码其实比平均早期公司的代码要好。短程编程在公司的起步阶段非常普遍。Facebook只是其中之一。

在硅谷的其他公司,也有使用策略设计的公司。谷歌和VMware和Facebook差不多时间开始发展,他们都遵从策略设计,非常强调高质量的代码和好的设计,他们的系统也十分可靠。

3.5 结论

好的设计不是平白无故产生的。一定是你长期投资,所以小的问题不会积累成大问题。幸运的是,好的设计最后会给你想不到的回报。
最有效的方法是所有人都为好的设计做一部分工作。

第4章

模型层次应该足够深

一个管理软件复杂度的技巧是模块设计,它的作用是让开发者只关注一小面从而降低开发难度。

4.1

模块设计

在模块设计中,软件系统被拆分成独立模块。模块可以以很多形式存在,例如类、子系统、服务。理想中,每个模块间相互独立:开发者可以在不知道其他模块的情况下开发一个慕课。实际上系统的复杂度在于最差的模块的复杂程度。

不幸的是,这个理想情况是不可实现的。模块必须通过方法或函数连接。随之而来的是,模块必须相互了解。这使得模块间产生依赖:如果一个模块变了,其他模块必须修改来适配。模块设计的目标是减少模块间的依赖。

为了管理依赖,我们把模块拆成两部分:一个接口和一个实现。接口说明如何使用但不会说明它是如何实现的。开发者需要知道如何使用它。

一个模块可能很复杂,但它的API很简单易懂,使用者不需要关心复杂的实现,只需要花时间知道如何使用它。

在这本书中,模块是任何代码的实现。任何函数,即使不是面向对象的也被称为模块。模块被设计成很多形式。高层次的子类和服务也是模块,他们的接口以多种形式呈现,例如内核调用或者HTTP请求。

最好的模块是那些接口比实现简单的。这些模块有两个优点。第一,简单的接口减少了指向其他系统的复杂度,第二,如果模块变化不会导致接口变化,那么模块改动就没有依赖英雄。如果接口足够简单,模块内部修改导致的影响越小。

4.2 什么是一个接口?

一个模块的接口包含两个信息:正式的和不正式的。
正式的是指明确在API中声明的,包含参数的类型和名称,是公开的变量。

不正式的是指那些高层的行为,例如一个函数会删除以一个参数命名的文件。在调用一个方法前也许需要调用另一个函数。这些隐含信息只能通过注释的行为添加到API中,这比正式的部分要复杂的多。

声明一个明确的接口是为了让开发者明确知道如何使用模块。可以帮助减少“不知道的不知道”的情况。

4.3 摘要

摘要是阐述一个模块的思想。摘要很重要,他是描述如何实现的。

在模块编程中,每个模块需要一个摘要来描述接口。它会简述一个模块的功能,细节是从接口描述的。摘要描述整个模块需要使用者知道的重点,不需要描述细节。

4.4 深模块

好的模块是提供强大功能但是简单的接口。我使用“深度”这个词秒速模块。从图中看,好的模块是很深的因为它把复杂性包含在模块内部,暴露尽可能简单的接口。

模块的深度是花费的时间和收益的比例。收益是指模块提供的功能。花费的时间是接口的复杂度。接口越小,模块越简单。

Unix和它的后代使用的文件I/O的机制,例如Linux,是深模块的例子。只有5个基础系统调用方法提供:

1
2
3
4
5
int open;
ssize_t read;
ssize_t write;
off_t lseek;
Int close;

这些简单的方法背后是有成百上千行代码完成的。随着时间推移,内部实现进化了很多代,但接口没有变化。

另一个例子是垃圾回收机制,存在于Go或者Java,这些模块甚至没有接口,它默默地工作。
深度模块使用简单,但是隐藏了非常复杂的实现。

4.5 浅模型

从另一个角度说,浅模型是一个糟糕的接口,增加了功能的复杂性。描述比本身功能还要复杂的模型会增加复杂度。

4.6 过度分类

不幸的是,很多学生被灌输了过渡拆分的概念,他们认为一个类超过一定行数代码就要拆分,这使得类越来越多,增加了复杂度。很多错误发生在有“分类是好的,越多累越好”的理念。

4.7 例子,Java和Unix I/O 的对比

FileInputStream 只提供初级的 I/O 功能,既不能读也不能写。BufferedInputStream 添加了缓存,ObjectInputStream 添加了读和写的功能,但是FileInputStream和BufferedInputStream在文件打开后并没有其他作用了。后面的操作只用ObjectInputStream实现。

这是非常恼人的如果buffer被遗漏了,没有缓存I/O读写变得非常缓慢。也许java开发者会说不是所有人想要使用buffering对于文件读写,所以它不应该在基础机制里。他们可能认为使buffering分离,这样使用者可以选择是否使用它。提供选择是好的,但是接口应该设计的越简单越好。几乎所有人需要buffering,所以它应该默认被使用。在这个例子,库可以提供一个方法不使用它,这样大部分开发不需要知道有这个东西。

相反地,Unix系统设计地很简单。随机访问是很少用的,但使用也很简单,通过lseek系统调用,所以很多人都不需要知道这个机制。如果一个接口有很多特性,但是大部分开发只需要知道一部分,这样复杂性只针对使用这部分不常用API的人生效。

4.8 结论

通过分离接口和实现,我们可以隐藏系统复杂的实现。使用者只需从接口了解使用方法。重要的是设计一个深度足够深的类,使接口简单但是仍提供强大的功能。这样就把复杂度隐藏了。

第5章

信息隐藏(泄漏)

这章和后几章讲如何创建深度足够深的模块。

5.1 信息隐藏

实现深模块最重要的技巧是信息隐藏。这个技术是 David Parnas 第一次提出的。基础思想就是模块应该压缩到少量知识,能代表设计者的决策。这个知识点隐藏在模块的实现而不是接口,所以它对于其他模块是隐藏的。

这个隐藏的信息一般是如何实现一些机制。这里有一些可能不被隐藏在模块的信息例子:
怎么在一个 B-tree存储信息,并且高效地访问?
如何在一个文件内辨别物理磁盘的block对每个逻辑block的响应?
怎么实现TCP网络协议?
如何在一个多核的进程计划多线程?
如何解析json文件?

这些隐形信息包含数据结构和算法机制。还包含低层级的细节,例如一页的大小,还有高层的概念,他们更抽象,例如假定大部分文件很小。

信息隐藏从两方面减少复杂度。第一,它简化了模块的接口。接口反应了一个更简单,更抽象的模块功能视角,减少了认知负荷。第二,信息隐藏使得优化系统更简单。如果隐藏了部分信息,那么认为它对外没有依赖,外部的升级不会影响模块内部。例如,如果TCP协议改了,协议的实现需要修改,但是高层代码使用TCP发送和传输数据不需要修改。

在设计一个新模块的时候,你应该仔细考虑什么信息能够被隐藏在模块。如果你隐藏更多信息,你同样可以简化模块接口,使模块更深。

注意:将变量和方法在内部用private修饰并不是这里指的信息隐藏。私有声明确实可以帮助信息隐藏,它使外部不能访问。然而,私有item还是可以通过getter和setter访问。

最好的信息隐藏形式是当信息隐藏后,使它和使用者无关。

5.2 信息泄露

和信息隐藏相反的是信息泄露。信息泄露出现在一个设计决定反应在多个模块中。这使得模块间多了依赖:任何有关那部分设计的改动都会影响依赖的每个模块。然而,信息泄露还存在于即使没有暴露接口的模块中。例如两个模块都依赖一个文件格式,一个模块要读,一个模块要写,如果这个文件格式改了,两边的交互就有问题了。像这样的后门泄露比一般的信息泄露更可怕,因为它还是隐藏的。

信息泄露是一个红色标志,一个避免它的方法是提高对它的敏感程度。一旦你发现有可能出现的地方,问一下自己”如何修改这个类才能让这个知识点只影响一个类?“。另一个方法是找到所有相关的类,将这个方法独立出去。

5.3 暂时分解

一个常见的导致信息泄露的设计风格我称之为暂时分解。在暂时分解中,系统的结构和操作的顺序有关。考虑一个应用用一个特定格式读取一个文件,修改文件的内容,然后重写一遍。通过暂时分解,这个过程被分为三部分,一是读文件,二是修改,三是写。读和写的步骤都会导致信息泄露。解决方法是将读和写归到一个类中去。你很容易掉入暂时分解的陷阱,因为分解操作会在你编码时出现在你脑海。

在设计模块时,关注需要完成每个任务的知识点,而不是每个任务发生的次序。

5.4 例子:HTTP 服务

5.8 在一个类中信息隐藏

信息隐藏不仅在对外的API中,内部实现也可以用到。尝试在一个类中设计私有方法压缩信息。此外,尝试减少每个实例变量调用次数。一些变量也许要被整个类访问,但是有些只需要在很少地方用到。如果你能减少变量使用次数,那你可以减少依赖和复杂度。

5.9 不要做过头

如果是一些配置外部需要使用,不要过度隐藏它。作为软件设计,你的目标是减少模块使用的信息。例如,如果一个模块可以自己完成配置,那么比暴露配置接口要好。但是,重要的是你要分清哪个信息是被外界所需的然后暴露它。

5.10 结论

信息隐藏和深模块关联性很强。如果一个模块隐藏了大量信息,会增加模块提供的功能减少它的接口。这样就时模块更深。
当分解一个系统到模块时,不要被执行的次序影响。多考虑实现功能需要的不同知识点。然后压缩这些知识点。

第6章

通用目的的模块要更深

一个你常遇到的要做的决定是把一个新模型设计成通用模型还是特例模型。一些人认为你应该做成通用,这样可以用到很多地方,而不是只解决当下问题。在这个例子,新机制能够发现未来的扩展来节省时间。就像第三章讲的那样,用少量时间来节省未来的大量时间。

另一方面,我们知道很难预测未来的需求,所以通用设计可能会有一些实际不需要的多余特性。如果太通用,有时很难解决当下问题。随之而来的是,有人指出要优先解决当下问题。如果你用特例,然后在未来需要时改成通用也可以。

6.1 以通用为目的去封装类

在我的经验中,最佳方法是以通用为目的去写每个新模块。这里说的以通用为目的指的是模块功能应该反映你当下需求,但是接口不是。接口应该足够通用可以支持多方调用。接口应该足够简单,当下需求调用时并不需要特殊声明。不要为了通用设计的使实现当下需求变得难以理解。

最重要的是通用写法会让你未来更省力,如果你用这个类做扩展。然而,即使模块只是用于原始需求,以通用目的而写的方法也更好因为它足够简单。

6.2 例子:编辑器存储文本

假设我们要学生做一个GUI文本编辑器。编辑者可以点击,输入来编辑文件。编辑器要支持多端同时访问,要支持多层撤销和修改。
学生的例子:
void backspace(Cursor cursor);
void delete(Cursor cursor);
void deleteSelection(Selection selection);

6.3 一个更通用的API

void insert(Position position, String newText);
void delete(Position start, Position end);
Position changePostion(Position position, int numChars);
text.delete(cursor, text.changePostion(cursor, 1));
撤销可以这样实现:
text.delete(text.changePostion(cursor, -1), cursor);
扩展:
Position findNext(Position start, String string);

6.4 通用会帮助信息隐藏

通用目的的方法提供文本和接口类分离的方法,做了更好地信息隐藏。文本类不需要被使用者知道,比如撤销功能如何实现,这些细节被压缩到一个接口类中。新的功能很容易添加通过之前的接口类。开发者只需知道几个很少的函数去实现新的功能。

6.5 问你自己的问题

识别一个通用目的的类比创建一个要更容易。这里有一些你可以问的问题,可以帮助你找出使用通用目的还是特例来提供接口。

解决我当下的问题哪种接口更简单?
如果你要引进很多额外参数去降低方法数量,实际上并没有简化。

这个方法可以用在多少情况下?
如果可以用在很多地方,用通用的目的去设计。

这个API当下使用简单吗?
如要你需要额外写很多代码去用它,那还不如用特例解决。

6.6 结论

通用目的接口相比特殊目的接口更有优势。他们表现简单,但是深度很深。尝试让你的模块以通用目的去开发可以降低系统的复杂度。

第7章

不同的层,不同的抽象

软件系统是由很多层组成的,高层是由底层的服务为支撑的。一个好的设计系统,每层都可以为上层和下层提供支持。

如果一个系统的层有相似的抽象功能,是容易产生问题的。这章讨论这种情况发生的场景,发生导致的问题,和怎么消除这个问题。

7.1 ‘通过’方法

如果临近的层有相似的抽象功能,问题往往会由 通过方法 出现。‘通过’方法指的是方法接口和内部实现接口非常相似。例如:
public Character getLastTypedCharacter() {
return textArea.getLastTypedCharacter();
}
这个就是‘通过’方法,它只是一个传递函数的作用。

通过方法增加了类的复杂度。实际上也没有为系统增加功能。

7.2 什么时候的接口重复是ok的?

有相同功能的方法并不总是不对的。重要的是提供新的功能,通过方法不好是因为它本身没有功能。

一个ok的例子是dispatcher调度方法。调度方法是指通过参数选择合适的方法给使用者。因为它有新的功能,就是帮助你选择一个合适的方法完成任务。

7.3 修饰

修饰对象是使存在的对象有扩展功能。在第4章中 Java I/O 的例子,bufferedInputStream 类是一个修饰。bufferedInputStream 提供了一个相似的API但是引进了buffering的概念。

修饰的目的是把特例目的的扩展和通用部分分离。然而,修饰类变的狭窄,它额外提供的功能小于带来的复杂度。它往往带来很多 ‘通过’方法。所以创建一个修饰类时,考虑以下问题:
你是否能提供一个直接的新功能,而不是创建一个修饰类?
这个新功能是否特殊到需要一个特别对象,是否可以把它和已有的类合并,而不是创建一个修饰类?
是否可以和已有的修饰类合并而不是创建一个新的?如果合并,反而可以增加模型深度。
最后,问你自己新功能是否真的有必要去包裹已存在的功能?

一些修饰可能会起到效果,但是往往还有别的更好地方案。

7.4 接口和实现

另一个影响不同层,不同深度原则的是类的接口应该和实现不同,即是具体的实现和抽象的接口不同。如果相同,说明这个模块不够深。例如上面编辑器的例子。

7.5 ‘通过’变量

API复制的另一个形式是‘通过’变量,指的是一个变量通过一长串方法传递。通过变量增加了复杂度因为每个中间方法都需要知道这个变量的存在。如果要修改这部分也会增大难度。

提供三个优化方法:

  1. 使用共享、单例的形式
  2. 使用全局变量
  3. 封装到一个上下文对象中

推荐使用3,如果要增加一个变量,增加到上下文对象中。控制器之间的传递代码就不用动。也更容易测试,修改上下文中的参数可以测试各种情况。

上下文是一个理想的解决方案。比全局变量更好。全局变量让你困惑在哪里要使用到。但是上下文对象也会导致线程安全问题,最好的方法是使上下文中的内容不可变,目前我也没找到更好的方法。

7.6 结论

每一部分加入到系统,比如接口、实现、参数、函数、类都会增加系统复杂度。消除复杂度可以通过设计消除部分表现元素。例如类通过压缩隐藏部分功能来隐藏使用者不需要知道的知识点。

不同层,不同抽象的原则是这个思想的应用。如果不同层有相似功能,例如‘通过’方法和修饰,它们可能没有提供足够的效益。还有‘通过’参数增加了中间层的负担。

第8章

降低复杂度

这章会从另一个角度考虑创建深模块。假设你在开发一个新模块,你发现了不可避免的复杂度。哪个更好?你是否应该让用户解决复杂度,还是在模块内部解决?如果复杂度关乎于功能,第二个回答往往是更好。大部分模块的用户比开发者多,所以让少数面临复杂度。尽管这会增加你开发世界。另一个支持的观点是有一个简单的接口比简单的实现要重要。

结论

开发一个模块时,从使用者的角度看问题而不是你自己的角度,去降低复杂度。

第9章

合起来好还是分开好?

一个常见的开发问题是:有两个功能,应该是把它们一起实现还是分开实现?
当决定要结合还是分开,目标是从整体来降低系统复杂度然后改善它。似乎最好的方法是把系统拆分成小组件。组件越小,个体部分也越简单。然而,分离会导致额外的问题:
复杂度会因为模块多而增加。模块多意味着更多接口,每个接口都会增加复杂度。
细分会导致更多代码去管理组件。
细分导致分离,分离使开发者很难从一个地方获取接口。如果组件真的是独立的,分离是对的。如果组件间还有依赖,那么就是坏的。
细分会导致复制,在一个实例的代码可能要复制到细分的组件中。

把相似的、相关的代码组合起来是有益的。这里有他们相关的因素:
他们共享信息,例如,都依赖一个具体类型文件。
他们是一起使用的。
他们概念上重叠,这样一个高层的分类可以覆盖他们。
如果不看另一个模块这个会看起来很费力。

9.1 如果信息共享,把它们合起来

9.2 如果能简化接口,把它们合起来

9.3 把它们合起来消除复制

9.4 分开通用目的和特殊目的的代码

如果代码重复出现,就是抽象没有做好

9.5 例子:插入光标和选择

不要混合使用,即有通用目的,又包含特殊

9.6 例子:拆分log

9.7 例子:编辑撤销机制

9.8 分裂和结合方法

不要出现要看另一个模块才能理解这个模块的情况

9.9 结论

是拆分还是合并代码应该基于复杂度。选择可以信息隐藏的方案,更少的依赖,更深的接口。

第10章

定义不存在的错误

10.1 为什么特例会增加复杂度

10.2 太多特例

10.3 定义不存在的错误

最好的方式消除例外的复杂度是定义你的API,不产生例外。

10.4 例子:windows的文件删除

10.5 例子:Java substring 方法

0.6 mask 特例

10.7 特例聚集

10.8 只是崩溃?

10.9 设计不存在的特例

如果你对不存在的错误定义,你也可以对其他特殊情况定义。

10.1 别做过头了

10.11 结论

任何形式的特例会使代码难以理解,增加bug。这章关注特例,是特例编程最重要的部分,然后讨论怎么减少特例要出现的地方。最好的方法是重新定义语义去消除错误情况。把特例封装成更通用的形式,这个技术可以降低系统复杂度。

第11章

设计两次

第12章

为什么写注释?4个借口不写

12.1 好的代码是自我注释的

一些人认为如果代码写的足够好,很明显不需要注释。这是一个很好的理由,就像冰淇淋有利你的健康一样好听。不幸的是,这是不对的。你可以通过好的变量名来减少注释。但是仍有很多情况下的信息不能代表现在代码。

12.2 我没有时间写注释

如果你认为注释是次要的,总有一些其他事情会优于写注释。

12.3 注释过时导致错误

如果你没有大量修改代码需要增加的注释也不多。代码审核可以帮助完善注释。

12.4 我见过的注释都是多余的

很多人见过没有用的注释,这是可以通过提高写注释能力来改善。

12.5 写好注释的收益

你从好的注释中可以获得。注释背后的关键在于获取设计者不能体现在代码中的思想。

第13章

注释应该描述代码说不清楚的事情

13.1 使用惯例

13.2 不要重复代码

不要用代码出现的词在描述里重复

13.3 低层次的注释可以增加精准度(和抽象相反)

精确很重要尤其在使用实例变量,方法参数和返回值时。命名和类型在一个变量声明时没有那么精确。注释可以补充丢失的细节例如:

  1. 这个变量的单元是什么?
  2. 边界有没有包括?
  3. 如果传入null,是如何工作的?
  4. 如果指向资源的变量要销毁,谁负责销毁?
  5. 有没有一定存在的变量,例如这个列表至少包含一个元素?

13.4 高层注释提供直觉

帮助使用者从结构和整体去理解。

13.5 接口注释

不要写对使用没有帮助的实现细节

13.6 写什么和为什么,不是怎么

帮助使用者理解这个代码在干什么。

13.7 跨模块设计决定

13.8 结论

注释的目标是让系统的结构和行为让读者容易理解,这样他们可以快速判断是否可以使用。一些信息可以通过代码呈现,一些重要但是不能通过代码提现的部分需要注释。

遵守注释是描述代码不能提现的部分的原则。在写注释时,把自己放在使用者的角度,看使用者需要什么。如果使用者说看不明白,不要和他争论。尝试去理解是哪部分让读者难以理解,然后写更好地注释。

13.9 13.5的问题的答案

第14章

命名

14.1 例子:差的命名导致bug

14.2 想象

14.3 命名应该准确

模糊的命名应该避免,会让开发者误会。
一个很短的命名很难描述具体的对象,所以出现短命名时是一个提示。

14.4

命名一致

14.5 不同的观点:统一风格

14.6 结论

我们使用可以帮助代码更清晰的命名,当有人遇到时能马上猜出它的行为,不经过考虑就能理解。这会降低你之后编程的难度。不太容易导致bug。起一个好命名也是投资,你会从中受益的。

第15章

先写注释
(使用注释作为设计的一部分)

15.1 延迟的注释是坏注释

15.2 先写注释

15.3 注释是设计的工具

描述一个方法或变量的注释应该完整。如果你发现很难写一个注释,你应该发现设计上是不是有问题了。

15.4 早期的注释是有趣的

15.5 早期注释是否昂贵?

15.6 结论

如果你没有尝试在开始就写注释,试一下。然后考虑它如何影响设计,然后享受软件开发的过程。你尝试过再来和我分享你的经验。

第16章

修改存在的代码

16.1 保持策略的

16.2 保留注释:使注释靠近代码

16.3 注释属于代码的一部分,不是提交日志

16.4 保留注释:避免复制

16.5 保留注释:检查不同

16.6 高层注释更容易保留

第17章

一致性
一致性是降低系统复杂度有利的工具。如果一个系统保持一致,意味着类似的事情用类似的方法去做,不同的事情用不同的方法去做。一致性降低了认知负荷。如果你知道了一处如何做,那么很快会理解其他相似的地方怎么做。

一致性减少了错误。如果两个类似的地方用不同方法,开发者会认为他们已经用通方法导致错误。

17.1 一致性的例子

一致性可以用多种形式体现。
命名:
14章已经详细阐述了。
代码风格:
除了编译器的规则外,现在大多数开发部门都会用一本指导手册限制编码风格的太多样化。手册里列举了一些问题,例如缩进问题,大括号位置,声明的位置,命名,注释,一些危险的代码。指导手册可以使代码更易读,并且减少错误。
接口:
接口的多种实现形式。一旦你理解了一种接口的实现形式,另一个实现变的容易理解,因为你已经了解了它是如何提供的。
设计模式:
设计模式用于解决特定问题,例如mvc用于ui设计。如果你使用存在的模式去解决问题,实现会进步地更快,你的代码更容易阅读。在19.5章讨论模式。
常量:
例如,一个存储结构需要一个标志来表明是一行的结束。常量减少特例的数量,更容易表现代码的行为。

17.2 追求一致性

一致性很难去维持,特别是当许多人长期工作时。一组人不清楚另一组人建立起的习惯。新来的不知道这些规则,所以他们并非故意违反规定,冲突就产生了。这里有一些推广一致性的建议:

文档:
创建一个文档,列出最重要的规则,例如代码风格指南。把文档放在容易查到的地方,例如显而易见的工程里。鼓励新来的加入小组阅读文档,鼓励老人时不时去审核一下。一些习惯的指南很多组织都放在网页上,尝试这样工作。

强制:
即使有了好的文档,很难要求开发者记住所有的规则。最好的方式是有一个工具可以帮助检查,保证不合规范的代码不能提交到仓库。
我最近的一个项目在行数特征方面遇到困难。一些开发者在Unix工作,行数被新的行终结,在windows上,行数被回车终结。如果开发者在一个系统做了小改动,在另一个系统会被认为每行都改了。这使得判断哪行真的改了发生问题。我们建立了一个换行的规则,但是很难保证每个工具都遵循这个规则。每次一个新开发者加入项目,我们总是会遇到换行问题。
最后我们写了一段脚本在提交前自动执行。脚本检查所有文件,保证没有回车符。这很有用,并且对新来的也有帮助。
代码审核提供另一个强制规则的机会,减少新来的规则问题。审核越频繁,代码越干净。

用谚语:When in Rome, do as the Romans do.
最重要的规则是大家遵循这个谚语,“如果你在罗马,照罗马人的习惯做”。在新建一个文件时,看一下已经有的代码是如何写的。所有公共变量和方法是否在私有变量前声明?是否所有方法以字母排序?是否变量都用了“驼峰式”还是“蛇式”?就如“firstServerName”还是“first_server_name”?如果你觉得一些看起来像规则的写法,照着它写。当做一个设计时,这是否符合这个软件已有的规则,如果和已存在的相似,模仿它。

不要更改已经存在的规则:
限制修改已存在的规则。我有一个更好的注意并不是这么做的理由。你的新注意可能更好,但是注重一致性比找到一个好一点的方法要重要。当引进新规则时,问你自己两个问题:1,你是否有重要的信息通过新规则声明,老规则并不能实现?第二,新规则是否适用于所有老规则用到的地方?如果两个回答都是yes,才去升级。完成后,老的规则使用的地方要没有影响,否则很可能后面又改回老规则。总的来说,改之前的规则并不是一个很好的注意。

17.3 别做过头

一致性不仅仅是说相似的事情用相似的方法做,而且要不相似的事情用不同的方法去做。如果你尝试把不同事情用已有的规则做,会增加复杂度。一致性只在确定的情况下才有用,就像“如果这个看起来像一个x,它确实是x”这种情况。

17.4 结论

一致性是投资观念模式的一个例子。它会让你花费额外时间,在谈话中做决定,创造自动检查,发现相似场景用代码模仿,代码审核来指导团队。开发者会更容易理解代码,工作地更快,产生更少的bug。

第18章

隐晦是增加复杂度的两个原因之一,在2.3章提到过。隐晦发生在当系统重要信息对新开发者不明显的情况。解决隐晦问题的方法是写明显的代码。这章讨论一些使代码更明显的因素。
如果代码明显,意味着一些人可以快速阅读代码,不花费太多实际,他们第一映像关于代码做什么的猜测也是正确的。如果代码明显,读者不需要花大量时间在写代码前阅读。代码不明显就需要读者花大时间去理解,这样不仅仅减少效率,增加误会会导致bug。明显的代码只需要少量注释。
“明显”是读者的看法,看别人的代码往往比看你自己的不明显。因此,最好的方法决定代码是否明显取决于其他人。如果其他人读你的代码说不明显,尝试去理解他为什么觉得不明显,这样你才可以做的更好。

18.1 使代码更明显的事情

两个最重要技术已经在前面讨论过。第一是用好的命名。第二是一致性,如果相同的事用相同的形式去做,读者更容易辨识。
这里有一些其他使代码更明显的例子:
明智地使用空格:
使用空格拆分参数可以使读者更容易理解。
注释:
有时很难避免代码不明显。因此,使用注释去补充就很重要。你要猜测读者可能在哪部分疑惑,用注释补充。

18.2 使代码不明显的事情

事件驱动编码。
如果快速读完代码很难理解,要注意了,说明重要的信息没有呈现。
通用的容器。
代码不是读者期望那样的。

18.3 结论

另一个考虑是否明显的形式是信息。如果代码不明显,说明重要的信息没有传递给读者。
为了让代码明显,你必须保证读者有足够的信息去理解它。你可以用三个方面去做。最好的方法是减少读者需要的信息,使用设计技巧例如抽离和消除特例。第二,你可以使用读者已经获得的上下文知识,这样读者可以少学习新的知识。第三,你可以通过代码把重要信息呈现,使用技巧例如好的命名和有策略的注释。

第19章

软件趋势
前面讲了软件设计原则,这章考虑一些软件发展趋势。对于每个趋势,我会描述他们和本书的关系,去评估这些趋势和复杂度的关系。

19.1 面向对象的编程和继承

面向对象编程已经留下30-40年了。它引进了类,继承,私有方法和实例变量。如果好好使用,这些机制可以帮助生产更好的软件。例如,私有方法和变量能够用于信息隐藏:没有代码可以通过接口访问私有变量,所以对外没有依赖。

面向对象的一个关键元素是继承。继承来自两种形式,对于软件复杂度有不同实现。第一个是接口继承,就是表面的接口可以继承但是实现方法补基础。每个子类必须实现父类的接口,但是不同子类可以用不同的实现。例如,接口定义I/O,在一个子类用于实现磁盘操作,另一个子类可能用于网络接口。

接口基础降低复杂度通过相同接口的不同实现。它允许获得解决问题的一个方法用于解决其他问题。另一个角度看,增加了深度,有更多的不同实现,接口更深。为了让接口有很多实现,必须捕获重要特征,并且清楚实现的区别,这个概念取决于抽象程度。

第二个继承的形式是实现继承。在这个形式中,一个表面的类决定不仅仅是签名或更多方法,还是默认的实现。子类能够选择继承父类的实现或者重写。没有实现继承,相同的实现方法可能要复杂到很多子类中,会增加类之间的依赖。因此,实现继承减少了代码。换句话说,它减少了变化放大问题,在第2章提到的那样。

然而,实现继承使父类和子类直接产生依赖。类实例变量在父类经常被子类和父类访问,这会导致信息类之间的泄露,使得不看另一个类很难改这个类。类继承使用实现继承会增加复杂度。

因此,要十分小心地使用实现继承。考虑是否可以使用帮助类去实现共享方法。而不是继承来自父类的继承方法。

如果没有可选变量去实现继承,尝试分离父类。可以使用父类去管理所有方法,子类只用只读防区去使用。

尽管面向对象变成的机制能够帮助实现简化设计,但他们不保证能产生好的设计。例如,如果类很窄,或者有复杂接口,还是会导致复杂性。

19.2 敏捷开发

19.3 单元测试

19.4 测试驱动开发

19.5 设计模式

19.6 getters和setters

19.7 结论

无论你遇到什么样的新模式的提议,通过复杂性去评估它。是否新提议真的可以帮助降低复杂度在一个大系统中。很多提议表面看起来很好,一旦你看的更深你会发现它使问题变得更复杂了。

第20章

为了表现设计

20.1 怎么评估表现

20.2 在修改前先评估

20.3 围绕关键点设计

20.4 一个例子:RAMCloud Buffers

20.5 结论

这章最重要的部分是见简洁的设计和高性能是匹配的。复杂性高的代码往往更慢因为它要做更多工作。另一方面来说,如果你写的简洁,你的系统会更快,你就不用担心性能问题。找到性能的关键点,越简单越好。

第21章

结论
这本书关于一个事情:复杂度。处理复杂度是软件设计最重要的部分。这是使系统难于构建和维护的关键。在这本书我尝试描述复杂性的关键原因,例如依赖和隐晦。我的红旗部分是帮助你避免复杂度的产生,例如信息泄露,不必要的错误,或者太通用的命名。我已经呈现了一些通用思想去构建简单的系统,例如编写更深的代码,定义不存在的错误,分离接口文档和实现文档。最后,我讨论了投资观念模式去实现简单设计。

这些所有的建议都会增加你早起的工作。然而一旦你开始考虑设计问题,会降低你的进度,但是掌握好的设计技巧。如果你只为了实现当下需求,似乎这些都是你的阻碍。

但换一面想,如果好的设计是你的目标,那么这本书的主意会使编码更有趣。设计是幻想的拼图:一个具体的问题如何通过简单的结构解决?寻找不同方法很有趣,并且如果找到简单和强大的解决方法是很棒的。一个简单,明显的设计是美好的东西。

进一步说,你很快会从好的设计的投资中得到回报。你定义的模块会降低之后开发的实际。如果你上手了,好的设计实际上并不会花费你太多时间。

变成一个好的设计者的回报是你花费大量时间在设计阶段,这非常有趣。差的设计者花费大量时间在修改bug。如果你改善你的设计技能,不仅可以开发出高质量软件,软件开发过程也会变得更有趣。

设计原则摘要

这里有最重要的软件设计原则:

  1. 复杂性是递增的,你必须从小事耕耘。
  2. 只写代码是不够的。
  3. 使用持续的小投资来改善你的系统。
  4. 模块应该足够深。
  5. 接口应该设计的使大部分通用调用足够简单。
  6. 模块有简单的接口比有简单的实现重要。
  7. 通用为目标开发的模块更深。
  8. 分离通用目标和特殊目标的代码。
  9. 不同层应该有不同职责。
  10. 降低复杂度。
  11. 定义不存在的错误(特殊错误)。
  12. 设计两遍。
  13. 注释应该描述代码不能展示的部分。
  14. 软件应该设计的更容易阅读,不是写。
  15. 软件开发的增长应该抽象,不是具体。

Red Flags 的摘要

这些特征出现都暗示系统设计有问题。

浅的模块:接口没有比它实现更简单。
信息泄露:一个设计决定会影响多个模块。
临时分解:代码结构基于操作的顺序,不是信息隐藏。
过渡暴露:为了使用通用的特征暴露一个不太用的API。
通过式方法:一个方法只是起到传递另一个方法的作用。
重复:一部分代码反复出现。
特例和通用混合:特例的代码没有从通用中分离。
方法结合:两个方法相互依赖,如果不理解其中一个实现就没法理解另一个。
注释中使用重复代码:注释中出现附近的代码。
实现文档污染接口:一个接口注释中出现具体实现的细节,这些细节对使用者是无用的。
模糊的命名:命名没用传递有效信息。
很难命名:你发现很难给这个整体命名。
很难描述:为了描述完整,你发现注释写的很长。
不明显的代码:一部分代码在干啥很难理解。

关于作者

John Ousterhout 是斯坦福大学教授。他开发了Tcl脚本语言并且在分布式操作系统和存储系统知名。耶鲁大学毕业,在卡梅隆大学获得博士。是国际学术会员,获得过ACM软件系统奖,ACM Grace Murray Hopper Award,国际科学基金,UC伯克利教学贡献奖。

我把近十年读到的最好的100本睡前阅读推荐给大家。
标明了类型、作者和简介。

阅读氛围:
1.温暖的小夜灯(工作室原创小夜灯)
2.半杯温牛奶
3.轻柔的背景音乐

我读书用到的APP:
1、微信阅读
容易可以和好友分享。
2、网易蜗牛读书
优质领读多,帮助选书。
3、喜马拉雅
偶尔听书,放松眼睛。
4、瞬间记录(工作室自制,AppStore免费)
自己开发的摘抄笔记,聚合
整理各平台的优质内容。

一句话介绍:
《一日一果》“待客的心意是最高的细节。本书里的每幅照片,于我都是一场盛会。”
《鱼翅与花椒》这是关于中国菜的故事,也是一个英国女孩的中国历险记。
《酒店关门之后》莫西里酒吧被抢[小猫小姐]的账本被偷、蒂勒里太太被杀。三个当事人都请马修出面帮助查出真相,在一条条线索发现之后,三个案件的零碎拼图慢慢被拼在了一起,所有真相水落石出,马修自会有他的解决办法。
《威尼斯日记》威尼斯是一个小岛,以旅游来说,一个星期刚好,印象饱满细致。如果待过半个月,就会开始无聊,以致厌恶。
《阿勒泰的角落》若无意指认那在伤感中徘徊、欲望中沉浮的生命就是我们本来的生命,那么,总还有别样干净明亮的生命,等着人去认领。
《挽救计划》硬核工程风+幽默碎碎念打造年度科幻。
《切尔诺贝利的午夜》栩栩如生地讲述了这场历史上最严重的灾难,同时也让我们看到了苏联最后的岁月中那些每天都在发生的令人迷惑不解的真实事件。
《抓落叶》艾略特8岁的时候第一次看到黑影一样的怪物,但是没有人愿意相信他,还觉得他是个喜欢撒谎的怪胎。这个世界不欢迎不一样的人,这是生活教给艾略特的第一课。
《最后来的是乌鸦》纷乱时代里的小人物,寂寂无名却熠熠生辉,让人心疼也令人意外。背着步枪的孩子和惊恐地目睹了他那百发百中枪法的德国兵,枪决路上幻想重获自由的游击队员,靠每租半小时床垫赚五十里拉的狡猾小摩尔人,专注于舔奶油糕点而互相忘了对方的小偷与警察……
《如何说服一只猫》猫不像你我,它们油盐不进。它们更谨慎、更多疑,也往往比人更有智慧,尤其是在处理跨种族的关系上。当然,猫不会比人更有逻辑。相同的修辞技巧对猫和人都有效。学习说服猫不仅仅会促进你与猫的关系,还有助于你和人的相处。
《凪的新生活》原来,这么简单就可以重启人生。28岁,一个人,10平方米郊区出租屋,6万元银行存款。离开了原本的生活后,凪很快就有了新的邂逅。但见到再多的风景,学到再多的活法,帮人解决再多的问题。最难改变的也永远是自己。
《菜根谭》集儒、道、释思想于一体,蕴含着丰富的哲理,加上优美的语言,问世之后就受到追捧。由于书中各个条目内容之间没有紧密的逻辑联系,因此格局开放包容,后人陆续往里面增添、补充内容,形成了流传于后世的多个不同版本。明代版本中,将《菜根谭》分为前集、后集两大部分。清代版本中,将《菜根谭》分为修省、应酬、评议、闲适和概论五个部分。《菜根谭》流传到日本后,也形成十几种版本,它对日本的影响一直持续到现代,被有关企业奉为经典管理书籍。
《中世纪之美》中世纪是大教堂的世纪,上帝之树,千枝凌空,万叶纷披;中世纪是大城堡的世纪,断壁巨石铭刻着隐约在岁月深处的琱戈玉钺;中世纪是手抄本的世纪,神秘的文字寂寂地轩昂在灿烂的羊皮纸上。
《克苏鲁神话》假设你的脚边有一只蚂蚁在爬,你不会在意有没有踩死它,因为它太渺小了,是死还是活,对你来说没有分毫影响。在“克苏鲁神话”中描述的远古邪神的眼中,人类就是那只蚂蚁。
《日本蜡烛图技术》“K线之父”“蜡烛图泰斗”史蒂夫·尼森畅销近30年的金融投资技术分析经典,让大众熟知蜡烛图技术的开山之作。
《血酬定律》所谓血酬,即流血拼命所得的酬报,体现着生命与生存资源的交换关系。从晚清到民国,吃这碗饭的人比产业工人多得多。血酬的价值,取决于所拼抢的东西,这就是“血酬定律”。
《夜晚的潜水艇》九个故事,游走于旧山河与未知宇宙间,以瑰奇飘扬的想象、温厚清幽的笔法,在现实与幻境间辟开秘密的通道:海底漫游的少年、深山遗落的古碑、弥散入万物的字句、云彩修剪站、铸剑与酿酒、铁幕下的萨克斯、蓝鲸内的演奏厅……
《乌合之众》古斯塔夫・勒庞在他在书中极为精致地描述了集体心态,对人们理解集体行为的作用以及对社会心理学的思考发挥了巨大影响。
《洛丽塔》洛丽塔,我生命之光,我欲念之火。我的罪恶,我的灵魂。 洛一丽一塔:舌尖向上,分三步,从上颚往下轻轻落在牙齿上。洛。丽。塔。
《克拉拉与太阳》“太阳总有办法照到我们,不管我们在哪里。”
《桶川跟踪狂杀人事件》“我的女儿被杀害了三次。”第一次是罪犯,第二次是怠于调查的警方,第三次是伤害她名誉的媒体。
《鹿川有许多粪》“鸟儿奋力破壳而出,蛋就是世界。若要出生,就必须打破世界。鸟儿飞向神灵,神灵的名字叫作阿布拉克萨斯。”
《昨天堂》在急剧变迁的时代中,背起相机出发,徘徊于新旧交替的乡镇村野、城市边缘,透过方形构图,用黑白胶片留存传统人文的流动和消逝,记录如你我一样的大国小民。
《拳》隐姓埋名的茶馆幺师、天真憨厚的年轻姑娘、神秘莫测的问海禅师,谁才是所向披靡的武林高手?中国武术和西洋拳术孰强孰弱?手上之力与心上之力孰轻孰重?
《死亡赋格》把那些词放进这死者的墓里, 他是为了活着才说它们。 把他的头搁在它们中间, 让他感到 那些渴望之舌, 那些钳子。
《癌症·防御》如果你关注健康,希望远离晚期癌症,这就是写给你的书。
《梅里雪山》“(它)很可能是全球山岳冰川中流速最快的一个。” 遇难的十七人中有冰雪和气象研究的专家。他们付出了生命的代价,让我们得知了这样一处冰川的存在。
《银、剑、石》通过对三重“烙印”的独到挖掘,阿拉纳将拉丁美洲的历史娓娓道来……在这部探索、联结与分析的杰作中,阿拉纳提供了一种扣人心弦、另辟蹊径的视角,重新看待一个被贪婪和暴政背叛的重要地区。
《其主之声》机缘巧合之下,科学家发现了来自外太空的一封中微子信件,也许这正是智慧生物的象征。我们不知道发信人是谁,该如何解读这封信的内容呢?如果我们甚至无法确定是否存在发信人呢?
《血色浪漫》那是一个特殊的群体,他们的父母曾是高官和将军,他们曾是满怀激情的红卫兵战士,但是到了一九六八年,这个群体正在残酷的青春中茫然游荡,他们穿着家里箱子底儿翻出来的将校呢军服,在北京的街头成群呼啸,他们身怀利器,随时为微不足道的理由大打出手,他们“拍婆子”,他们看白皮书或灰皮书,他们有自己的一套仪式、礼俗和黑话,在“革命”的废墟上,一种独特的青少年亚文化悄然形成,他们是那个时代的周杰伦。
《路西法效应》然而,试验仅仅进行了一周,原本单纯的大学生,已变成了残暴的狱卒和崩溃的犯人,试验不得不终止了。
《东京贫困女子》她是在入学典礼前被迫成为“风俗小姐”的花季少女,她是付不起医疗费用的东京大学硕士,她是在网络上征集“干爹”的学生。
《奶酪与蛆虫》他不相信基督救赎,怀疑圣经文本,讥讽洗礼等诸圣事不过是一桩生意。他说上帝是一缕空气,视众天使为奶酪中的蛆虫。他大声指斥压迫穷人的教士、贵族,呼吁教会放弃特权、返朴归贫,甚至渴望发起一场激进的宗教改革。
《上海胶囊》展览可以成为小说,在现实空间创设虚构的情境;小说亦可以成为纸上的展览,将虚构成分织入真实的生活。
《祥瑞》上追尧舜,禅让称帝,王莽究竟是民选的圣主,还是篡汉的罪人?
《花衣魔笛手》不管1284年发生的这件事情究竟是什么,哈默尔恩庶民的悲伤与痛苦都跨越了时空,直抵我们的内心。当接近产生这种悲伤、痛苦的庶民生活时,我们就超越了单纯解密似的兴趣或好奇心,直接触摸到欧洲社会史的一角。
《破晓时分》街灯总嫌亮得早了些,当城市的太阳似落未落的时候,福成白铁号那块亚铁底子黑漆字的横招牌,便在这夕阳和街灯的争执里,似明又暗地拿不定是一种甚么色气了。
《字母表谜案》一座神秘的公寓,不定期举行推理合战。红茶、曲奇,搭配寒意逼人的谜案,与案件相关的字母似乎是重要线索。一群特殊的房客,联手组成侦探团,秘密追踪警视厅也难以锁定的幕后真凶。
《置身事内》“在成功的经济体中,经济政策一定是务实的,不是意识形态化的。是具体的,不是抽象的。”
《眩晕》这是一幅心灵的自画像——关于一颗躁动不安、持续不满的心灵,一颗受尽折磨、轻易落入幻觉的心灵。
……

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

bench_ios开发指南

(http://www.wyaq.com/youxi/gonglue/11966_27.html)

以增加开发效率为目的集成bench_ios开发。
bench_ios 不仅仅是它本身,还提供的是一种调度模式,可以根据自己业务需求接入不同模块来使用。详见 组件化分类使用方法

模板使用

参考XcodeCustom文件夹下README.md
包括工程模板和类模板,模板只需添加一次,Xcode更新后需要重新添加

Xcode 工程模板

模板文件在XcodeCustom文件夹下。
使用工程模板会初始化 CC_AppDelegate CC_ViewController pch 文件。
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/Project\ Templates/Base
目录下面添加bench_ios Application文件夹即可。

  1. 打开Xcode选择 File - New - Project
  2. 滚到下面选择 bench_ios Application > Custom Single App 创建一个新工程。
  3. 在 targets > Build Settings > Prefix Header 添加 xxx 工程的pch的路径。$(SRCROOT)/xxx/xxx-prefix.pch
  4. 使用pod安装bench_ios。

Xcode 类模板

类模板支持 CC_AppDelegate CC_ViewController CC_TabBarController
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates 目录下面添加bench_ios Class文件夹即可。
新建类使用 New File > bench_ios Class > Cocoa Touch Class

Podfile 安装

To integrate bench_ios into your Xcode project using CocoaPods, specify it in your Podfile:

1
2
3
4
5
6
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

target 'TargetName' do
pod 'bench_ios'
end

Then, run the following command:

1
$ pod install

在.pch文件或需要的地方引入,如使用工程模板会自动引入。

1
import "ccs.h"

模式是核心,工具是基础的扩展

模式类使用方法

ccs调度中心介绍

通过ccs只用一种调用方式,获得bench_ios中所有功能。

1
2
3
4
// 获取实例或执行函数
ccs.xxx;
// 调用类方法
[ccs xxx];

ccs有最高调度权限,没有管理权限。管理由各个模块各自管理,分布式架构,ccs可以获取访问权限。
使用者只需按需调用上层,具体的实现不需要关心,如果需要添加未实现方法或者现有方法不能满足,可以联系维护bench小伙伴

AppDelegate使用方法

修改main函数入口,如使用模板自动生成。

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, @"CC_AppDelegate");
}
}

新建AppDelegate继承CC_AppDelegate获得控制权限,CC_AppDelegate之后也会分发代理函数给子模块。如使用模板自动生成。

1
2
3
4
5
#import "CC_AppDelegate.h"

@interface AppDelegate : CC_AppDelegate

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

+ (void)load{
[ccs registerAppDelegate:self];
}

- (void)cc_willInit {
// 配置函数 在此函数中添加初始化配置
[ccs configureAppStandard:@{
YL_SUBTITLE_FONT :RF(13),
YL_SUBTITLE_COLOR :UIColor.whiteColor
}];

CCLOG(@"%@",APP_STANDARD(YL_SUBTITLE_FONT));

//入口单页面
// [self cc_initViewController:HomeVC.class withNavigationBarHidden:NO block:^{
// [self launch];
// }];

//入口TabBar
[self cc_initTabbarViewController:TestTabBarController.class block:^{
[self launch];
}];
}

#pragma mark life circle
- (BOOL)cc_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//当程序载入后执行,应用程序启动入口
return YES;
}

- (void)cc_applicationWillResignActive:(UIApplication *)application {
//应用程序将要进入非活动状态,即将进入后台
}

- (void)cc_applicationDidEnterBackground:(UIApplication *)application {
//应用程序已经进入后台运行
}

- (void)cc_applicationWillEnterForeground:(UIApplication *)application {
//应用程序将要进入活动状态,即将进入前台运行
}

- (void)cc_applicationDidBecomeActive:(UIApplication *)application {
//应用程序已进入前台,处于活动状态
}

- (void)cc_applicationWillTerminate:(UIApplication *)application {
//应用程序将要退出,通常用于保存数据和一些退出前的清理工作
}

@end

TabBarController使用方法

可使用模板类创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "TestTabBarController.h"
#import "HomeVC.h"
#import "ccs.h"

@interface TestTabBarController ()

@end

@implementation TestTabBarController

- (void)cc_viewWillLoad {
self.view.backgroundColor = UIColor.whiteColor;

// 纯图片 tabbar
// [self cc_initWithClasses:@[HomeVC.class,UIViewController.class]
// images:@[@"tabbar_mine_high",@"tabbar_mine_high"]
// selectedImages:@[@"tabbar_mine_high",@"tabbar_mine_high"]];
// 图片 + 文字 tabbar
[self cc_initWithClasses:@[HomeVC.class,UIViewController.class]
titles:@[@"首页",@"首页"]
images:@[@"tabbar_mine_high",@"tabbar_mine_high"]
selectedImages:@[@"tabbar_mine_high",@"tabbar_mine_high"]
titleColor:UIColor.blackColor
selectedTitleColor:UIColor.blueColor];

// 动态添加item
// [self cc_addTabBarItemWithClass:UIViewController.class
// image:@"tabbar_mine_high"
// selectedImage:@"tabbar_mine_high"
// index:2];
[self cc_addTabBarItemWithClass:UIViewController.class
title:@"我的"
image:@"tabbar_mine_high"
selectedImage:@"tabbar_mine_high"
index:2];
// 消息角标
[self cc_updateBadgeNumber:200 atIndex:2];
}

CC_ViewController使用方法

  1. 使用 ViewController 继承 CC_ViewController。
  2. cc_displayView 进行自定义绘制。它的frame即可显示内容的区域。(如有状态栏,会去除状态栏的部分,如有导航栏,会去除导航栏的部分)
  3. 针对 ViewController 业务复杂,存在ABC多个模块时,新建控制器继承 CC_Controller 委托代理进行业务拆分,模块共享。这个控制器也可放入其他 ViewController 使用。

对比传统开发和模板开发:
原来模板新建 ViewController.h文件

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestViewController : UIViewController

@end

NS_ASSUME_NONNULL_END

新模板新建 ViewController .h文件

1
2
3
4
5
6
7
8
9
#import "CC_ViewController.h"

NS_ASSUME_NONNULL_BEGIN

@interface TestNewViewController : CC_ViewController

@end

NS_ASSUME_NONNULL_END

原来模板新建 ViewController .m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "TestViewController.h"

@interface TestViewController ()

@end

@implementation TestViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/

@end

新模板新建 ViewController .m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import "TestNewViewController.h"

@interface TestNewViewController ()

@end

@implementation TestNewViewController

- (void)cc_viewWillLoad {
// Do any additional setup before loading the view.
}

- (void)cc_viewDidLoad {
// Do any additional setup after loading the view.
}

@end

使用 cc_xxx 替换原先 UIViewController 的 xxx 方法。如 cc_viewDidLoad 代替 viewDidLoad 。

设置标题:

1
2
3
- (void)cc_viewWillLoad{
self.cc_title = @"首页";
}

根据可选区域 cc_displayView 创建一个 tableview:

1
2
3
4
5
6
7
8
- (void)cc_viewDidLoad {
ccs.TableView
.cc_addToView(self)
.cc_frame(0, 0, WIDTH(), self.cc_displayView.height)
.cc_delegate(self)
.cc_dataSource(self)
.cc_backgroundColor(UIColor.whiteColor);
}

在 TestNewViewController 获取名为 abc 的视图:

1
CC_View *view = [self cc_viewWithName:@"abc"];

在 TestNewViewController 获取名为 Comment 的控制器C:

1
TestController *controller = [self cc_controllerWithName:@"Comment"];

自动适配方法会根据 cc_displayView 中最底部的控件来决定 cc_displayView 的 contentSize ,这样在小尺寸(如5c)的屏幕中你那个可以自动扩展成可以滑动。

1
2
3
4
- (void)cc_viewDidLoad {
...
[self cc_adaptUI];
}

Controller使用方法

需要拆分的模块继承 CC_Controller 实现自身代理。控制器初始化注册代理类,实现代理方法。
CC_Controller 通过start() 函数初始化,我们在 CC_Controller 中方便地提供了代理,通过代理分发回调到 CC_ViewController 来交互,传统方法需要声明代理,以及响应判断。
使用 CC_Controller 的模式解决了两个相似的 ViewController 的命名问题,使得控制器的维护更加方便。比如修改银行卡的 ViewController 和新增 ViewController 的相似度在90%,按照往常开发我们可能会创建一个 BankViewController 然后根据 type 区分是哪一个。使用子控制器的模式我们可以创建 AddBankViewController 和 EditBankViewController,然后在其注册 BankEditController,这样我们就可以在使用同一个功能模块的同时区分两个 ViewController。

在 TestNewViewController 中注册控制器后可接收控制器中的代理:

1
2
3
4
- (void)cc_viewWillLoad {
// 注册完可直接实现TestController里的协议'methd2withA:b:'
[self cc_registerController:TestController.class];
}

声明协议原来要写的代码:

1
2
3
4
5
6
7
// 属性声明
@property(nonatomic,assign) id <CC_LabelGroupDelegate>delegate;

// 代理
if ([self.delegate respondsToSelector:@selector(labelGroup:initWithButton:)]) {
[self.delegate labelGroup:self initWithButton:button];
}

变成直接代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import "CC_Foundation.h"
#import "ccs.h"

NS_ASSUME_NONNULL_BEGIN

@protocol TestControllerDelegate

- (void)methd2withA:(NSString *)a b:(NSArray *)b;

@end

@interface TestController : CC_Controller

@end

NS_ASSUME_NONNULL_END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "TestController.h"

@implementation TestController

- (void)cc_start {
// 初始化配置
self.cc_name = @"test1";
[ccs delay:2 block:^{
// 初始化配置完成
[self.cc_delegate cc_performSelector:@selector(methd2withA:b:) params:@"",@""];
}];
}

@end

在对应的 CC_ViewController 中便可接收到此方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import "TestViewController.h"
#import "TestController.h"
#import "ccs.h"

@interface TestViewController ()

@end

@implementation TestViewController

- (void)methd2withA:(NSString *)a b:(NSArray *)b{
// TestController里的协议
CCLOG(@"callback methd2withA");
}

- (void)cc_viewWillLoad {
// 注册完可直接实现TestController里的协议'methd2withA:b:'
[self cc_registerController:TestController.class];
}

- (void)cc_viewDidLoad {
}

CC_NavigationController使用方法

不需要自己管理navigationController,直接调用ccs的控制器进出方法。
进入控制器:

1
2
3
4
TestViewController2 *vc1 = [ccs init:TestViewController2.class];
vc1.tests1 = @"";
[ccs pushViewController:vc1];
// [ccs presentViewController:vc1];

进入带导航栏的控制器:

1
2
InputViewController *vc = [ccs init:InputViewController.class];
[ccs presentViewController:vc withNavigationControllerStyle:UIModalPresentationFullScreen];

退出控制器:

1
2
[ccs popViewController];
// [ccs dismissViewController];

控制器后退后发送消息到上一个控制器:

1
[ccs popViewControllerFrom:self userInfo:@"abc"];

在上一个控制器接收:

1
2
3
- (void)cc_viewDidPopFrom:(CC_ViewController *)viewController userInfo:(id)userInfo {

}

屏幕控制使用方法

通过 ccs 手动控制当前控制器的屏幕方向

1
2
3
4
- (void)cc_viewWillDisappear {

[ccs setDeviceOrientation:UIDeviceOrientationPortrait];
}

CC_Model 使用方法

TestModel 继承 CC_Model 后声明属性。
将 json 赋值给 CC_Model:

1
2
3
4
5
6
7
TestModel *modelObj = [ccs model:[TestModel class]];
[modelObj cc_setProperty:@{@"str1":@"xin",@"str2":@"yi"}];
[modelObj cc_update];

//replace Property name
[modelObj cc_setProperty:@{@"str1":@"xin",@"id":@"b"} modelKVDic:@{@"str2":@"id"}];
[modelObj cc_update];

通过 TestModel 中的 cc_update 函数做模型的个性化处理,如拼接字符串:

1
2
3
4
5
6
7
@implementation Test_model

- (void)cc_update {
CCLOG(@"update Test_model key value %@",self.cc_modelDic);
}

@end

CC_UIKit使用方法

使用链式声明一个ui控件可减少变量名的出现。包含常用ui控件button、label、view、…

控件使用

对比传统方法,创建一个label:

1
2
3
4
UILabel *label = [[UILabel alloc] init];
label.text = @"mylabel";
label.frame = CGRectMake(RH(10),RH(100),RH(100),RH(100));
label.backgroundColor = HEXA(@"FFD700", 1);

使用 CC_UIKit 中的CC_Label:

1
2
3
4
5
//省去变量名,通过链式返回对象扩展书写
ccs.label
.cc_name(@"mylabel")
.cc_frame(RH(10),RH(100),RH(100),RH(100))
.cc_backgroundColor(HEXA(@"FFD700", 1));

CC_Alert

系统弹窗快速调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)test_Alert {
[ccs showAltOn:self title:@"haha" msg:@"你猜" bts:@[@"取消",@"确定"] block:^(int index, NSString *name) {
CCLOG(@"showAlert index = %d btn name = %@",index,name);
}];

[ccs showTextFieldAltOn:self title:@"haha" msg:@"你猜" placeholder:@"猜不着" bts:@[@"取消",@"确定",@"ok"] block:^(int index, NSString *name, NSString *text) {
CCLOG(@"showTextFieldsAlert index = %d btn name = %@",index,name);
}];

[ccs showTextFieldsAltOn:self title:@"haha" msg:@"你猜" placeholders:@[@"猜",@"不",@"着"] bts:@[@"取消",@"确定",@"ok"] block:^(int index, NSString *name, NSArray *texts) {
CCLOG(@"showTextFieldsAlert index = %d btn name = %@ textFields text array = %@",index,name,texts);
}];
}

app标准使用方法

通过配置一些app标准后可全局调用,默认属性有:

1
2
3
4
5
6
7
8
9
10
11
//App Font And Color Standard
#define HEADLINE_FONT @"HEADLINE_FONT"
#define HEADLINE_COLOR @"HEADLINE_COLOR"
#define TITLE_FONT @"TITLE_FONT"
#define TITLE_COLOR @"TITLE_COLOR"
#define CONTENT_FONT @"CONTENT_FONT"
#define CONTENT_COLOR @"CONTENT_COLOR"
#define DATE_FONT @"DATE_FONT"
#define DATE_COLOR @"DATE_COLOR"
#define MASTER_COLOR @"MASTER_COLOR"
#define AUXILIARY_COLOR @"AUXILIARY_COLOR"

也可自定义配置更多标准,如医疗项目:

1
2
3
4
5
6
7
8
9
#define YL_SUBTITLE_FONT     @"YL_SUBTITLE_FONT"
#define YL_SUBTITLE_COLOR @"YL_SUBTITLE_COLOR"

[ccs configureAppStandard:@{
YL_SUBTITLE_FONT :RF(13),
YL_SUBTITLE_COLOR :UIColor.whiteColor
}];

CCLOG(@"%@",APP_STANDARD(YL_SUBTITLE_FONT));

自动适配使用方法

包含布局和字号:
所有布局的数字包一层RH()函数(Relative Height),如 CGRectMake(0, 0, RH(200), RH(40)

1
float x = RH(30)

字号使用RF()函数(Relative Font):

1
UIFont *font = RF(14);

textBind使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//数据和视图绑定
// 绑定string
NSString *str = [ccs string:@"abc%@%d",@"a",34];
// 方法三
ccs.label
.cc_name(@"mylabel")
.cc_frame(RH(10),RH(100),RH(100),RH(100))
.cc_backgroundColor(HEXA(@"FFD700", 1))
.cc_textColor(HEXA(@"9B30FF", 1))
.cc_bindText(str)
.cc_addToView(self)
.cc_tappedInterval(0.1,^(id view) {
// 改变labele内的富文本
NSMutableAttributedString *att = [ccs mutAttributedString];
[att cc_appendAttStr:@"abc" color:COLOR_LIGHT_ORANGE];
[att cc_appendAttStr:@"123" color:[UIColor greenColor] font:RF(22)];
CC_Label *v = view;
v.attributedText = att;
// 延时5秒后退出控制器
[ccs delay:5 block:^{
[ccs popViewController];
}];
});

// 3秒后更新string view跟踪变化
[ccs delay:3 block:^{
// 无需获取控件,更新数据源自动更新视图控件
[str cc_update:@"cvb"];
}];

如果我们在控制器的另一个函数中要获取控件对象,我们可以声明它为全局变量或者属性:

1
2
3
4
@interface TestViewController () {
UILabel *label;
}
@end

或者使用viewWithTag()来取对象,但用tag会很不方便,如果要直观还需将tag声明成static,所以我们提供了viewWithName()的函数来取对象:

1
2
3
- (void)funtionB {
id v = [self cc_viewWithName:@"abc"];
}

单例使用方法

将单例注册到ccs中即可:

1
2
3
+ (instancetype)shared {
return [ccs registerSharedInstance:self];
}

包含一个返回block,可配置初始化变量,这个block只会走一次:

1
2
3
4
5
+ (instancetype)shared {
return [ccs registerSharedInstance:self block:^{
//do something init
}];
}

共享数据使用方法

共享全局的对象,比如在控制器B更新控制器A的 model 。
设置共享参数:

1
[ccs setShared:@"name" obj:@"xinyi"];

获取共享参数:

1
NSString *name = [ccs sharedValueForKey:@"name"];

更新共享参数:

1
[ccs resetShared:@"name" obj:@"apple"];

移除共享参数:

1
[ccs removeShared:@"name"];

多线程使用方法

进入子线程:

1
2
3
[ccs gotoThread:^{
// do something in thread.
}];

进入主线程:

1
2
3
[ccs gotoMain:^{
// do something in main.
}];

延时执行:

1
2
3
[ccs delay:3 block:^{
// do something after 3s.
}];

可取消的名为 notice 的延时执行:

1
2
3
[ccs delay:3 key:@"notice" block:^{

}];

取消名为 notice 的延时,在延时完成前取消才有效,如延时是3秒需在3秒前调用停止就不会执行block中的函数:

1
[ccs delayStop:@"notice"];

一组异步任务 在多个线程执行。如3个异步任务,在3个线程同时执行:

1
2
3
4
5
6
7
8
9
10
11
12
CCLOG(@"cc_group %@",[NSThread currentThread]);
[ccs threadGroup:3 block:^(NSUInteger taskIndex, BOOL finish) {
if (taskIndex == 0) {
CCLOG(@"cc_group 0 finish %d %@",finish,[NSThread currentThread]);
} else if (taskIndex == 1){
CCLOG(@"cc_group 1 finish %d %@",finish,[NSThread currentThread]);
} else if (taskIndex == 2){
CCLOG(@"cc_group 2 finish %d %@",finish,[NSThread currentThread]);
} else {
CCLOG(@"cc_group 3 finish %d %@",finish,[NSThread currentThread]);
}
}];

异步完成一组有异步回调的函数后执行下一个函数。如在两个线程同时执行两个任务后再去执行下一个任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CCLOG(@"cc_blockGroup %@",[NSThread currentThread]);
[ccs threadBlockGroup:2 block:^(NSUInteger taskIndex, BOOL finish, id sema) {
if (taskIndex == 0) {
CCLOG(@"cc_blockGroup 0 %@",[NSThread currentThread]);
[ccs delay:10 block:^{
[ccs threadBlockFinish:sema];
}];
} else if (taskIndex == 1){
CCLOG(@"cc_blockGroup 1 %@",[NSThread currentThread]);
[ccs delay:2 block:^{
[ccs threadBlockFinish:sema];
}];
}
if (finish) {
CCLOG(@"cc_blockGroup finish %@",[NSThread currentThread]);
}
}];

顺序执行一组有异步回调的函数后执行下一个函数。如顺序执行任务1和任务2后再执行下一个任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CCLOG(@"cc_blockSequence %@",[NSThread currentThread]);
[ccs threadBlockSequence:2 block:^(NSUInteger taskIndex, BOOL finish, id _Nonnull sema) {
if (taskIndex == 0) {
CCLOG(@"cc_blockSequence 0 %@",[NSThread currentThread]);
[ccs delay:5 block:^{
[ccs threadBlockFinish:sema];;
}];
} else if (taskIndex == 1) {
CCLOG(@"cc_blockSequence 1 %@",[NSThread currentThread]);
[ccs delay:2 block:^{
[ccs threadBlockFinish:sema];;
}];
}
if (finish) {
CCLOG(@"cc_blockSequence finish %@",[NSThread currentThread]);
}
}];

定时器使用方法

获取当前时间戳:

1
NSTimeInterval *t = [ccs nowTimeTimestamp];

获取当前唯一时间戳,比如并发在两个子线程获取也会返回不一样的时间戳:

1
NSTimeInterval *t = [ccs uniqueNowTimestamp];

注册一个定时任务:

1
2
3
[ccs timerRegister:@"testTimer1" interval:1 block:^{
CCLOG(@"CoreTimer block 1 ");
}];

取消一个定时任务:

1
[ccs timerCancel:@"testTimer2"];

组件化生命周期分发使用方法

App 生命周期的分发,将耦合在 AppDelegate 中逻辑拆分,每个模块以微应用的形式独立存在。一个工程就会有多个 AppDelegate 存在,包括主工程 AppDelegate 以及部分组件库中的 AppDelegate 。
执行步骤:

  1. 需要所有组件库依赖 bench_ios 。
  2. 在组件库内创建一个 module_delegate 继承 CC_AppDelegate 。
  3. 用pod集成组件后 module_delegate 就会像传统 AppDelegate 一样工作。

组件化分类使用方法

通过 bench_ios 中的 cc_message 发送消息来调用组件化模块。因为没有 include 或者 import 组件化库,不会参与链接过程,会在运行时调用组件库。
执行步骤:

  1. 需要所有组件库依赖 bench_ios 。

  2. 创建一个 ccs 的分类。如推送组件创建 ccs+APNs.h 和 ccs+APNs.m 。

  3. 在分类的h文件声明API:

    1
    + (void)APNs_updateTokenToServerWithDomainUrl:(NSURL *)domainUrl authedUserId:(NSString *)authedUserId pushMessageBlock:(void(^)(NSDictionary *messageDic, BOOL lanchFromRemote))pushMessageBlock;
  4. 在m文件声明实现。在m文件的实现使用 cc_message 发送库的调用消息:

    1
    2
    3
    4
    5
    6
    7
    + (void)APNs_updateTokenToServerWithDomainUrl:(NSURL *)domainUrl authedUserId:(NSString *)authedUserId pushMessageBlock:(void(^)(NSDictionary *messageDic, BOOL lanchFromRemote))pushMessageBlock {
    [cc_message cc_targetAppDelegate:@"AppDelegate_APNs" method:@"updateTokenToServerWithDomainUrl:authedUserId:pushMessageBlock:" block:^(BOOL success) {
    if (!success) {
    CCLOGAssert(@"you need add pod 'bench_ios_APNs' in podfile.");
    }
    } params:domainUrl,authedUserId,pushMessageBlock];
    }
  5. 在项目中使用,使用pod安装APNs库,导入头文件:

    1
    2
    // 在pch或指定位置导入组件化分类
    #import "ccs+APNs.h"

调用:

1
2
3
4
// 组件化方法 调用推送库
[ccs APNs_updateTokenToServerWithDomainUrl:[NSURL URLWithString:@"http://xxx.com"] authedUserId:@"123456" pushMessageBlock:^(NSDictionary * _Nonnull messageDic, BOOL lanchFromRemote) {

}];

config使用方法

使用动态域名方案。 动态域名方案-将服务接口请求地址集合放在一个json文件里,上传到阿里云或者服务器,app拿到这个文件再获取域名,可以用于域名修改要发包的情况。
新建一个CC_CommonConfig.xcconfig文件,在里面写上:

1
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) CCBUILDTAG='$(CCBUILDTAG)'

新建发布:

1
CC_ReleaseConfig.xcconfig)、 主干(CC_TrunkConfig.xcconfig)、分支1CC_Branch1Config.xcconfig)等.xcconfig文件,在里面写上tag值和导入'CC_CommonConfig.xcconfig'文件,release=0,trunk=1,branch1=2 ...
1
2
CCBUILDTAG=0
#include "CC_CommonConfig.xcconfig"

之后只需修改project - info里的configurations来区分线上、主干和分支。

1
2
3
4
//我们传入动态域名的地址来获取正确的配置域名:
[ccs configureDomainWithReqGroupList:@[@[线上地址1,线上地址2...], @[主干地址1,主干地址2...], @[分支1地址1,分支1地址2...] ...] andKey:@"eh_doctor_api" cache:NO pingTest:YES block:^(HttpModel *result) {
//从result获取域名
}];

工具类使用方法

是一些常用方法的工具库。也是从 ccs 中获取。

CC_Monitor使用方法

目前CC_Monitor的几大功能是:

  1. 监控app本身和组件的生命周期(包括耗时和内存)。
  2. 监控app运行时的异常(包括耗时、内存和异常)。

打开监控和监控日志:

1
2
[ccs openLaunchMonitor:YES];
[ccs openLaunchMonitorLog:YES];

打开日志后会在启动时打印app和各组件启动耗时情况和内存使用情况:

1
2
3
4
5
6
7
8
9
10
2019-10-10 17:59:10.890059+0800 CatPhotoPicker[16705:372162] -[CC_Monitor reviewLaunchFinish] [Line 128]
{
AppDelegate = {
"execution time-consuming" = {
"cc_application:didFinishLaunchingWithOptions:" = "0.0001050233840942383";
"cc_willInit" = "0.01480793952941895";
};
"malloc_size" = 32;
};
}

打开app运行定期检查监控和日志:

1
2
[ccs openPatrolMonitor:YES];
[ccs openPatrolMonitorLog:YES];

打开后如注册的实例在某段时间段过大会打印相关日志。

网络请求使用方法

通过 ccs.httpTask 来实现网络相关的任务。

http请求使用方法

一个简单的 get 请求:

1
2
3
[ccs.httpTask get:@"https://blog.csdn.net/gwh111/article/details/100700830" params:nil model:nil finishBlock:^(NSString *error, HttpModel *result) {

}];

一个有特例配置的 get 请求:

1
2
3
4
5
HttpModel *model = [[HttpModel alloc]init];
model.forbiddenJSONParseError = YES;
[ccs.httpTask get:@"https://blog.csdn.net/gwh111/article/details/100700830" params:nil model:model finishBlock:^(NSString *error, HttpModel *result) {

}];

加密请求

如需使用加密请求,需要先做加密准备:

1
2
3
[ccs.httpEncryption prepare:^{

}];

准备完毕后,所有使用 httpTask 的请求会默认加密。加密方法为 AES+RSA
如单个请求不加密,需对单个接口配置 forbiddenEncrypt 的属性:

1
2
3
4
5
HttpModel *model = [[HttpModel alloc]init];
model.forbiddenEncrypt = YES;
[ccs.httpTask get:@"https://blog.csdn.net/gwh111/article/details/100700830" params:nil model:model finishBlock:^(NSString *error, HttpModel *result) {

}];

图片上传使用方法

上传一组图片,(通过特定接口上传):

1
2
3
[ccs.httpTask imageUpload:@[IMAGE(@"abc")] url:[NSURL URLWithString:@"xxx"] params:nil imageSize:1024 reConnectTimes:3 finishBlock:^(NSArray<HttpModel *> *errorModelArr, NSArray<HttpModel *> *successModelArr) {

}];

根据图片压缩比例上传:

1
2
3
[ccs.httpTask imageUpload:@[IMAGE(@"abc")] url:[NSURL URLWithString:@"xxx"] params:nil imageScale:0.3 reConnectTimes:3 finishBlock:^(NSArray<HttpModel *> *errorModelArr, NSArray<HttpModel *> *successModelArr) {

}];

文件上传使用方法

暂无

下载图片使用方法

不带占位图:

1
2
3
4
5
UIImageView *imgV = ccs.ImageView;
imgV.showProgressView = YES;
[self.view addSubView:imgV];

[imgV cc_setImageWithURL:[NSURL URLWithString:@"imageUrl"]];

带占位图下载:

1
[imgV cc_setImageWithURL:[NSURL URLWithString:@"imageUrl"] placeholderImage:[UIImage imageNamed:@"placeholder"]];

带占位图、进度回调、完成回调:

1
2
3
4
5
[imgV cc_setImageWithURL:[NSURL URLWithString:@"imageUrl"] placeholderImage:[UIImage imageNamed:@"placeholder"] processBlock:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
//进度回调
} completed:^(UIImage * _Nullable image, NSError * _Nullable error, BOOL finished) {
//完成回调
}];

带占位图、进度图、完成回调

1
2
3
[imgV cc_setImageWithURL:[NSURL URLWithString:@"imageUrl"] placeholderImage:[UIImage imageNamed:@"placeholder"] showProgressView:YES completed:^(UIImage * _Nullable image, NSError * _Nullable error, BOOL finished) {
//完成回调
}];

加密使用方法

MD5 唯一性校验:

1
NSString *md5Str = [CC_MD5Object cc_signString:@"testString"];

将所有参数加上MD5Key 做 MD5 后带上 sign 字段:

1
NSString *md5Str = [ccs function_MD5SignWithDic:@{@"key":@"value"} andMD5Key:@"abc123"];

将所有参数加上MD5Key 做 MD5 后获得 sign 值:

1
NSString *sign = [ccs function_MD5SignValueWithDic:@{@"key":@"value"} andMD5Key:@"abc123"];

DES加密:

1
NSString *desEncryptStr = [CC_DES cc_encryptUseDES:@"testString" key:@"xxxxxxxxx"];

DES解密:

1
NSString *desDecryptStr = [CC_DES cc_decryptUseDES:desEncryptStr key:@"xxxxxxxxx"];

AES加密:

1
2
3
4
5
//AES加密
//加密-带偏移量
NSData *aesEncryptData = [CC_AES cc_encryptWithKey:@"testKey" iv:@"testIV" data:[@"testString" cc_convertToUTF8data]];
//加密-不带偏移量
NSData *aesEncryptData = [CC_AES cc_encryptWithKey:@"testKey" data:[@"testString" cc_convertToUTF8data]];

AES解密:

1
2
3
4
//解密-带偏移量
NSData *aesDecryptData = [CC_AES cc_decryptWithKey:@"testKey" iv:@"testIV" data:aesEncryptData];
//解密-不带偏移量
NSData *aesDecryptData = [CC_AES cc_decryptWithKey:@"testKey" data:aesEncryptData];

RSA加密:

1
2
3
4
//公钥加密
NSString *rsaencryptStr = [CC_RSA cc_encryptStr:@"testString" publicKey:@"testPublicKey"];
//私钥加密
NSString *rsaDecryptStr = [CC_RSA cc_encryptStr:@"testString" privateKey:@"testPrivateKey"];

RSA解密:

1
2
3
4
//公钥解密
NSString *rsaencryptStr = [CC_RSA cc_decryptStr:@"testString" publicKey:@"testPublicKey"];
//私钥解密
NSString *rsaDecryptStr = [CC_RSA cc_decryptStr:@"testString" privateKey:@"testPrivateKey"];

数据存储使用方法

CC_Lib/CC_LibStorage 具体使用见Test_LibViewController

CC_KeyChainStore 钥匙串存储
CC_DefaultStore NSUserDefaults
CC_BundleStore NSBundle
CC_SandboxStore 沙盒 Documents 存储
CC_GCoreDataManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#pragma mark CC_LibStorage
//KeyChainStore
+ (NSString *)keychainKey:(NSString *)name;
+ (void)saveKeychainKey:(NSString *)key value:(NSString *)value;
+ (NSString *)keychainUUID;

//NSUserDefaults
+ (id)defaultKey:(NSString *)key;
+ (void)saveDefaultKey:(NSString *)key value:(id)value;

+ (id)safeDefaultKey:(NSString *)key;
+ (void)saveSafeDefaultKey:(NSString *)key value:(id)value;

//NSBundle
+ (NSString *)appName;
+ (NSString *)appBid;
+ (NSString *)appVersion;
+ (NSString *)appBundleVersion;
+ (NSDictionary *)appBundle;

+ (NSArray *)bundleFileNamesWithPath:(NSString *)name type:(NSString *)type;
+ (NSData *)bundleFileWithPath:(NSString *)name type:(NSString *)type;
+ (NSDictionary *)bundlePlistWithPath:(NSString *)name;

+ (BOOL)copyBunldFileToSandboxToPath:(NSString *)name type:(NSString *)type;
+ (BOOL)copyBunldPlistToSandboxToPath:(NSString *)name;

//沙盒 Documents 存储
+ (NSString *)sandboxPath;
+ (NSArray *)sandboxDirectoryFilesWithPath:(NSString *)name type:(NSString *)type;

+ (NSData *)sandboxFileWithPath:(NSString *)name type:(NSString *)type;
+ (NSDictionary *)sandboxPlistWithPath:(NSString *)name;

+ (BOOL)deleteSandboxFileWithName:(NSString *)name;
+ (BOOL)saveToSandboxWithData:(id)data toPath:(NSString *)name type:(NSString *)type;

音频使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CC_MusicBox *musicBox = CC_MusicBox.shared;
// 淡入淡出
musicBox.cc_fadeIn = Yes;
// 音效循环次数
musicBox._cc_soundReplayTimes=10000;
// 音乐循环次数
// musicBox.cc_musicReplayTimes=10000;
// 设置最大音量 (0.0~1.0)
musicBox.cc_defaultVolume=0.8;
// 播放音乐
[musicBox cc_playMusic:@"testMusicName" type:@"wav"];
// 播放音效
//[musicBox cc_playSound:@"testMusicName" type:@"wav"];
// 关闭音乐
[musicBox cc_stopMusic];

动画使用方法

1
2
3
4
5
6
7
// 不停闪烁
// [noteTextV.layer addAnimation:[CC_Animation cc_flickerForever:.5] forKey:nil];
+ (CABasicAnimation *)cc_flickerForever:(float)time;

// 按钮点击放大动画
// [CC_Animation cc_buttonTapEnlarge:checkBt];
+ (void)cc_buttonTapEnlarge:(CC_Button *)button;

函数使用方法

CC_Lib/CC_Function 具体使用见Test_FunctionViewController

CC_Function 常用工具函数
CC_String CC_Array CC_Dictionary CC_Data CC_Date CC_Object相关类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#pragma mark CC_Function
+ (NSData *)function_dataWithInt:(int)i;

+ (BOOL)function_isEmpty:(id)obj;
+ (BOOL)function_isInstallFromAppStore;
+ (BOOL)function_isJailBreak;

+ (int)function_compareVersion:(NSString *)v1 cutVersion:(NSString *)v2;

+ (NSString *)function_stringWithJson:(id)object;
+ (NSString *)function_formatDate:(NSString *)date nowDate:(NSString *)nowDate;
+ (NSString *)function_formatDate:(NSString *)date nowDate:(NSString *)nowDate formatArr:(NSArray *)formatArr;

+ (NSString *)function_replaceHtmlLabel:(NSString *)htmlStr labelName:(NSString *)labelName toLabelName:(NSString *)toLabelName trimSpace:(BOOL)trimSpace;
+ (NSArray *)function_getHtmlLabel:(NSString *)htmlStr start:(NSString *)startStr end:(NSString *)endStr includeStartEnd:(BOOL)includeStartEnd;

+ (NSMutableString *)function_MD5SignWithDic:(NSMutableDictionary *)dic andMD5Key:(NSString *)MD5KeyString;
+ (NSMutableString *)function_MD5SignValueWithDic:(NSMutableDictionary *)dic andMD5Key:(NSString *)MD5KeyString;

+ (NSMutableArray *)function_sortChineseArr:(NSMutableArray *)sortMutArr depthArr:(NSArray *)depthArr;
+ (NSMutableArray *)function_sortMutArr:(NSMutableArray *)mutArr byKey:(NSString *)key desc:(int)desc;
+ (NSMutableArray *)function_mapParser:(NSArray *)pathArr idKey:(NSString *)idKey keepKey:(BOOL)keepKey pathMap:(NSDictionary *)pathMap;
+ (NSMutableArray *)function_addMapParser:(NSMutableArray *)pathArr idKey:(NSString *)idKey keepKey:(BOOL)keepKey map:(NSDictionary *)getMap;

+ (NSTimeInterval)function_compareDate:(id)date1 cut:(id)date2;

+ (NSData *)function_archivedDataWithObject:(id)object;

+ (UIImage *)function_imageWithColor:(UIColor*)color width:(CGFloat)width height:(CGFloat)height;

+ (id)function_unarchivedObjectWithData:(id)data;
+ (id)function_copyObject:(id)object;
+ (id)function_jsonWithString:(NSString *)jsonString;

Build your first macOS app - PackageMachine

Why build this app?

Code:https://github.com/gwh111/testcocoappswift

This tutorial is better for iOS developer to master build basic macOS app skills. Meanwhile, this app is also a useful tool for an iOS developer.

We know how package with Xcode. We use archive so that we can export .ipa file. Before submit to AppStore, we often offer .ipa file to tester to do test. When you do several projects at same time, you are easily to make mistake or a bit of trouble to do this work. And PackageMachine is a macos app that can help you manage your projects and export .ipa file with one click.

Here’s a tutorial for how to build such a macos app.

Build the app

Create project

First you need to open Xcode then to do File-New-Project.

then choose macOS-CocoaApp and tap next to create a CocoaApp template.
We give it a name “PackageMachine”. So now we have:

Draw views in Main.storyboard

Use Main.storyboard can help us see the project structure clearly and to do less code.
You can use library and search to find plugs you want quickly.

Then layout the view carefully.

It’s not hard to layout the display like this.

State IBOutlet

In swift it do not like ObjectiveC has .h file, we just declare IBOutlet in .swift.
We find ViewController.swift and do following statement. So that we can connect code to our storyboard layout later.

1
2
3
4
5
6
7
8
@IBOutlet var taskName: NSTextField!
@IBOutlet var projectPath: NSTextField!
@IBOutlet var projectName: NSTextField!
@IBOutlet var debugRelease: NSSegmentedControl!
@IBOutlet var exportOptionsPath: NSTextField!
@IBOutlet var ipaPath: NSTextField!
@IBOutlet var logTextField: NSTextField!
@IBOutlet var showInfoTextView: NSTextView!

Remember to open assistant editor in the middle and show the inspectors in Xcode.

Connect all the target one to one carefully. Once you have done, you can change display in storyboard with your code now.

Don’t forget to link button and segment control at same time.
For button, I use tag to distinguish the different path selected.

1
2
3
4
@IBAction func selectPath(_ sender: NSButton) {
let tag=sender.tag
print(tag)
}

Interaction method

We want to get project path with one click and show the document list. So we need to use NSOpenPanel() It has been realized by Apple, once you import Cocoa you can use all the base classes.
The selectPath(_ sender: NSButton) method can be realized as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@IBAction func selectPath(_ sender: NSButton) {
let tag=sender.tag
print(tag)

// 1. create Panel object
let openPanel = NSOpenPanel()
// 2. set button text
openPanel.prompt = "Select"
// 3. forbidden select file, we only need path.
openPanel.canChooseFiles = true
if tag==0||tag==2 {
openPanel.canChooseFiles = false
}
// 4. set can choose directories
openPanel.canChooseDirectories = true
if tag==1 {
openPanel.canChooseDirectories = false
openPanel.allowedFileTypes=["plist"]
}
// 5. pop sheet method
openPanel.beginSheetModal(for: self.view.window!) { (result) in
// 6. when ok button clicked
if result == NSApplication.ModalResponse.OK {
// 7. get the current path
let path=openPanel.urls[0].absoluteString.removingPercentEncoding!
if tag==0 {
self.projectPath.stringValue=path
let array=path.components(separatedBy:"/")
if array.count>1{
let name=array[array.count-2]
print(array)
print(name as Any)
self.projectName.stringValue=name
}
}else if tag==1 {
self.exportOptionsPath.stringValue=path
}else{
self.ipaPath.stringValue=path
}
}
}
}

This method help us record all the paths and we will talk how to save all the data later. And we use path.compoents(separatedBy:”/“) to separate string to array.

When segment control state changed, we can add a select to monitor the state.

1
debugRelease.action = #selector(segmentControlChanged(segmentControl:))
1
2
3
@objc func segmentControlChanged(segmentControl: NSSegmentedControl) {
print(segmentControl.selectedSegment)
}

Start(Run shell task)

Before run task, we will check all the textview content is standard. Notice we use showNotice() method, and we will explain how to realize this later.

1
2
3
4
5
6
7
8
9
10
11
12
guard projectPath.stringValue != "" else {
showNotice(str: "project path cannot empty", suc: false)
return
}
guard projectName.stringValue != "" else {
showNotice(str: "project name cannot empty", suc: false)
return
}
guard ipaPath.stringValue != "" else {
showNotice(str: "output ipa path cannot empty", suc: false)
return
}

Can we run task now? The answer is no. We need something else to do.

We should replace some var in shell script with the path we set before by using replacingOccurrences method. The code can be see as:

1
2
3
4
5
6
7
8
9
10
11
12
13
let returnData = Bundle.main.path(forResource: "package", ofType: "sh")
let data = NSData.init(contentsOfFile: returnData!)
var str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String
if debugRelease.selectedSegment==0 {
str = str.replacingOccurrences(of: "DEBUG_RELEASE", with: "debug")
}else{
str = str.replacingOccurrences(of: "DEBUG_RELEASE", with: "release")
}
str = str.replacingOccurrences(of: "NAME_PROJECT", with: nameStr)
str = str.replacingOccurrences(of: "PATH_PROJECT", with: projectStr)
str = str.replacingOccurrences(of: "PATH_PLIST", with: plistStr)
str = str.replacingOccurrences(of: "PATH_IPA", with: ipaStr)
str = str.replacingOccurrences(of: "file://", with: "")

And last step before running we need go to global thread to make sure the app will not get stuck when running our task.

Our code will default run in main thread, which is UI thread. Normally we should put time cost task in child thread so that the UI thread will not get stuck.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
isLoadingRepo = true  //set sign
DispatchQueue.global(qos: .default).async {
let task = Process() // init NSTask

// set task
task.launchPath = "/bin/bash" // absolute execution path

// set command
task.arguments = ["-c",str]
task.terminationHandler = { proce in // finish block
self.isLoadingRepo = false // recover sign
//5. back to UI thread
DispatchQueue.main.async(execute: {
self.logTextField.stringValue="finish running task...";
})
}
self.captureStandardOutputAndRouteToTextView(task)
task.launch() // run task
task.waitUntilExit() // wait to finish
}

Then we create a Process() to run the shell script, and go back to main thread in terminationHandler block.
If user not set a path, we use default file to execute:

1
2
3
4
5
var plistStr:String=self.exportOptionsPath.stringValue
if plistStr.count<=0 {
let homeDic=NSHomeDirectory().appending("/ProjectPackage/eo") as String
plistStr=homeDic.appending("/"+"ExportOptions"+".plist")
}

So this part all the function code can be realize as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@IBAction func start(_ sender: Any) {
guard projectPath.stringValue != "" else {
showNotice(str: "project path cannot empty", suc: false)
return
}
guard projectName.stringValue != "" else {
showNotice(str: "project name cannot empty", suc: false)
return
}
guard ipaPath.stringValue != "" else {
showNotice(str: "output ipa path cannot empty", suc: false)
return
}

if isLoadingRepo {
showNotice(str: "is running last task", suc: false)
return
}
isLoadingRepo = true

var projectStr=self.projectPath.stringValue
if projectStr.first=="/" {
}else{
projectStr="/"+projectStr
}
let nameStr=self.projectName.stringValue
var plistStr:String=self.exportOptionsPath.stringValue
if plistStr.count<=0 {
let homeDic=NSHomeDirectory().appending("/ProjectPackage/eo") as String
plistStr=homeDic.appending("/"+"ExportOptions"+".plist")
}

var ipaStr=self.ipaPath.stringValue
if ipaStr.first=="/" {
}else{
ipaStr="/"+ipaStr
}

let returnData = Bundle.main.path(forResource: "package", ofType: "sh")
let data = NSData.init(contentsOfFile: returnData!)
var str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String
if debugRelease.selectedSegment==0 {
str = str.replacingOccurrences(of: "DEBUG_RELEASE", with: "debug")
}else{
str = str.replacingOccurrences(of: "DEBUG_RELEASE", with: "release")
}
str = str.replacingOccurrences(of: "NAME_PROJECT", with: nameStr)
str = str.replacingOccurrences(of: "PATH_PROJECT", with: projectStr)
str = str.replacingOccurrences(of: "PATH_PLIST", with: plistStr)
str = str.replacingOccurrences(of: "PATH_IPA", with: ipaStr)
str = str.replacingOccurrences(of: "file://", with: "")
print("result:\(str)");

self.logTextField.stringValue="start running task...";

DispatchQueue.global(qos: .default).async {
let task = Process() // init NSTask
// set task
task.launchPath = "/bin/bash" // absolute execution path
// set command
task.arguments = ["-c",str]
task.terminationHandler = { proce in // finish block
self.isLoadingRepo = false // recover sign
//5. back to UI thread
DispatchQueue.main.async(execute: {
self.logTextField.stringValue="finish running task...";
})
}
self.captureStandardOutputAndRouteToTextView(task)
task.launch() // run task
task.waitUntilExit() // wait to finish
}

}

Monitor task output

We all know when we run task in terminal, it will output a lot of log to help us find error. So we need capture some log when we are running our shell script.

If you are carefully, you must find captureStandardOutputAndRouteToTextView(task) before in *start(_ sender: Any) * method. So now we realize this function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
extension ViewController{
fileprivate func captureStandardOutputAndRouteToTextView(_ task:Process) {
//1. set output standard pipe
outputPipe = Pipe()
task.standardOutput = outputPipe

//2. waiting for notification
outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()

//3. receive notification

observe=NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in

//4. get pipe data, transform to string
let output = self.outputPipe.fileHandleForReading.availableData
let outputString = String(data: output, encoding: String.Encoding.utf8) ?? ""
if outputString != ""{
//5. goto main UI
DispatchQueue.main.async {

if self.isLoadingRepo == false {
let previousOutput = self.showInfoTextView.string
let nextOutput = previousOutput + "\n" + outputString
self.showInfoTextView.string = nextOutput
// scroll to bottom
let range = NSRange(location:nextOutput.utf8CString.count,length:0)
self.showInfoTextView.scrollRangeToVisible(range)

if self.observe==nil {
return
}
NotificationCenter.default.removeObserver(self.observe!)

return
}else{
let previousOutput = self.showInfoTextView.string
var nextOutput = previousOutput + "\n" + outputString as String
if nextOutput.count>5000 {
nextOutput=String(nextOutput.suffix(1000));
}
// scroll to bottom
let range = NSRange(location:nextOutput.utf8CString.count,length:0)
self.showInfoTextView.scrollRangeToVisible(range)
self.showInfoTextView.string = nextOutput
}
}
}

if self.isLoadingRepo == false {
return
}
//6. waiting for next notification
self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}

}

Pipe is used for serial port communication, to see more information, you can see https://developer.apple.com/documentation/foundation/pipe

Now you can run one task now, next we will add some codes to make it easy to use and talk how to write shell script.

Shell script

Learn write some shell script

You can open your terminal to do such test.
Output “hello world”:

1
2
3
a="hello world!"
num=2
echo "a is : $a num is : ${num}nd"

move file to another document and change name:

1
cp /Users/gwh/Documents/aaa123.jpg /Users/gwh/aaa1234.jpg

shell script in package machine

Now you master some grammar for shell script, let’s see what’s in PackageMachine’s shell script. I have added some notes between command line:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# !/bin/bash

# project name
project_name='NAME_PROJECT'
# project path
project_path='PATH_PROJECT'
# exportOptionsPlist path
plist_path='PATH_PLIST'
# debug release
debug_release='DEBUG_RELEASE'
# ipa output path
# create archive doc
output_path='PATH_IPA'
# xcarchive temp store path
archive_path="$output_path/archive"

appName='xx'
appId='xx'

packaging(){

# ***********configure
MWConfiguration=$debug_release
#date
MWDate=`date +%Y%m%d_%H%M`

# structure
xcodebuild archive \
-workspace "$project_path$project_name.xcworkspace" \
-scheme "$project_name" \
-configuration "$MWConfiguration" \
-archivePath "$archive_path/$project_name" \
clean \
build \
-derivedDataPath "$MWBuildTempDir"

# output ipa file
xcodebuild -exportArchive -exportOptionsPlist $plist_path -archivePath "$archive_path/$project_name.xcarchive" -exportPath $output_path/$appId

# move and rename
# mv /$output_path/$appId/LotteryShop.ipa /$output_path/$appId.ipa
time3=$(date "+%Y-%m-%d %H-%M-%S")
mv /$output_path/$appId/$project_name.ipa /$output_path/"$appId $time3.ipa"

# delete temp file
rm -r $output_path/$appId/
rm -r $output_path/archive/

}


group(){

appNames=($project_name)

appIds=($project_name)


if [[ $all -eq 0 ]]; then
echo "all=$all"

appNames=($project_name)

appIds=($project_name)

fi

i=0
while [[ i -lt ${#appIds[@]} ]]; do

appName=${appNames[i]}
appId=${appIds[i]}
let i++

echo $appName
# replace resource
# prepare

#package
packaging

done

open $output_path

}
#---------------------------------------------------------------------------------------------------------------------------------

#start
group

Read and Save

Once we create a task, we need to save it to local, so that we can recover when reopen the app.

Create store documents when launch

When we use the app in first time, i will create two documents to store data when in use.
The /ProjectPackage/eo is used to save ExportOptions.plist file and the /ProjectPackage/task is used to save tasks we will create when in use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let taskPath=NSHomeDirectory().appending("/ProjectPackage/task") as String
let eoPath=NSHomeDirectory().appending("/ProjectPackage/eo") as String

let fileM=FileManager.default
let existed:Bool=fileM.fileExists(atPath: taskPath, isDirectory: nil)
if (existed==false) {
do {
try fileM.createDirectory(at: NSURL.fileURL(withPath: taskPath), withIntermediateDirectories: true, attributes: nil)
}catch{

}
}
let existed_eo:Bool=fileM.fileExists(atPath: eoPath, isDirectory: nil)
if (existed_eo==false) {
do {
try fileM.createDirectory(at: NSURL.fileURL(withPath: eoPath), withIntermediateDirectories: true, attributes: nil)
}catch{

}
}
// print(read() as Any)

//save exp
let returnData = Bundle.main.path(forResource: "ExportOptions", ofType: "plist")
let data = NSData.init(contentsOfFile: returnData!)
var str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String
let homeDic=NSHomeDirectory().appending("/ProjectPackage/eo") as String
let fileName:String=homeDic.appending("/"+"ExportOptions"+".plist")
data!.write(toFile: fileName, atomically: true)

Save task

When we add a new task, we don’t want to configure it again. So we save it as template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func saveCurrentTable(){
let saveDic=readFromCurrentTable();
let taskS=saveDic.object(forKey: "taskName") as! String
if taskS.count<=0 {
showNotice(str: "give a taskName please", suc: false)
return;
}
showNotice(str: "create【"+taskS+"】success", suc: true)

let homeDic=NSHomeDirectory().appending("/ProjectPackage/task") as String
let fileName:String=homeDic.appending("/"+taskS+".plist")

saveDic.write(toFile: fileName, atomically: true)
}

The task we save as plist file, it looks as:

Read task

We can recover tasks we saved in documents we create before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func read() ->[NSDictionary]? {
let homeDic=NSHomeDirectory().appending("/ProjectPackage/task") as String
print(homeDic)

var taskList:[NSDictionary]=[]

let fileM=FileManager.default
do {
let cintents1 = try fileM.contentsOfDirectory(atPath: homeDic)
// print("cintents:\(cintents1.count)\n")

for name in cintents1 {
let fileName:String=homeDic.appending("/"+name)
let taskDic=readFileName(fileName: fileName)
if (taskDic) != nil{
taskList.insert(taskDic!, at: 0)
}
}
} catch {

}
return taskList
}

Task board

Task board is used to manage many different tasks. So we add a tableview in task board to display and let user select a task they want to use.

If you are an iOS developer, you must know how to create tableview. In macOS, it be named as NSTableView. And we will add a NSTableView to NSView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func addBoard(){

board=NSView.init()
board.frame=self.view.visibleRect
board.wantsLayer=true
self.view.addSubview(board)

let colorBoard:NSView=NSView.init()
colorBoard.frame=board.visibleRect
colorBoard.wantsLayer=true
colorBoard.layer?.backgroundColor=NSColor.black.cgColor
colorBoard.alphaValue=0.5;
board.addSubview(colorBoard)

let cancelBt:NSButton=NSButton.init()
cancelBt.frame=board.visibleRect
cancelBt.alphaValue=0;
cancelBt.action = #selector(cancelBtTapped(bt:))
board.addSubview(cancelBt)

let leftBoard:NSView=NSView.init()
leftBoard.frame=NSRect(x:0,y:0,width:300,height:board.visibleRect.height)
leftBoard.wantsLayer=true
leftBoard.layer?.backgroundColor=NSColor.white.cgColor
board.addSubview(leftBoard)

dataSource = read()

scrollView.frame=NSRect(x:leftBoard.frame.origin.x,y:leftBoard.frame.origin.y+40,width:leftBoard.frame.size.width,height:leftBoard.frame.size.height-40)
// tableView.delegate=(self as NSTableViewDelegate)
// tableView.dataSource=(self as NSTableViewDataSource)
board.addSubview(scrollView)

let addBt:NSButton=NSButton.init()
addBt.frame=NSRect(x:0,y:0,width:100,height:40)
let str="add task" as String
let attrTitle = NSMutableAttributedString.init(string: str)
let titleRange = NSMakeRange(0, str.count)
attrTitle.addAttributes([NSAttributedString.Key.foregroundColor: NSColor.black], range: titleRange)

addBt.attributedTitle=attrTitle
addBt.bezelStyle=NSButton.BezelStyle.regularSquare
addBt.action = #selector(addBtTapped(bt:))
board.addSubview(addBt)

tableView.reloadData()
}

Also we need to implement some Delegate and DataSource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//MARK: - NSTableViewDataSource
extension ViewController: NSTableViewDataSource{
func numberOfRows(in tableView: NSTableView) -> Int {
guard let dataSource = dataSource else {
return 0
}
return dataSource.count
}

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let rowView = tableView.rowView(atRow: row, makeIfNecessary: false)
rowView?.isEmphasized = false
let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CustomCell"), owner: self) as! CustomCell
let item = dataSource?[row]
cell.row=row
if row==selectIndex {
cell.selected=true;
}else{
cell.selected=false;
}
cell.setContent(item: item)
cell.callBackClosureFunction { (name, index) in
print("name:\(name), index:\(index)")

//delete from sandbox
self.deleteAtIndex(index: index)

self.dataSource?.remove(at: index)
self.tableView.reloadData()
}
return cell
}


func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CustomCell"), owner: self) as! CustomCell
let item = dataSource?[row]
cell.setContent(item: item)

if tableView.tableColumns.count>0 {
let tc = tableView.tableColumns[0]
let gap: CGFloat = 10 //width outside of label
cell.titleLabel.preferredMaxLayoutWidth = tc.width - gap
cell.detailLabel.preferredMaxLayoutWidth = tc.width - gap
}

return cell.fittingSize.height
}

}

//MARK: - NSTableViewDelegate
extension ViewController: NSTableViewDelegate{


func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
print("click at row \(row)")
selectIndex=row
displayCurrentTable()
tableView.reloadData()
return true
}

func tableViewSelectionDidChange(_ notification: Notification) {
let itemsSelected = tableView.selectedRowIndexes.count

if itemsSelected > 0 {
let row = tableView.selectedRow
tableView.deselectRow(row)
}
}


}

Auxiliary function

func alt(altStr:String)

Give a alt view to show notice:

1
2
3
4
5
6
7
8
9
10
11
func alt(altStr:String) {
let alert:NSAlert=NSAlert.init()
alert.addButton(withTitle: "OK")
alert.messageText=altStr
alert.beginSheetModal(for: self.view.window!) { (result) in
print(result.rawValue)
if result.rawValue==1000 {
print("ok")
}
}
}

func showNotice(str:String, suc:Bool)

This notice will show in log board, and we use green and red color to separate error and success logs:

1
2
3
4
5
6
7
8
9
10
func showNotice(str:String, suc:Bool) {
self.logTextField.stringValue=str
if suc {
let color: NSColor = NSColor.init(red: 18.0/255.0, green: 189.0/255.0, blue: 0, alpha: 1.0);
self.logTextField.textColor=color
}else{
let color: NSColor = NSColor.init(red: 150.0/255.0, green: 0, blue: 0, alpha: 1.0);
self.logTextField.textColor=color
}
}

@IBAction func courseTapped(_ sender: Any)

1
2
3
4
5
6
@IBAction func courseTapped(_ sender: Any) {
let returnData = Bundle.main.path(forResource: "README", ofType: "md")
let data = NSData.init(contentsOfFile: returnData!)
let str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String
self.showInfoTextView.string=str
}

Tips

Remember do not open App Sandbox in capabilities. If you do so, you cannot get local path to execute shell script. But if you want to let your app pass AppStore, you should open it.

The first time open .dmg you will get a warning that you cannot open an app from un-know developer. So you should goto security and safety to grant authorization.

Demonstration

When finish set template, we click start:

Then we get .ipa file. Also I append date to file name to tell different package file, otherwise it will overlap old file.

Link

You can get all codes in https://github.com/gwh111/testcocoappswift
I also add PackageMachine.dmg file for using directly.

Privacy policy

‘ShakeVenom’ users’ personal information and privacy policy

Important notice: in order to implement the Ministry of Culture issued the “Interim Measures for the management of network game” and “on the implementation of the notice” Interim Measures for the administration of online games “, to protect the user’s privacy, regulate the use of the user’s personal information, Shanghai ‘ShakeVenom’ network Polytron Technologies Inc (hereinafter referred to as” ‘ShakeVenom’ “) is hereby formulated the” ‘ShakeVenom’ the user’s personal information and privacy protection policy “(hereinafter referred to as the” privacy policy “).

‘ShakeVenom’ hereby reminds users to carefully read all the clauses in the privacy policy (juveniles should be accompanied by their legal guardians). If users do not agree with any content of privacy policy, please don’t register or use ‘ShakeVenom’ game services.

1, users agree that personal information refers to personal identification or relates to personal communication information to the user, including but not limited to the following information: user name, ID number, home address, telephone number, IP address, email address and other information. Non personal privacy information refers to some common information which is clear and objectively reflected on the ‘ShakeVenom’ server’s basic record information and other personal privacy information.

2, Generally speaking, ‘ShakeVenom’ needs to use the user’s information resources for the following reasons:

(1) executing software verification service;

(2) implement the software upgrade service;

(3) network synchronization service;

(4) improve the user’s security and provide customer support.

(5) because users use the specific function of the software or provide specific services for ‘ShakeVenom’ and its associated companies or cooperative units, ‘ShakeVenom’ and its associated companies or partners need to provide users with the information related to the third party.

(6) other benefits for users and ‘ShakeVenom’.

3, ‘ShakeVenom’ pays attention to the protection of users’ information resources, and uses various security technologies and programs to protect users’ information resources from unauthorized access, use and leakage. Except for legal or governmental requirements or user’s consent, ‘ShakeVenom’ does not disclose or disclose users’ information resources without the consent of users, except for the third party other than ‘ShakeVenom’ related companies or partners. But the exception is disclosed to the third party for the following reasons:

(1) disclosure on the basis of the provisions of national laws and regulations;

(2) the state judiciary and other relevant organs should be disclosed on the basis of the requirements of legal procedures.

(3) for the protection of ‘ShakeVenom’ or your legitimate rights and interests and the appropriate disclosure of the measure;

(4) in an emergency, it is disclosed for the protection of other users and the safety of the three party.

(5) the authorized disclosure of the user or the user’s guardian;

(6) when the user’s guardian’s legal requirement is provided, the user’s personal identity information is provided.

Special note: this privacy policy applies only to users who use ‘ShakeVenom’ game services. If this policy is inconsistent with the terms of privacy policy in the licensing and service agreement of ‘ShakeVenom’ games, the terms of this policy shall prevail. If you have opinions or suggestions on this policy or ‘ShakeVenom’ game service, we can give you the necessary help to contact the ‘ShakeVenom’ customer service.

你好 欢迎来到我这里