小程序中的大道理--综述

前言

以下将用一个小程序来探讨一些大道理, 这些大道理包括可扩展性, 抽象与封装, 可维护性, 健壮性, 团队合作, 工具的利用, 可测试性, 自顶向下, 分而治之, 分层, 可读性, 模块化, 松耦合, MVC, 领域模型, 甚至对称性, 香农的信息论等等.

为什么不用大程序来说大道理呢?

因为大程序太大了, 代码一端上来, 读者就晕菜了, 看不过来甚至压根不想去看, 这样说理就很抽象了, 效果反而不好.

小程序中也能说出大道理来吗?

我们有句话, 叫"以小见大", 我们又常常有种说法, 叫:

麻雀虽小, 五毒俱全. (咦? 好像应该是五脏俱全…总之你明白我的意思就好了. )

所以呢, 小程序也是可以来说大道理的, 而且小程序又有短小的特点, 大家看得也没那么累, 也很快能看懂. 毕竟那种代码, 叫什么来着, “意大利面条式的代码”, 大家在实际的开发中, 已经见得太多了.

意大利面(spaghetti), 翻译过来好像叫"通心粉", 非常长的一条条, 彼此缠缠绕绕的, 所以"意大利面条式的代码"就是又长又绕, 让人非常头痛的那种代码.

按我们的习惯, 也许叫它"裹脚布式的代码"大家觉得更熟悉, 更形象一点, 也正好符合"又长又臭"的特点.

啊, 说"又长又臭"可能有点刻薄了, 毕竟大家都可能写过这样的代码(本人就写过好多), 即便现在不会再写这样的代码, 想当年应该也是写过的, 除非你从一开始觉悟通天, 那我就无话可说了.

这种代码我们在工作中见得太多了, 所以这里就不再弄出来考验大家的毅力了, 闲话少提, 让我们看个简单的例子.

我们的例子

就是要打印出如下的一个三角形图案:

  *
 ***
*****

当然了, 这只是以三行为一个示例, 我们的程序应该接受任意的正整数, 比如, 给一个 5, 就要能打出 5 行的类似的三角形来. 让我们来看看如何写出这样一个程序, 并在这个过程中借此兜售我们的大道理.

玩具式的代码

我知道很多"数学帝"可能一眼就被图案中的规律吸引过去了, 他们很快就指出星号是等差数列, 然后很快就弄出了计算每行前面要缩进多少个空格的公式, 然后呢, 一层循环, 二层循环, blablablah…然后最里面几条优雅而性感的 print 语句, 搞掂!一种智商上的优越感油然而生, 接着他们可能就要问:

这么简单的东西, 你也好意思拿出来讲?

下面是这样的一个代码, 能够完全实现以上要求(只演示了 3 行的情况):

public static void main(String[] args) {
    int i = 3;
    for (int j = 0; j < i; j++) {
        for (int k = 0; k < i - j; k++) {
            System.out.print(" ");
        }
        for (int z = 0; z < 2*j+1; z++) {
            System.out.print("*");
        }
        System.out.println();
    }
}

这里用的是 java 语言来演示, 包括以下的. 我相信像 java 这样烂大街的语言, 即使你没这个背景看懂也不是难事, 在代码中也不会用到什么高深的特性. (这一点皆因我的能力有限所导致, 而不是想装逼的意愿所能决定的~)

怎么说呢, 我们不要以上那种"玩具式"的代码(toy program), 我们要的是生产级(production, 生产环境)的代码.

生产级的代码

让我们来看看如何写出这样的代码.

可扩展性(Extensibility)

首先呢, print 语句是绝对要避免的. 你要明白, print 语句写得太死, 而需求是不断在变化的, 有句话是怎么说的?

唯一不变的就是变化本身.

客户今天跟你说的是要 print 这个图案, 你要是按着客户怎么说, 你就怎么做, 你可就惨了.

客户哪一天突然又会说, 再加点特性, 要能输出到文件;哪一天又说, 再加点 web service, 能供其它程序调用.

让我们多留点心眼, 代码如下:

