大型 web 前端架构设计-面向抽象编程入门

发表于 3年以前  | 总阅读数:265 次

面向抽象编程,是构建一个大型系统非常重要的参考原则。但对于许多前端同学来说,对面向抽象编程的理解说不上很深刻。大部分同学的习惯是 拿到需求单和设计稿之后就开始编写 UI 界面,UI 里哪个按钮需要调哪些方法,接下来再编写这些方法,很少去考虑复用性。当某天发生需求变更时,才发现目前的代码很难适应这些变更,只能重写。日复一日,如此循环。

当第一次看到“将抽象和具体实现分开”这句话的时候,可能很难明白它表达的是什么意思。什么是抽象,什么又是具体实现?为了理解这段话,我们耐下性子,先看一个假想的小例子,回忆下什么是面向具体实现编程。

假设我们正在开发一个类似“模拟人生”的程序,并且创造了小明,为了让他的每一天都有规律的生活下去,于是给他的核心程序里设置了如下逻辑:

1、8点起床
2、9点吃面包
3、17点打篮球

过了一个月,小明厌倦了一成不变的重复生活,某天早上起来之后他突然想吃薯片,而不是面包。等到傍晚的时候他想去踢足球,而不是继续打篮球,于是我们只好修改源代码:

 1、8点起床
 2、9点吃面包 -> 9点吃薯片
 3、17点打篮球 -> 17点踢足球

又过了一段时间,小明希望周 3 和周 5 踢足球,星期天打羽毛球,这时候为了满足需求,我们的程序里可能会被加进很多 if、else 语句。

为了满足需求的变换,跟现实世界很相似,我们需要深入核心源代码,做大量改动。现在再想想自己的代码里,是不是有很多似曾相识的场景?

这就是一个面向具体实现编程的例子,在这里,吃面包、吃薯片、打篮球、踢足球这些动作都属于具体实现,映射到程序中,它们就是一个模块、一个类,或者一个函数,包含着一些具体的代码,去负责某件具体的事情。

一旦我们想在代码中更改这些实现,必然需要被迫深入和修改核心源代码。当需求发生变更时,一方面,如果核心代码中存在各种各样的大量具体实现,想去全部重写这些具体实现的工作量是巨大的,另一方面,修改代码总是会带来未知的风险,当模块间的联系千丝万缕时,修改任何一个模块都得小心翼翼,否则很可能发生改好 1 个 bug,多出 3 个 bug 的情况。

抽取出共同特性

抽象的意思是:从一些事物中抽取出共同的、本质性的特征。

如果我们总是针对具体实现去编写代码,就像上面的例子,要么写死 9 点吃面包,要么写死 9 点吃薯片。这样一来,在业务发展和系统迭代过程中,系统就会变得僵硬和修改困难。产品需求总是多变的,我们需要在多变的环境里,尽量让核心源代码保持稳定和不用修改。

方法就是需要抽取出“9 点吃面包”和“9 点吃薯片”的通用特性,这里可以用“9 点吃早餐”来表示这个通用特性。同理,我们抽取出“17 点打篮球”和“17 点踢足球”的通用特性,用“17 点做运动”来代替它们。然后让这段核心源代码去依赖这些“抽象出来的通用特性”,而不再是依赖到底是“吃面包”还是“吃早餐”这种“具体实现”。

我们将这段代码写成:

  1、 8点起床
  2、 9点吃早餐
  3、17点做运动

这样一来,这段核心源代码就变得相对稳定多了,不管以后小明早上想吃什么,都无需再改动这段代码,只要在后期,由外层程序将“吃早餐”还是“吃薯片”注入进来即可。

真实示例

刚才是一个虚拟的例子,现在看一段真实的代码,这段代码依然很简单,但可以很好的说明抽象的好处。

在某段核心业务代码里,需要利用 localstorge 储存一些用户的操作信息,代码很快就写好了:

import ‘localstorge’ from 'localstorge';

class User{
    save(){
        localstorge.save('xxx');
    }
}

const user = new User();
user.save();

这段代码本来工作的很好,但是有一天,我们发现用户信息相关数据量太大, 超过了 localstorge 的储存容量。这时候我们想到了 indexdb,似乎用 indexdb 来存储会更加合理一些。

现在我们需要将 localstorge 换成 indexdb,于是不得不深入 User 类,将调用 localstorge 的地方修改为调用 indexdb。似乎又回到了熟悉的场景,我们发现程序里,在许多核心业务逻辑深处,不只一个,而是有成百上千个地方调用了 localstorge,这个简单的修改都成了灾难。

