拼贴画是基于Wellington Silva的这张照片和Markus Spiske这张照片制作的:https://www.pexels.com/photo/people-taking-photos-of-the-painting-hanging-on-the-wall-14638945/ 和 https://www.pexels.com/photo/coding-script-965345/
“作为一名程序员和艺术家,这是其中最好的部分之一:在创造令人愉悦的事物时感受到那种兴奋。就像刚出炉的面包香气弥漫整个房间时你对品尝它的期待。”
— Dr. Joy Buolamwini, 揭开AI的面纱
他们首先教你的是,你的代码首先要正确;它应该能在所有输入下正常工作,并且能够处理异常和边界情况。然后,你开始学习效率;你了解不同的算法有不同的复杂度,并试着找到解决问题的最佳方法。随着时间和合作的增加,你开始学习行业中的各种编码规范和最佳实践。但是下一步会是什么呢?什么样的特质能让一个程序员成为工匠级别?什么样的代码才能称为语法上的杰作?
你觉得什么样的代码算是好看的?正如我们在音乐、绘画或文学中所见的那样,美是主观的,就像在这些艺术形式中一样。在我看来,几个特点可以让你的代码更漂亮,以下是我认为最重要的三个特点:
安塞尔·亚当斯的提顿山与蛇河(公共版权)
怎么样才能让文字容易读“专业人士明白清晰性是最重要的。专业人士运用他们的能力,编写易于理解的代码。”
― 罗伯特·C·马丁,《清洁代码:敏捷软件工艺手册》“任何人都能写出计算机可以理解的代码。优秀的程序员写出易于人类理解的代码。”
― 马丁·福勒
清晰性是使代码美观(就像文艺复兴时期的绘画一样)的关键因素之一。我总是力求写出他人可以轻松阅读并因此轻松理解的代码。复杂的代码不仅会浪费程序员的时间,还可能导致因误解或错误修改代码而产生的严重错误,这可能会给公司带来巨大的经济损失。此外,难以阅读的代码就像滚雪球一样,下一个需要修改代码的人可能会选择最简单的路径来完成任务,通常不会进行代码重构。这意味着更多的糟糕代码会被添加进去,让后续接手的人更加头疼。
下面五件事能帮你让代码更清晰:
- 命名:为类、变量和方法使用描述性强、信息丰富且简洁的名称。尽量简洁。比如,为常量也要命名。良好的命名可以减少对注释的需求。记住,在代码中,好的命名非常重要。与绘画不同,如果随便给作品命名为“Galacidalacidesoxyribonucleicacid”,你不会被认为是超现实主义艺术家中最优秀的艺术家之一。
// 不要这样做
public static double bmi(final double w, final double h) {
return w / (h * h);
}
// 要这样做
public static double calculateBMI(final double weight, final double height) {
return weight / (height * height);
}
// 不要这样做
public static int size(final int[] 用户提交的电影数组) {
return 用户提交的电影数组.length * 4;
}
// 要这样做
public static int getSizeInBytes(final int[] arr) {
return arr.length * Integer.BYTES;
}
2. 简化:尽量使代码保持简洁。将逻辑拆分成类和方法,这样每个组件都有明确的单一职责。让代码行保持简短,让逻辑易于理解和跟踪。
// 避免这种写法
final int[] arr = new int[10];
for (int i = 0; i < arr.length - 1; arr[i + 1] = arr[i]++ | 1 << i++) {}
// 试试这种写法
final int[] arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) Math.pow(2, i);
}
arr[arr.length - 1]--; // 就是这样
3. 避免过早优化: 写出今天运行很好的简单代码更好,比为了你永远不需要的场景或性能而过度设计的代码要好。遇到问题时再改进代码是很常见的,但是很少有人会去简化这些过去的过度复杂的代码。
// 实现如下
public int gcd(final int a, final int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
// 初次尝试时不要这样做,除非你有特别好的理由
// 缓存用于存储计算过的最大公约数以提高效率
private final Cache<Pair<Integer, Integer>, Integer> cache = Caffeine.newBuilder()
.maximumSize(100_000)
.build();
public int gcd(final int a, final int b) {
if (b == 0) {
return a;
}
final Pair<Integer, Integer> key = Pair.of(b, a % b); // 创建键
Integer result = cache.getIfPresent(key);
if (result != null) {
return result;
}
result = gcd(b, a % b);
cache.put(key, result);
return result;
}
4. 避免深层次嵌套: 当你发现缩进太深,接近屏幕的一半时,可以考虑反转条件。这通常意味着你要从错误处理开始,并跳出当前代码段。
// 以下为不应采用的方式
public Long getLatestDateInSecondsSinceEpoch(final List<Map<String, LocalDateTime>> list) {
long maxValue = Long.MIN_VALUE;
if (list 不为空) {
for (final Map<String, LocalDateTime> map : list) {
if (map 不为空) {
for (final LocalDateTime date : map.values()) {
if (date 不为空) {
maxValue = Math.max(maxValue, date.atZone(ZoneId.systemDefault()).toEpochSecond());
}
}
}
}
}
return maxValue;
}
// 以下为推荐的方式
public Long getLatestDateInSecondsSinceEpoch(final List<Map<String, LocalDateTime>> list) {
long maxValue = Long.MIN_VALUE;
if (list 为空) {
直接返回 maxValue;
}
for (final Map<String, LocalDateTime> map : list) {
if (map 为空) {
继续下一次循环;
}
for (final LocalDateTime date : map.values()) {
if (date 为空) {
继续下一次循环;
}
maxValue = Math.max(maxValue, date.atZone(ZoneId.systemDefault()).toEpochSecond());
}
}
return maxValue;
}
5. DRY(不要重复自己): 尽量不要代码重复。如果不同地方有相同的(或相似的)代码,考虑提取到共享位置,并供两种情况使用。代码越少,越容易跟踪。我知道有些人可能会说(比如那些更聪明的人)伟大的艺术家如安迪·沃霍尔、M. C. 尤尔和雷内·马格里特做过重复的艺术作品——但请相信我,程序员不喜欢重复代码。
// 以下是不要的做法
private static Character getFirstLexicographicallyOrderedLetter(final String str) {
if (str == null || str.isEmpty()) {
return null;
}
final String lowerCaseStr = str.toLowerCase();
Character first = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (first == null || c < first)) {
first = c;
}
}
return first;
}
private static Character getLastLexicographicallyOrderedLetter(final String str) {
if (str == null || str.isEmpty()) {
return null;
}
final String lowerCaseStr = str.toLowerCase();
Character last = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (last == null || c > last)) {
last = c;
}
}
return last;
}
private static Character getFirstLexicographicallyOrderedVowelOrLetter(final String str) {
if (str == null || str.isEmpty()) {
return null;
}
final ToIntFunction<Character> score = "zyxwvtsrqpnmlkjhgfdcbuoiea"::indexOf;
final String lowerCaseStr = str.toLowerCase();
Character first = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (first == null || score.applyAsInt(c) > score.applyAsInt(first))) {
first = c;
}
}
return first;
}
// 以下是推荐的做法
private static Character getFirstLexicographicallyOrderedLetter(final String str) {
return getLetterByComparator(str, Comparator.reverseOrder());
}
private static Character getLastLexicographicallyOrderedLetter(final String str) {
return getLetterByComparator(str, Comparator.naturalOrder());
}
private static Character getFirstLexicographicallyOrderedVowelOrLetter(final String str) {
final ToIntFunction<Character> charToVowelScore = "uoiea"::indexOf;
return getLetterByComparator(str, Comparator.comparingInt(charToVowelScore)
.thenComparing(Comparator.reverseOrder()));
}
private static Character getLetterByComparator(final String str, final Comparator<Character> comparator) {
if (str == null || str.isEmpty()) {
return null;
}
final String lowerCaseStr = str.toLowerCase();
Character result = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (result == null || comparator.compare(c, result) > 0)) {
result = c;
}
}
return result;
}
“代码读得多,写得少。”
— Guido van Rossum。“代码就像幽默,如果需要解释,那它就不够好。”
― Cory House。
照片由 Alina Grubnyak 拍摄,来自 Unsplash
语法糖和新功能“你写的代码让你成为程序员,你删掉的代码让你成为更好的程序员,你不需要写的代码让你成为顶尖的程序员。”
——马里奥·富索“好软件的作用是将复杂的东西变得简单。” ——格雷迪·布鲁奇
一个好的程序员应该熟悉内置的库和特性,并且关注新的发展和动态。新版本的语言往往会提供优雅的解决方案来解决现有的问题,并用更好的语法替换旧的、难看的语法。例如在Java中,用字符串模板代替字符串拼接,用记录代替不可变类,以及更高级的模式匹配和增强的switch语句。
看到一行代码时能感叹“哇,原来我们还可以这样写”,或是“这种语法选择真棒”,而不是“为什么这段代码重新发明了轮子?从Java 8起就有标准实现方法了。”总是感觉很棒。这反映了进展。从猪膀胱涂抹管子到使用羽毛笔,从胶片相机到数码相机,这些都是相似的转变,还有使用brew安装工具。
// 以下为不应采用的方法
private void 添加到计数(final List<String> list) {
for (int i = 0; i < list.size(); ++i) {
final String str = list.get(i);
final Integer count = counter.get(str);
if (count == null) {
counter.put(str, 1);
} else {
counter.put(str, count + 1);
}
}
}
// 以下为推荐的方法
private void 添加到计数(final List<String> list) {
list.forEach(str -> counter.merge(str, 1, Integer::sum));
}
曾有人说过:“在语言中,我们最应该警惕的就是那句‘我们一直这么做的’。”——格蕾斯·霍普
神奈川冲浪图 / 葛饰北斋。公共领域作品,作为大都会艺术博物馆开放获取政策下的公共领域标记(CC0)。
可维护性“生活中唯一不变的就是这个变化的事实。”
—— 赫拉克里特
正如大家都知道的,添加新的类和方法只是工作的一部分而已,在很多情况下,我们还需要修改现有的类或方法。这可能只是修复一个小bug或增加一个新的指标,但在很多时候,我们还得支持全新的功能或特性,这些功能或特性原本并未考虑进来。
发现旧代码可以轻松修改以适应新需求,这真是最美妙的事情。同时又不会显得生硬或不协调。就像人一样,代码也应该能够优雅地应对变化,经受住未来你或他人可能的调整和修改。
等等,效率呢?怎么说?生活就是做选择,有时候这些选择会牺牲一些东西。在性能与清晰度之间,我建议选择最易读的版本,但要确保不会超出系统的性能限制。有时你可能不得不牺牲代码的优雅性,去处理大量的对象分配和动态调用。在某些情况下,你可能只能写出一些高效的但略显原始的代码。
例如,在大多数情况下,我会建议使用 i * 2 而不是 i << 1。左移通常比乘法在底层指令上更快,但后者可能会让其他程序员(甚至未来的你)感到困惑,因为这可能不容易理解。另外,很多编程语言中,编译器可能会自动将乘法优化成左移操作。
一些写得漂亮的代码示例如我之前所说——美在人心中,而将现实生活中的例子带入博客文章中可能涉及许多文件和代码行。因此,这里我将展示几个简短的例子,叫做“流畅接口”,这是一种我非常欣赏的编程方式。流畅接口是一种面向对象的API,它使用方法链和级联来构建领域特定语言。简单来说,逻辑的不同部分(方法调用)被组织成像一句话那样,这样使用起来非常直观且声明式。
- Hamcrest: 一个用于匹配断言的框架,内置了多种匹配器,并支持编写自定义匹配器。
assertThat("foo", equalToIgnoringCase("FoO")); // 断言 "foo" 等于忽略大小写的 "FoO"
assertThat(Cat.class,typeCompatibleWith(Animal.class)); // 断言 Cat 类型兼容于 Animal 类型
assertThat(person, hasProperty("city", equalTo("New York"))); // 断言 person 具有属性 "city" 等于 "New York"
assertThat(list, containsInAnyOrder("hello", "world")); // 断言 list 包含任意顺序的 "hello", "world"
assertThat(array, hasItemInArray(42)); // 断言 array 数组中有项 42
assertThat(map, hasEntry(key, value)); // 断言 map 包含条目 key, value
assertThat(1, greaterThan(0)); // 断言 1 大于 0
assertThat("test", equalToIgnoringWhiteSpace(" test")); // 断言 "test" 等于忽略空格的 " test"
assertThat("congratulations",stringContainsInOrder(Arrays.asList("con","gratul","ations"))); // 断言 "congratulations" 包含顺序的 "con", "gratul", "ations"
assertThat("congratulations", startsWith("cong")); // 断言 "congratulations" 以 "cong" 开头
assertThat(list, everyItem(greaterThan(42))); // 断言 list 每一项都大于 42
assertThat("congratulations", anyOf(startsWith("cong"), containsString("ions"))); // 断言 "congratulations" 任意一个以 "cong" 开头 或 包含字符串 "ions"
2. Awaitility: 一个名为Awaitility的库,用于异步系统测试,允许用户自定义等待设置。
await()
.atMost(5, SECONDS)
.until(result::isNotEmpty);
使用()
.pollInterval(1, SECONDS)
.await("等待将行插入到数据库")
.atMost(1, MINUTES)
.until(直到新行被添加())
3. DataStax Java 驱动: 用于程序化生成 CQL 查询的驱动。
Select select =
selectFrom("keyspace", "table")
.column("name")
.column("age")
.whereColumn("id").isEqualTo(bindMarker());
// 等同于 SELECT name, age FROM keyspace.table WHERE id=?
deleteFrom("table")
.whereColumn("key").isEqualTo(bindMarker())
.ifColumn("age").isEqualTo(literal(28));
// 删除 FROM table WHERE key=? IF age = 28
insertInto("table")
.value("name", bindMarker())
.value("age", bindMarker())
.usingTtl(60)
// 插入 INTO table (name, age) VALUES (?, ?) USING TTL 60
照片由Sara Darcaj拍摄,来自Unsplash。
结尾解决同一个问题有很多不同的方法,每个情况都有其考量,从而导向不同的路径。你的目标不是选择更容易实现的那一个,而是选择更容易理解和维护的那一个。选择伴侣也是一样的,因为你会和他的/她在一起很长一段时间。弗朗西斯科·戈雅无疑是西班牙浪漫派画家中最伟大的画家之一,但我不认为会把《Saturn吞食一个儿子》这种画放在婴儿房里——你应该根据观众选择适合的艺术品。
“构建软件设计有两种方式:一种是让它足够简单,以至于显然没有缺陷;另一种是让它足够复杂,以至于没有明显的缺陷。第一种方法要困难得多。”
- C.A.R. Hoare
共同學習,寫下你的評論
評論加載中...
作者其他優質文章