public void printPattern(int lineCount) {
    String pattern = getPattern(lineCount);
    System.out.print(pattern);
}

我们先借助 getPattern 方法拿到要打印的内容, 这样, 如果哪天要输出到文件, 哪天要供 web service 调用, 我们都可以把这个 getPattern 方法提供出去.

我们只要多抽象出那么一层来, 就会给我们带来很多方便.

抽象与封装(Abstraction & Encapsulation)

抽象与封装同时也是很多其它特性的基础, 在后面我们还会不断说到这一主题.

getPattern 就是一个抽象, 是对一系列动作的一个封装.

可能有人会比较教条地认为抽象与封装只能在类层次中进行, 这常常导致在类的内部缺少必要的抽象层次, 常常是一大件事情在一个方法里完成, 方法巨大巨长无比, 这样的所谓面向对象编程不过是虚有其表, 其模块性甚至还比不上那些用面向过程语言写就的代码.

printPattern 层面, 我们不需要知道 getPattern 的细节, 我们只需要传入所需参数及定义好需要的返回值即可.

大道理: 定义好输入与输出, 描述清楚想要做的事, 先不用去管细节.

然后呢, 我们是不是需要手动去把这个方法写出来呢?

利用好你的工具(Tools)

你不用手动去做这些, 以 eclipse 为例, 只要把光标定位到错误的地方(可以按Ctrl+“.”(点)快速定位), 然后按下"Ctrl+1", 然后选择"Create Method"即可:

eclipse create method

工具将根据传入参数及返回值自动为我们生成方法, 结果如下:

eclipse create method

只要输入与输出定义清楚了, 工具就能自动帮我们生成方法定义, 这里默认它是 private 的, 我们可以把它改成 public.

这里说的是 Eclipse 这个 IDE, 其它的我相信也会有类似的功能. 如果你偏好轻量级的文本编辑器, 那我就不敢说也一定有这些功能了.

利用好任务标识(Task Tags)

我们可以看到, 生成的代码里有个 TODO, 显示出了特别的颜色, 这是个任务标识.

类似的标识还有 FIXME, XXX, 甚至你还可以自定义标识.

打开 eclipse 的菜单-- windows–preferences, 在过滤框中输入"task tag":

eclipse task tag

这些有什么用呢? 我们可以看下, 在编辑器的左右侧, 都有显著的标志提示有个任务标识存在;在 Markers 视图里, 有列举出这些标识:

eclipse todo task tag

在代码质量分析工具 sonar 中, 它也会追踪这些标识. 下图是我在 sonar.oschina.net 上的一个项目的截图:

sonar todo tag

这些有什么用呢? 我们在写代码中, 写到一半, 很可能被某些难题卡住了, 为了不中断正常的流程, 我们先用个 TODO 来标识, 然后就可以继续地把一些简单的问题先处理完, 再回过头来对付这些.

又或者像现在这样, 我们生成了出来了这个方法, 工具为我们自动加了入"TODO"的标识, 毕竟方法的主体还没有, 可不巧的是, 现在到了下班时间了, 然后呢, 我们就可以存盘并提交到 svn 或者 git 上去了. 有人可能要说: "啊? 不是吧, 你的代码都没写完你怎么就提交了? "

没关系, 我们已经标识好了 TODO, 所以它会提醒我们还有工作是没做完的. 另外我们为何如此着急提交呢? 因为我们并不是在单打独斗:

团队合作(Teamwork)

我们前面说了, 我们可能还要做输出图案到文件的需求, 很可能你有个同事哥们, 他就正做着这个模块, 而他现在呢, 就在等着你这个 getPattern 方法. 你提交了, 他就可以继续写他的代码了:

package org.jcc.core.demo;

public class PatternFile {
    
    private Pattern pattern;

    public PatternFile(Pattern pattern) {
        this.pattern = pattern;
    }
    
    public void generatePatternFile(int lineCount) {
        String content = pattern.getPattern(lineCount);
        saveInFile(content);
    }