所以,我们依然需要提取出 localstorge 和 indexdb 的共同抽象部分,很显然,localstorge 和 indexdb 的共同抽象部分,就是都会向它的消费者提供一个 save 方法。作为它的消费者,也就是业务中的这些核心逻辑代码,并不关心它到底是 localstorge 还是 indexdb,这件事情完全可以等到程序后期再由更外层的其他代码来决定。

我们可以申明一个拥有 save 方法的接口:

interface DB{
   save(): void;
}

然后让核心业务模块 User 仅仅依赖这个接口:

import DB from 'DB';

class User{
    constructor(
        private db: DB
   ){

    }
    save(){
        this.db.save('xxx');
    }
}

接着让 Localstorge 和 Indexdb 分别实现 DB 接口:

class Localstorge implements DB{
    save(str:string){
        ...//do something
    }
}

class Indexdb implements DB{
    save(str:string){
        ...//do something
    }
}

const user = new User( new Localstorge() );
//or
const user = new User( new Indexdb() );

userInfo.save();

这样一来,User 模块从依赖 Localstorge 或者 Indexdb 这些具体实现,变成了依赖 DB 接口,User 模块成了一个稳定的模块,不管以后我们到底是用 Localstorge 还是用 Indexdb,User 模块都不会被迫随之进行改动。

让修改远离核心源代码

可能有些同学会有疑问,虽然我们不用再修改 User 模块,但还是需要去选择到底是用 Localstorge 还是用 Indexdb,我们总得在某个地方改动代码把,这和去改动 User 模块的代码有什么区别呢?

实际上,我们说的面向抽象编程,通常是针对核心业务模块而言的。User 模块是属于我们的核心业务逻辑,我们希望它是尽量稳定的。不想仅仅因为选择使用 Localstorge 还是 Indexdb 这种事情就得去改动 User 模块。因为 User 模块这些核心业务逻辑一旦被不小心改坏了,就会影响到千千万万个依赖它的外层模块。

如果 User 模块现在依赖的是 DB 接口,那它被改动的可能性就变小了很多。不管以后的本地存储怎么发展,只要它们还是对外提供的是 save 功能,那 User 模块就不会因为本地存储的变化而发生改变。

相对具体行为而言,接口总是相对稳定的,因为接口一旦要修改,意味着具体实现也要随之修改。而反之当具体行为被修改时,接口通常是不用改动的。

至于选择到底是用 Localstorge 还是用 Indexdb 这件事情放在那里做,有很多种实现方式,通常我们会把它放在更容易被修改的地方,也就是远离核心业务逻辑的外层模块,举几个例子:

* 在main函数或者其他外层模块中生成Localstorge或者Indexdb对象,在User对象被创建时作为参数传给User
* 用工厂方法创建Localstorge或者Indexdb
* 用依赖注入的容器来绑定DB接口和它具体实现之间的映射

内层、外层和单向依赖关系

将系统分层,就像建筑师会将大厦分为很多层,每层有特有的设计和功能,这是构建大型系统架构的基础。除了过时的 mvc 分层架构方式外,目前常用的分层方式有洋葱架构(整洁架构)、DDD(领域驱动设计)架构、六边形架构(端口-适配器架构)等,这里不会详细介绍每个分层模式,但不管是洋葱架构、DDD 架构、还是六边形架构,它们的层与层之间,都会被相对而动态地区分为外层和内层。

前面我们也提过好几次内层和外层的概念(大部分书里称为高层和低层),那么在实际业务中,哪些模块会对应内层,而哪些模块应该被放在外层,到底由什么规律来决定呢?

先观察下自然届,地球围绕着太阳转,我们认为太阳是内层,地球是外层。眼睛接收光线后通过大脑成像,我们认为大脑是内层,眼睛是外层。当然这里的内层和外层不是由物理位置决定的,而是基于模块的稳定性,即越稳定越难修改的模块应该被放在越内层,而越易变越可能发生修改的模块应该被放在越外层。就像用积木搭建房子时,我们需要把最坚固的积木搭在下面。

这样的规则设置是很有意义的,因为一个成熟的分层系统都会严格遵守单向依赖关系。

我们看下面这个图:

假设系统中被分为了 A、B、C、D 这 4 层,那么 A 是相对的最内层,外层依次是 B、C、D。在一个严格单向依赖的系统中,依赖关系总是只能从外层指向内层。

