原文转载自:https://mp.weixin.qq.com/s/gOahvFuVPYi_yWVNH1u4zw

引子

对于计算机从业者而言不论你的母语是什么语言,中文,英语或是法语,西班牙语等,你的第一工作语言都是编程语言,你一定听说过那句话 “talk is cheap show me the code”。所以,快速学习和掌握编程语言一直以来都是每一个工程师梦最想要拥有的超能力。

我从小学开始学习编程,在后来17年的职业生涯中也主动和被动的学习了一众编程语言,如C/C++,Java,Python,Haskell,Groovy,Scala,Clojure,Go等等,在这期间付出了很多努力,取得了不少经验,当然也走过了更多弯路。下面分享一下自己的学习心得,希望可以对大家的学习有所帮助和借鉴。

掌握编程范式优于牢记语法

各种编程语言里的独特语法简直是五花八门,下面就随便选取了其中几种语言,看看你们知道他们都是什么语言吗?

1.

def biophony[T <: Animal](things: Seq[T]) = things map (_.sound) def biophony[T >: Animal](things: Seq[T]) = things map (_.sound())

2.

quicksort [] = [] quicksort (x:xs) =                 let smaller = [a|a<-xs,a<=x]                    larger = [a|a<-xs, a>x]                in quicksort smaller ++[x]++ quicksort larger

3.

my @sorted = sort {$a <=> $b} @input; my @results = map {$_ * 2 + 1} @input;

很多工程师往往把学习语言的重点放在了学习不同语言的语法上,而忽略了语言背后的思想及适合的应用场景。

其实对于编程语言的学习,意义最大,收获最大的就是对于编程思想的学习。正如著名的计算机学者,首位图灵奖获得者,Alan Perlis说的那样如果一个编程语言不能够影响你的编程思维,这个语言便不值得学习。

“A language that doesn’t effect the way you think about programming, is not worth knowing.” – Alan Perlis

程序语言的编程思想主要受到编程范式的影响,如果了解这点你就会发现很多新语言其实是新瓶装老酒。

编程范式(programming paradigm):

官方的定义是:A programming paradigm is a style, or “way,” of programming. (一种编程风格和方式)

以下是目前广泛应用的一些编程范式:

  • 结构化 (Structured)
  • 函数式 (Functional)
  • 面向对象 (Object Oriented)

关于这些典型编程范式相信你已经有所耳闻,也可以在网络上找到很多详细的相关资料,这里就不再赘述,仅通过一些简单的实例对比,来让大家认识到不同编程范式对程序设计思想的影响。

结构化  vs. 函数式

我们通过快速排序的实现来看看这两种编程范式的差别:

快速排序 结构化实现:Groovy语言实现

class QuickSort {   
       private static void exch(int pos1,int pos2,List data){
             int tmp=data[pos1];
             data[pos1]=data[pos2];
             data[pos2]=tmp;
       }
       private static void partition (int lo,int hi, List a){                   
             if (lo<0||hi<0||lo>=hi-1){
                    return;
             }
             int midValue=a[lo];
             int i=lo+1;int j=hi;
             while (true){
                    while(i<hi&&a[i]<midValue){ i++; } while(j>lo&&a[j]>midValue){
                           j--;
                    }
                    if (i>=j) break;
                    exch(i,j,a);        
             }
             exch(i,lo,a);       
             partition(lo,i-1,a);             
             partition(i+1,hi,a);
             
       }
       public static List sort(List a){
             int lo=0; int hi=a.size()-1;
             partition(lo, hi, a);      
             return a;    
       }
}

快速排序 函数式实现:Groovy实现

def quickSort_fp(List list){
       if (list.size()==0){
             return []
       }
       def x=list[0] 
       def smaller=list.findAll{it<x} def mid=list.findAll{it==x} def larger=list.findAll{it>x}    
       return quickSort_fp(smaller)+mid+quickSort_fp(larger)
       
}

通过以上比较你会发现:对于结构化编程,我们要通过程序来告诉机器怎么做,而函数式编程则更像是通过程序告诉机器我们想要什么。

函数式 vs 面向对象

这里我们通过实现一个通用的计算过程计时功能来比较。大家可以细细体会其中的不同。

面向对象 Go语言实现

以下利用Decorator模式来为不同的Caculator实现的Caculate过程计时 (如果对于下面程序有些疑惑,建议先回顾一下设计模式里的Decorator模式)

type Caculator interface {
  Caculate(op int) int
}

// 通过Decorator模式来实现对不同的
type TimerDecorator struct {
  innerCal Caculator
}

func NewTimerFn(c Caculator) Caculator {
  return &TimerDecorator{
    innerCal: c,
  }
}

func (td *TimerDecorator) Caculate(op int) int {
  start := time.Now()
  ret := td.innerCal.Caculate(op)
  fmt.Println("time spent:",
    time.Since(start).Seconds())
  return ret
}

type SlowCal struct {
}