    private void saveInFile(String content) {
        // TODO Auto-generated method stub
        //System.out.println(content);
    }
}

可以看到, 他的类依赖你的类, 在他的方法 generatePatternFile 里还调用了 getPattern 方法, 你没实现, 那又怎样呢? 接口好了就行了!

面向接口编程(Interface)

有人可能比较死板, 比较教条主义, 以为呢, 说到接口就一定要弄个 interface, 其实呢, 我们这个方法 getPattern 就是一个承诺, 一个约定, 一个协议, 也是一个广义上的接口.

有人可能要问, 你方法细节还没有实现, 他怎么测试? 别担心, 办法会有的:

利用 Mockito 来测试

代码如下:

@Test
public void testGeneratePatternFile() {
    // 用mockito来模拟接口的行为, 为此我们手动构建一个三行的图案
    Pattern pattern = Mockito.mock(Pattern.class);
    String mockContent = "  *" + System.lineSeparator() 
                       + " ***" + System.lineSeparator() 
                       + "*****" + System.lineSeparator();
    
    // 当调用getPattern方法时, 就返回这里定义好的内容. 
    Mockito.when(pattern.getPattern(3)).thenReturn(mockContent);
    
    // 测试generatePatternFile方法, 在它的里面将会调用getPattern方法
    PatternFile pf = new PatternFile(pattern);
    pf.generatePatternFile(3);
    
    // TODO 断言文件存在并且文件中的内容与mockContent一致
    // assert that file is exists and content in file is equals the mock content
}

以上我们用一个 mock 对象以及 when, thenReturn 来主动模拟一个尚未实现的方法.

你也许对 Mock 之类的技术还不太了解, 但这些词表达的意思我想大家都不难明白. Mock 的更详细介绍请自行百度之.

借助 Mockito, 这个哥们就可以这样写好他的代码, 并完成他的测试了, 然后可以提交他的代码, 宣布工作完成, 接着他就可以飞到马尔代夫去度假去了.

可以看到, 尽管我们的功能八字还没一撇, 可只要我们坚持面向接口编程, 时时想着团队合作, 经常提交已经写好的代码, 特别是公共接口方面的代码, 我们的同事就能及时推进他们的工作, 甚至比我们还早完成, 这都是有可能的, 都是正常的, 也是我们应该追求的.

而利用好抽象及封装, 我们还能得到好几个好处:

可测试性(Testability)

通过以上举例, 可以看到, 我们可以手动构建一个图案, 并交给程序去判断(注: 为了简短起见, 代码中省略了具体的 assert 细节). 而如果是开头那样直接就打印了呢? 你根本没法让程序去判断, 只能通过人眼去观察输出, 这样就给 自动化的测试(Automatic Test) 带来了困难.

可重用性(Reusability)

getPattern 被抽象出来之后, 可以看到, 不但可以在 printPattern 方法里使用, 也可以在 generatePatternFile 方法里使用. 而如果按开头那样呢? 你没法复用, 你还是不得不重构;又或者你可能只是简单地把代码复制一遍了, 再作些改动.

当然, 现在这个程序很小, 全部拷贝一遍好像也很快, 但如果是很大的程序呢? 又或者我们又要拓展到可供 web service 调用, 难道就这样拷贝下去? 哪一天程序要做些小调整, 难道又要一一去修改吗?

不要重复(DRY: Don’t Repeat Yourself)

管理重复性一直都是程序开发中的重大关切, 在目前这个小程序里, 这一问题还不是那么迫切, 这个在此就不作详述, 以后会另写一些文章来做些介绍.

好了, 说了一大通, 绕了一大圈, 测试也测了, 同事也度假去了, 我们也要赶紧我们的工作. 那么接下来是不是赶紧写那些实现呢? 不!

我们已经介绍了不少的"ility"结尾的单词, 接下来还要说到!我有点担心大家说我"zhuangbility", 有句话说: “Don’t zhuangbility, zhuangbility leads to leipility”(莫装逼, 装逼遭雷劈), 没办法, 为了阐述这些大道理, 我也只好冒着被 leipility 的危险.

