亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

寫得好的代碼也是一種藝術形式

拼贴画是基于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·马丁,《清洁代码:敏捷软件工艺手册》

“任何人都能写出计算机可以理解的代码。优秀的程序员写出易于人类理解的代码。”
马丁·福勒

清晰性是使代码美观(就像文艺复兴时期的绘画一样)的关键因素之一。我总是力求写出他人可以轻松阅读并因此轻松理解的代码。复杂的代码不仅会浪费程序员的时间,还可能导致因误解或错误修改代码而产生的严重错误,这可能会给公司带来巨大的经济损失。此外,难以阅读的代码就像滚雪球一样,下一个需要修改代码的人可能会选择最简单的路径来完成任务,通常不会进行代码重构。这意味着更多的糟糕代码会被添加进去,让后续接手的人更加头疼。

下面五件事能帮你让代码更清晰:

  1. 命名:为类、变量和方法使用描述性强、信息丰富且简洁的名称。尽量简洁。比如,为常量也要命名。良好的命名可以减少对注释的需求。记住,在代码中,好的命名非常重要。与绘画不同,如果随便给作品命名为“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,它使用方法链和级联来构建领域特定语言。简单来说,逻辑的不同部分(方法调用)被组织成像一句话那样,这样使用起来非常直观且声明式。

  1. 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

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消