这是因为,如果最内层的 A 模块被修改,则依赖 A 模块的 B、C、D 模块都会分别受到牵连。在静态类型语言中,这些模块因为 A 模块的改动都要重新进行编译,而如果它们引用了 A 模块的某个变量或者调用了 A 模块中的某个方法,那么它们很可能因为 A 模块的修改而需要随之修改。所以我们希望 A 模块是最稳定的,它最好永远不要发生修改。

但如果外层的模块被修改呢?比如 D 模块被修改之后,因为它处在最外层,没有其他模块依赖它,它影响的仅仅是自己而已,A、B、C 模块都不需要担心它们收到任何影响,所以,当外层模块被修改时,对系统产生的破坏性相对是比较小的。

如果从一开始就把容易变化,经常跟着产品需求变更的模块放在靠近内层,那意味着我们经常会因为这些模块的改动,不得不去跟着调整或者测试系统中依赖它的其他模块。

可以设想一下,造物者也许也是基于单向依赖原则来设置宇宙和自然界的,比如行星依赖恒星,没有地球并不会对太阳造成太大影响,而如果失去了太阳,地球自然也不存在。眼睛依赖大脑,大脑坏了眼睛自然失去了作用,但眼睛坏了大脑的其他功能还能使用。看起来地球只是太阳的一个插件,而眼睛只是大脑的一个插件。

回到具体的业务开发,核心业务逻辑一般是相对稳定的,而越接近用户输入输出的地方(越接近产品经理和设计师,比如 UI 界面),则越不稳定。比如开发一个股票交易软件,股票交易的核心规则是很少发生变化的,但系统的界面长成什么样子很容易发生变化。所以我们通常会把核心业务逻辑放在内层,而把接近用户输入输出的模块放在外层。

在腾讯文档业务中,核心业务逻辑指的就是将用户输入数据通过一定的规则进行计算,转换成文档数据。这些转换规则和具体计算过程是腾讯文档的核心业务逻辑,它们是非常稳定的,从微软 office 到谷歌文档到腾讯文档,30 多年了也没有太多变化,它们理应被放在系统的内层。另一方面,不管这些核心业务逻辑跑在浏览器、终端或者是 node 端,它们也都不应该变化。而网络层、存储层,离线层、用户界面这些是易变的,在终端环境里,终端用户界面层和 web 层的实现就完全不一样。在 node 端,存储层或许可以直接从系统中剔除掉,因为在 node 端,我们只需要利用核心业务逻辑模块对函数进行一些计算。同理,在单元测试或者集成测试的时候,离线层和存储层可能都是不需要的。在这些易变的情况下,我们需要把非核心业务逻辑都放在外层,方便它们被随时修改或替换。

所以,遵守单向依赖原则能极大提高系统稳定性,减少需求变更时对系统的破坏性。我们在设计各个模块的时候,要将相当多的时间花在设计层级、模块的切分,以及层级、模块之间的依赖关系上,我们常说“分而治之”, “分”就是指层级、模块、类等如何切分,“治”就是指如何将分好的层级、模块、类合理的联系起来。这些设计比具体的编码细节工作要更加重要。

依赖反转原则

依赖反转原则的核心思想是:内层模块不应该依赖外层模块,它们都应该依赖于抽象。

尽管我们会花很多时间去考虑哪些模块分别放到内层和外层,尽量保证它们处于单向依赖关系。但在实际开发中,总还是有不少内层模块需要依赖外层模块的场景。

比如在 Localstorge 和 Indexdb 的例子里,User 模块作为内层的核心业务逻辑,却依赖了外层易变的 Localstorge 和 Indexdb 模块,导致 User 模块变得不稳定。

import ‘localstorge’ from 'localstorge';

class User{
    save(){
        localstorge.save('xxx');
    }
}

const user = new User();
user.save();

缺图

为了解决 User 模块的稳定性问题,我们引入了 DB 抽象接口,这个接口是相对稳定的,User 模块改为去依赖 DB 抽象接口,从而让 User 变成一个稳定的模块。

Interface DB{
   save(): void;
}

然后让核心业务模块 User 仅仅依赖这个接口:

import DB from 'DB';

class User{
    constructor(
        private db: DB
   ){

    }
    save(){
        this.db.save('xxx');
    }
}

接着让 Localstorge 和 Indexdb 分别实现 DB 接口:

class Localstorge implements DB{
    save(str:string){
        ...//do something
    }
}

依赖关系变成:缺图

User -> DB <- Localstorge

在图 1 和图 2 看来,User 模块不再显式的依赖 Localstorge,而是依赖稳定的 DB 接口,DB 到底是什么,会在程序后期,由其他外层模块将 Localstorge 或者 Indexdb 注入进来,这里的依赖关系看起来被反转了,这种方式被称为“依赖反转”。

找到变化,并将其抽象和封装出来

我们的主题“面向抽象编程”,很多时候其实就是指的“面向接口编程”,面向抽象编程站在系统设计的更宏观角度,指导我们如何构建一个松散的低耦合系统,而面向接口编程则告诉我们具体实现方法。依赖倒置原则告诉我们如何通过“面向接口编程”,让依赖关系总是从外到内,指向系统中更稳定的模块。

知易行难,面向抽象编程虽然概念上不难理解,但在真实实施中却总是不太容易。哪些模块应该被抽象,哪些依赖应该被倒转,系统中引入多少抽象层是合理的,这些问题都没有标准答案。

我们在接到一个需求,对其进行模块设计时,要先分析这个模块以后有没有可能随着需求变更被替换,或是被大范围修改重构?当我们发现可能会存在变化之后,就需要将这些变化封装起来,让依赖它的模块去依赖这些抽象。

比如上面例子中的 Localstorge 和 indexdb,有经验的程序会很容易想到它们是有可能需要被互相替换的,所以它们最好一开始就被设计为抽象的。

同理,我们的数据库也可能产生变化,也许今天使用的是 mysql,但明年可能会替换为 oracle,那么我们的应用程序里就不应该强依赖 mysql 或者 oracle,而是要让它们依赖 mysql 和 oracle 的公共抽象。

再比如,我们经常会在程序中使用 ajax 来传输用户输入数据,但有一天可能会想将 ajax 替换为 websocket 的请求,那么核心业务逻辑也应该去依赖 ajax 和 websocket 的公共抽象。

封装变化与设计模式

实际上常见的 23 种设计模块,都是从封装变化的角度被总结出来的。拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。

比如工厂模式,通过将创建对象的变化封装在工厂里,让核心业务不需要依赖具体的实现类,也不需要了解过多的实现细节。当创建的对象有变化的时候,我们只需改动工厂的实现就可以,对核心业务逻辑没有造成影响。

比如模块方法模式,封装的是执行流程顺序,子类会继承父类的模版函数,并按照父类设置好的流程规则执行下去,具体的函数实现细节,则由子类自己来负责实现。

通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统的演变过程中,只需要替换或者修改那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性。

避免过度抽象

虽然抽象提高了程序的扩展性和灵活性,但抽象也引入了额外的间接层,带来了额外的复杂度。本来一个模块依赖另外一个模块,这种依赖关系是最简单直接的,但我们在中间每增加了一个抽象层,就意味着需要一直关注和维护这个抽象层。这些抽象层被加入系统中,必然会增加系统的层次和复杂度。

如果我们判断某些模块相对稳定,很长时间内都不会发生变化,那么没必要一开始就让它们成为抽象。

比如 java 中的 String 类,它非常稳定,所以并没有对 String 做什么抽象。

比如一些工具方法,类似 utils.getCookie(),我很难想象 5 年内有什么东西会代替 cookie,所以我更喜欢直接写 getCookie。

比如腾讯文档 excel 的数据 model,它属于内核中的内核,像整个身体中的骨骼和经脉,已经融入到了各个应用逻辑中,它被替换的可能性非常小,难度也非常大,不亚于重写一个腾讯文档 excel,所以也没有必要对 model 做过度抽象。

结语

面向抽象编程有 2 个最大好处。

一方面,面向抽象编程可以将系统中经常变化的部分封装在抽象里,保持核心模块的稳定。

另一方面,面向抽象编程可以让核心模块开发者从非核心模块的实现细节中解放出来,将这些非核心模块的实现细节留在后期或者留给其他人。

这篇文章讨论的实际主要偏重第一点,即封装变化。封装变化是构建一个低耦合松散系统的关键。

这篇文章,作为面向抽象编程的入门,希望能帮助一些同学认识面向抽象编程的好处,以及掌握一些基础的面向抽象编程的方法。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/GG6AtBz6KgNwplpaNXfggQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236862次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6847次阅读
 目录