可维护性(Maintainability)

你首先把注释写好:

怎么说呢? 现在 IT 工作强度很大, 过劳死是不稀奇的事, 写着写着说不定哪天人就挂了. 一个人挂了不要紧, 工作可不能挂!(不是在说笑话, 貌似有些公司或老板表现出来的态度就是这样的~)

别人要能顺利接手你的活, 这是关键.

其实没必要说"挂了"这些不吉利的话, 也可能是有人要生了, 比如你老婆要生了, 你也休产假去了, 你写到一半, 老板把你的工作转交给你的同事.

试想, 要是一点注释都没有, 你的同事接手起来就很困难, 他要加班加点才能早点弄清你的代码的意图. 所以呢, 不要害了你的同事!把代码的可维护性做好, 大家的健康也才有更好的可维护性!

代码如下:

/**
 * 获取指定行数的图案, 比如3行时: 
 *   *
 *  ***
 * *****
 *  
 * @param lineCount 指定的行数
 * @return 图案的字符串表示, 包括换行符在内
 */
public String getPattern(int lineCount) {
    // TODO Auto-generated method stub
    return null;
}

其实, 良好的命名同样也是可维护性的关键, 比如上面的 getPattern, lineCount, 而不是像最前面那个示例中的 i, j, k, z 等乱七八糟的名字.

另外, 丰富的抽象层次也是如此, 这点我们后续还会不断提及.

好了, 注释也写完了, 然后呢, 现在该轮到写那个该死的等差数列了吧? 不!

健壮性(Robustness)

Robustness 又常常音译成鲁棒性.

作者在大学时读的是自动化专业, 在那些自动控制理论里, 老出现什么鲁棒性, 看了让人犯晕, 不如直接叫健壮性.

我倒是想起了小时候老爸常买给我喝的 Robust(即乐百氏, 与娃哈哈类似的饮品), 味道是不错, 不过喝完身体挺没见得健壮到哪去, 也许喝得还不够多~

我们先要把判断做好, 输入负数或者输入的数字太大了, 你要拒绝它们, 同时在注释中也作出说明:

/**
 * 获取指定行数的图案, 比如3行时: 
 *   *
 *  ***
 * *****
 *  
 * @param lineCount 指定的行数, 1-20之间
 * @return 图案的字符串表示, 包括换行符在内
 */
public String getPattern(int lineCount) {
    if (lineCount < 1) {
        throw new IllegalArgumentException("行数不能小于1!");
    }
    if (lineCount > 20) {
        throw new IllegalArgumentException("行数不能大于20!");
    }
    
    // TODO 
    return null;
}

我知道我在这里说这些, 有些人可能已经不耐烦了, 他们想着的是写那些有技巧的代码, 那些有挑战性的部分, 那些 tricky 的部分, 那些能体现出他们智商上的优越感的部分.

有个词是怎么说的, “rocket science”(火箭科学, 喻指那些高精尖的技术), 特别的有些刚毕业的心气很高的学生, 满脑子想的可能就是这些. 可是呢, 类似情况不是没有, 但通常是很少的:

骚年, 不是我在打击你, 你也许真的想多了. 工作上, 我们多数时候处理的都是一些细节的问题, 一些琐碎的事情, 一些按部就班的样板式的代码, 需要的不是多高的智商, 多么 tricky 的技巧, 要是是耐心, 细致, 严谨, 一丝不苟.

为何一开始就要把这些做好呢? 因为到了后面, 你就没时间去做了. 这一点你一定要相信我, 以下引自 wiki 的"90-90法则":

前 90% 的代码要花费你 90% 的开发时间, 剩余的 10% 的代码要花费你另一个 90% 的开发时间.

The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time.

–Tom Cargill, 贝尔实验室

而最后如果因为时间紧急, 就这样没保护就上了生产环境, 一旦出了问题, 你会花更多的时间去收拾这些烂摊子, 而最终你还是不得不将这些补上.

有一个"墨菲定律"(Murphy’s Law)大意是这么说的:

有可能出错的的东西一定会出错.

现在不擦屁股, 后面还有得擦. 你省掉了纸尿裤, 你的程序就裸奔了, 你就等着洗更多的外套.

我们也常说: "该来的一定会来. "如果用电影<<无间道>>里的话来说呢, 那就是:

“出来混, 迟早要还的”. (哇塞, 说得太精彩了. 这些编导或者剧作家不去写教科书太可惜了. )

所以呢, 不要有侥幸的心理, 把程序从一开始就写健壮才是正道.

小结

说了半天, 我们甚至连一行核心代码都没写, 不过, 文章至此倒是要先做一个阶段了结了. 我们说写代码有个原则, 那就是方法不能太长, 最好一个屏幕就能显示完, 否则看起来就很累了;自然的, 文章也不能写得太长, 否则写起来, 读起来都很累人.

所以呢, 虽然一开始那里提了好多的道理, 本来也是想一扒到底的, 但扒到一半发现已经很长了, 所以上半身扒完, 就此腰斩, 下半身留待后面继续扒, 下半身更精彩, 我们下回再见.

下一篇, 见 小程序中的大道理之二 .

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/187556.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flink源码分析之功能组件(二)-kubeclient

简介 本系列是flink源码分析的第二个系列,上一个《flink源码分析之集群与资源》分析集群与资源,本系列分析功能组件,kubeclient,rpc,心跳,高可用,slotpool,rest,metrics,future。其中kubeclient上一个系列介绍过,为了系列完整性,这里“copy”一下。 kubeclient组件…

EI期刊完整程序:MEA-BP思维进化法优化BP神经网络的回归预测算法,可作为对比预测模型,丰富内容,直接运行,免费

适用平台&#xff1a;Matlab 2020及以上 本程序参考中文EI期刊《基于MEA⁃BP神经网络的建筑能耗预测模型》&#xff0c;程序注释清晰&#xff0c;干货满满&#xff0c;下面对文章和程序做简要介绍。 适用领域&#xff1a;风速预测、光伏功率预测、发电功率预测、碳价预测等多…

【部署运维】docker:入门到进阶

0 前言 部署运维博客系列一共有三篇&#xff1a; 拥抱开源&#xff0c;将工作中的经验分享出来&#xff0c;尽量避免新手踩坑。 【部署运维】docker&#xff1a;入门到进阶 【部署运维】kubernetes&#xff1a;容器集群管理掌握这些就够了 【部署运维】pythonredisceleryd…

软件测试:超详细的Jmeter基础教程

JMeter 介绍&#xff1a; 一个非常优秀的开源的性能测试工具。 优点&#xff1a;你用着用着就会发现它的重多优点&#xff0c;当然不足点也会呈现出来。 从性能工具的原理划分 Jmeter工具和其他性能工具在原理上完全一致&#xff0c;工具包含4个部分&#xff1a; &#xff…

python基础教程:动态参数

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 Python的动态参数有两种&#xff0c;分别是*args和**kwargs&#xff0c; 这里面的关键是一个和两个星号的区别&#xff0c;而不是args和kwargs在名字上的区别&#…

二分查找之红蓝二分查找

꒰˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN …

每日一题 1457. 二叉树中的伪回文路径(中等,DFS)

一句话&#xff0c;深度搜索所有路径&#xff0c;判断路径是否伪回文 # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right clas…

AI 视频 | Stable Video Diffusion 来了!(附体验地址)

1. 介绍 11 月 21 日&#xff0c;Stability AI 推出了 Stable Video Diffusion&#xff0c;这是 Stability AI 的第一个基于图像模型 Stable Diffusion 的生成式视频基础模型。 目前 Stability AI 已经在 GitHub 上开源了 Stable Video Diffusion 的代码&#xff0c;在 Huggin…

基于springboot实现乒乓球预约管理系统项目【项目源码】