func (sc *SlowCal) Caculate(op int) int {
  time.Sleep(time.Second * 1)
  return op
}

func TestOO(t *testing.T) {
  // 为了SlowCal添加Decorator 
  tf := NewTimerFn(&SlowCal{})
  t.Log(tf.Caculate(10))
}

函数式 Go语言实现

以下也是通过类似于Decorator的思路来为不同的方法实现添加运行过程计时

// 为通过参数传人的内部方法添加运行过程计时
func timeSpent(inner func(op int) int) func(op int) int {
  return func(n int) int {
    start := time.Now()
    ret := inner(n)
    fmt.Println("time spent:",
      time.Since(start).Seconds())
    return ret
  }
}

func slowFun(op int) int {
  time.Sleep(time.Second * 1)
  return op
}

func TestFn(t *testing.T) {
  tsSF := timeSpent(slowFun)
  t.Log(tsSF(10))
}

通过上面的比较大家可以有个简单认识,在函数式编程中函数是第一公民可以作为参数和返回值,而在面向对象编程中对象才是第一公民。

可以类比,但不要翻译

在学习编程语言时,如果我们已经了掌握了一种语言,通过类比,尤其是比较不同点,可以有助于我们更快的掌握另一种新的语言。

要注意的是学习编程语言也和我们学习自然语言一样,要掌握不同语言的特点,习惯用法,否则就会出现类似于中式英语这样的问题。不管后来学习了什么语言,都是先用熟悉的语言的方式实现,然后翻译成另一种语言,我们常常可以看到C++语言描述的C程序,Go语言描述的Java程序等。

下面给大家举个例子:

让我们来生成一副扑克。

Java中的实现:

public static void main(String[] args) {
     List cards = new ArrayList<>();
     for (int i=2;i<12;i++){
       cards.add(String.valueOf(i));
     }
     cards.addAll(List.of("J","Q","K","A"));
     System.out.println(cards);
  }

Java程序员,学习了Python以后,通常会实现成这样

cards = ['J','Q','K','A']
for n in range(2,11):
  cards.append(str(n))
print cards

上面的Python代码虽然也可以准确的实现功能,但在一个Python程序员眼中上面的代码总觉得不是那么地道,下面是地道的python程序

cards = [str(n) for n in range(2,11)] + list('JQKA')
print cards

专注语言特性而不是语法糖

上面谈到写地道的程序,学好编程语言中的“语法糖”,通常可以让我的代码更简化和“看上去”很地道,因此也有很多程序员非常热衷于此,犹如对于IDE的快捷方式的热衷,似乎这些是一个资深,高效程序员的标志。

什么是语法糖呢?

  • 语言中某种特殊的语法
  • 对语言的功能并没有影响
  • 对程序员有更好的易用性
  • 增加程序的可读性

一些语法糖的例子:

Python中union两个list

[1,2,3,4] + [5,6,7,8]

Go交换两个变量的值

a,b = b,a

上面由语法糖带来的地道我之所以用了“看上去”这个词来修饰,就是因为要想真正的写出地道的程序,比掌握语法糖更重要的是掌握语言的特性。

什么是语言的特性呢?

我们以Go和Java语言中并发机制特性为例:

1 基本并发单元

Go中采用独特的协程(Goroutine)作为基本并发单元,这一定会让Java程序员联想起线程(Thread),并不免在编写Go并发程序时引入很多编写多线程程序的思维,实际由于两者间存在着很多差异,直接以多线程的编程思想来编写多协程的程序有时是不适合的。

先让我们简单看看两者的差异:

go和java并发的差异

虽然,我们没有完全列出两者的差异,但是你也可以发现像Java程序中常见的线程池,在Go程序中很多情况下并不会带来像Java中那样的性能提升。

2. 并发机制,

Java通常采用共享内存机制来进行并发控制,而Go中则支持了CSP(Communicating Sequential Processes)机制。

CSP并发机制

下面通过典型的生产者和消费者并发任务来比较两种方式不同的特性:

共享内存方式(Java实现)

import java.util.LinkedList;
import java.util.Queue;
import org.junit.jupiter.api.Test;

class Producer implements Runnable {
  private Queue sharedData;
  public Producer(Queue sharedData) {
    this.sharedData = sharedData;

  }

  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      synchronized (this.sharedData) {
        try {

          while (this.sharedData.size() != 0) {
            this.sharedData.wait();
          }
          this.sharedData.add(i);
          System.out.printf("Put data %d \n", i);
          this.sharedData.notify();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

      }
    }

  }
}

class Consumer implements Runnable {
  private Queue sharedData;
  public Consumer(Queue sharedData) {
    this.sharedData = sharedData;
  }

  @Override
  public void run() {
    while (true) {
      synchronized (this.sharedData) {
        try {
          while (this.sharedData.size() == 0) {
            this.sharedData.wait();
          }
          System.out.println(this.sharedData.poll());
          if (this.sharedData.size() == 0) {
            this.sharedData.notify();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }

    }
  }
}

class ProducerConsumer {
  @Test
  void test() throws InterruptedException {
    Queue sharedData = new LinkedList<>();
    new Thread(new Producer(sharedData)).start();
    new Thread(new Consumer(sharedData)).start();
    Thread.sleep(2 * 1000);
  }
}

利用共享的Queue来实现生产者和消费者之间的数据传递,为了保证数据在多线程间同步,我们使用了锁。

CSP方式 (Go实现)

package Demo
import (
  "fmt"
  "testing"
  "time"
)

func Producer(ch chan int) {
  for i := 0; i < 100; i++ {
    fmt.Printf("put %d \n", i)
    ch <- i
  }
  close(ch)
}

func Consumer(ch chan int) {
  for {
    select {
    case i, ok := <-ch:
      if !ok {
        fmt.Println("done.")
        return
      }
      fmt.Println(i)

    }
  }
}

func TestFn(t *testing.T) {
  ch := make(chan int)
  go Producer(ch)
  go Consumer(ch)
  time.Sleep(time.Second * 3)
}

Go这是利用CSP机制中的channel在生产者和消费者之间传递数据。

好的代码风格不能代替好的设计

代码是软件实现的最终形式。但是对于更为复杂的软件而言,我们仅仅在代码层面整洁,复用及高可读性还是远远不够。

人们总是通过更高层次的抽象来实现简化。犹如,编程语言由机器语言,到汇编语言,再到高级语言不断演进,抽象层次不断提高。更高的抽象层次,能够更加有助于人们去了理解和构建更复杂的软件。

所以,在每个抽象层面,都要考虑简洁,复用和易理解。而且软件的设计过程及人们理解软件设计的过程也通常是自顶向下的。这就产生了指导人们做好高层抽象的设计模式,甚至更高抽象层面的架构模式。

作为合格程序员光学习代码的整洁之道是不够的,还有学习更高层面的整洁和复用。

下面我们还是用一个例子来说明

这里模拟我们要实现一个可以扩展使用不同支付方式的支付过程:

type PayChannel int

const (
  AliPay    PayChannel = 1
  WechatPay PayChannel = 2
)

func payWithAli(price float64) error {
  fmt.Printf("Pay with Alipay %f\n", price)
  return nil
}

func payWithWechat(price float64) error {
  fmt.Printf("Pay with Wechat %f\n", price)
  return nil
}

func PayWith(channel PayChannel, price float64) error {
  switch channel {
  case AliPay:
    return payWithAli(price)
  case WechatPay:
    return payWithWechat(price)
  default:
    return errors.New("not support the channel")
  }
}

上面的代码在编码风格上是整洁的,可读性也不错。但我们会发现每增加一种支付模式我们都要修改PayWith这个方法,在switch中加入一个对应的分支。

通过利用面向对象中的命令模式的设计思想,我们可以将代码优化为如下(当然这里是采用函数式编程来实现这个命令模式的思想的)

type PayFunc func(price float64) error

func payWithAli(price float64) error {
  fmt.Printf("Pay with Alipay %f\n", price)
  return nil
}

func payWithWechat(price float64) error {
  fmt.Printf("Pay with Wechat %f\n", price)
  return nil
}

func Pay(payMethod PayFunc, price float64) error {
  return payMethod(price)
}

现在可以看到,新增支付方式时,Pay方法完全不用做任何修改。

这里给大家推荐两本设计模式方面的经典书籍

设计模式推荐书籍

不要害怕遗忘和混淆

“学了也用不上,很快就忘记了”常常会成为很多程序员拒绝学习新的编程语言的借口。

遗忘和混淆都是正常的,人类记忆就是这样。

记忆遗忘曲线

这是遗忘曲线,在一开始的遗忘率是最高的。对于语言学习,我的经验是我们会很快忘记我们学到的特殊语法,留存下来会是我们对语言编程范式,编程特性的理解。所以,正如前面我们已经提到过的《黑客与画家》中观点:学习了Lisp,即使你在工作中极少使用到它,你也会成为一个更优秀的程序员。

如果你是按照我们前面所说的方式充分掌握每种语言的特性并了解编程范式,你所遗忘和混淆的更多的是语法。通过写上一,两天,甚至几小时的程序,你很快就会发现所有那些对于这种语言的技能就都回来了。这好比练过健身的人,一段时间不练,肌肉会有流失,但是与从来没有练过的人不同,他们通过训练,肌肉很快能够恢复原有的状态,就是所谓的肌肉记忆,我们的大脑记忆也是这样的。

所以,不要因为害怕遗忘和混淆就不去学习新的语言,他们不仅可以拓宽你的编程思路,一旦需要你便可以经过较短时间从回巅峰!

不要让遗忘成为你放弃学习的借口,让遗忘成为一种提炼。

“Stay hungry,stay foolish.” — Steve Jobs.