基于springboot实现乒乓球预约管理系统演示 系统的开发环境 浏览器&#xff1a;IE 8.1&#xff08;推荐6.0以上&#xff09; 开发使用语言&#xff1a;JAVA JDK版本&#xff1a;JDK_8 数据库管理系统软件&#xff1a;Mysql 运行平台&#xff1a;Windows 7 运行环境&#…

外贸分享|如何从外贸小白成长为大咖?这10件事值得你坚持做

外贸成功不是一朝一夕的事&#xff0c;而是需要有充分的准备和持续的努力。作为一位有着丰富经验的外贸人员&#xff0c;我总结了成功的秘诀&#xff0c;分享了一个优秀的外贸人应该做好的10项工作。 1 找不到客户怎么办&#xff1f; 有很多各种各样的原因值得思考&#xff1a…

机器学习-线性回归

线性模型是一类用于建模输入特征与输出之间线性关系的统计模型。这类模型的基本形式可以表示为&#xff1a; 其中&#xff1a; 是模型的输出&#xff08;目标变量&#xff09;。 是截距&#xff08;常数项&#xff0c;表示在所有输入特征都为零时的输出值&#xff09;。 是权重…

禁止指定电脑程序运行的2种方法

你可能要问了&#xff0c;为什么要禁止电脑程序运行呢&#xff0c;因为有的公司要净化公司的工作环境&#xff0c;防止某些刺头员工在公司电脑上瞎搞。也有部分家长&#xff0c;是为了防止自己家的孩子利用电脑乱下载东西。 今天就分享2种禁止指定电脑程序运行的方法&#xff1…

教你IDEA解决GIT冲突

前言 GIT基本上贯穿我们的开发生涯&#xff0c;之所以要使用git也是有很多优点的 &#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339; 1.通俗易懂点&#xff0c;保存代码不丢失&#xff1a;防止因内存&#xff0c;操…

pulseaudio是如何测试出音频延迟的

通常专业的音频设备生产厂商都有专业的设备来测试精确的音频链路延时。 那么没有专业设备怎么测试出音频延迟呢?如下图,我们可以看到pulseaudio可以测试出硬件音频延迟。 那么,他是怎么测试出硬件延迟的呢?他的理论依据是什么呢?接下来我带大伙一起探索一下。 /*占位…

一篇文章让你入门python集合和字典

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 一、集合: 增加 add 删除 del 删除集合 discard(常用)删除集合中的元素 &#xff0c;删除一个不存在的元素不会报错 remove 删除一个不存在的元素会报错 pop随…

Spine深入学习 —— 数据

atlas数据的处理 作用 图集&#xff0c;描述了spine使用的图片信息。 结构 page 页块 页块包含了页图像名称, 以及加载和渲染图像的相关信息。 page1.pngsize: 640, 480format: RGBA8888filter: Linear, Linearrepeat: nonepma: truename: 首行为该页中的图像名称. 图片位…

【点云surface】Poisson表面重建

1 介绍 Poisson表面重建算法是一种用于从点云数据生成平滑曲面模型的算法。它基于Michael Kazhdan等人在2006年发表的论文《Poisson surface reconstruction》。该算法通过将点云数据转换为体素表示&#xff0c;并利用Poisson方程来重建曲面。 该算法的基本原理是将点云数据转…

python教程:正常shell与反弹shell

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 正常shell需要先在攻击端开机情况下开启程序,然后攻击端运行程序,才能连接 反弹shell,攻击端是服务端,被攻击端是客户端 正常shell,攻击端是客户端,被攻击端是服务端 反弹shell,先启用服务端,再启用客户端 反弹shell的好处…

2022年09月 Scratch(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 运行下列程序后,结果为120的是? A: B: C: D: 答案:C 本题考察阶乘知识,12345的结果为120. <

【Python自学】七个超强学习网站,你值得拥有!

学习Python最主要的还是要动手&#xff0c;去找一些自己感兴趣的脚本&#xff0c;代码去练习&#xff0c;练的越多&#xff0c;对于一些英语单词&#xff0c;特殊符号要比死记硬背要容易记得些。 以下这些网站&#xff0c;虽说不上全方位的满足你的需求&#xff0c;但是大部分也…