무엇 때문에 코드를 읽을 수 없습니까

33753 단어 readability
"어떤 바보라도 컴퓨터가 이해할 수 있는 코드를 쓸 수 있다. 좋은 프로그래머는 인간이 이해할 수 있는 코드를 쓸 수 있다."
읽을 수 있는 코드는 우리의 뇌가 원본 코드를 어떻게 해석하고 성능에 어떤 영향을 미치는지에 관한 것이다.코드를 읽을 수 없는 주요 원인은 다음과 같습니다.
  • 너무 많거나 너무 길다: 뇌에 추적 변수가 필요할 때 논리선
  • 비국부 논리: 우리는 연속, 선형, 격리 논리를 더욱 좋아한다.논리가 국부적이지 않은 세 가지 원인이 있다
  • 인코딩 스타일: 전역 변수,simd intrinsics v.s.spmd 스타일 gpu 계산, 리셋 v.s. 협정
  • 범위화: 코드를 다시 사용하기 위해서는 여러 개의 실행 경로를
  • 에 묶어야 합니다
  • 비기능성 수요: 기능 논리
  • 과 시간과 공간에서 공존(소스 코드와 실행 시)
  • 에서 당신이 본 것은 당신이 얻은 것이 아니다. 우리는 코드가 실행될 때 어떻게 작동하는지 상상해야 한다. 코드는 원본 코드 형식과 매우 다르다.예를 들어 메타프로그래밍, 공유 메모리의 다중 스레드
  • 낯선 개념: 우리는 명칭과 인용을 사용하여 하나의 개념을 다른 개념으로 연결한다.링크가 튼튼하지 않을 때, 우리는 그것이 무의미하다고 말하는 번거로움이 생길 것이다
  • 우리 하나하나 이야기합시다.샘플 가져오기
  • Psychology of Code Readability
  • Why global variables are evil
  • Asynchronous JavaScript: From Callback Hell to Async and Await
  • 너무 많거나 너무 길다


    우리는 업무 기억에서 변수를 보존하는 능력에 한계가 있다.머리의 변화를 추적할 때마다 배로 증가하는 에너지를 소모한다.
    // more variable
    sum1 = v.x
    sum2 := sum1 + v.y
    sum3 := sum2 + v.z
    sum4 := sum3 + v.w
    
    // less variable
    sum = sum + v.x
    sum = sum + v.y
    sum = sum + v.z
    sum = sum + v.w
    
    모든 변수는 바뀔 수 있다.만약 sum1, sum2, sum3, sum4이 상량이라면 압력은 비교적 작다.
    // longer execution path to track
    public void SomeFunction(int age)
    {
        if (age >= 0) {
            // Do Something
        } else {
            System.out.println("invalid age");
        }
    }
    
    // shorter execution path to track
    public void SomeFunction(int age)
    {
        if (age < 0){
            System.out.println("invalid age");
            return;
        }
    
        // Do Something
    }
    
    초기 반환은 우리가 머리에서 추적해야 할 실행 경로를 감소시켰다.결론을 얻는 경로는 짧을수록 좋다.

    비국부 논리


    우리는 연속, 선형, 격리 논리를 더욱 좋아한다.무슨 말인지 설명해 드릴게요.
  • 연속: 두 번째 줄은 첫 번째 줄과 관련이 있어야 한다. 그것들을 함께 놓으면 긴밀한 인과관계를 나타낸다
  • 선형: 위에서 아래로 코드를 읽고 코드는 위에서 아래로
  • 고립: 당신이 관심을 가져야 할 곳은 단지 한 곳입니다
  • // continuous, linear, isolated
    private static boolean search(int[] x, int srchint) {
      for (int i = 0; i < x.length; i++)
         if (srchint == x[i])
            return true;
      return false;
    }
    
    논리적 국부성은 가장 흔히 볼 수 있는 문제로 주관적이다.네가 관심을 가지는 것은'로컬'이 너에게 무엇을 의미하는지 정의했다.재구성은 바로 재분해이다. 카드 세척 논리에 관한 것이다. 어떤 방식으로 그것을 재구성하여 읽을 수 있도록 하는 것이다.
    논리가 국부적이지 않은 데는 세 가지 이유가 있다.
  • 인코딩 스타일: 전역 변수,simd intrinsics v.s.spmd 스타일 gpu 계산, 리셋 v.s. 협정
  • 범위화: 코드를 다시 사용하기 위해서는 여러 개의 실행 경로를
  • 에 묶어야 합니다
  • 비기능성 수요: 시간과 공간이 공존하는 (원본 코드와 운행 시)
  • 비국부 논리: 인코딩 스타일


    전역 변수를 통해 통신하면 인과관계를 은식화하고 필요한 논리 부분을 다시 조합하기 어렵다.
    // declare global variable
    int g_mode;
    
    void doSomething()
    {
        g_mode = 2; // set the global g_mode variable to 2
    }
    
    int main()
    {
        g_mode = 1; // note: this sets the global g_mode variable to 1.  It does not declare a local g_mode variable!
    
        doSomething();
    
        // Programmer still expects g_mode to be 1
        // But doSomething changed it to 2!
    
        if (g_mode == 1)
            std::cout << "No threat detected.\n";
        else
            std::cout << "Launching nuclear missiles...\n";
    
        return 0;
    }
    
    같은 논리를 전역 변수를 사용하는 것에서 매개 변수를 통해 상하문을 현저하게 전달하는 것으로 바꾸는 것은 매우 간단하다.이것은 우리가 선택한 코드를 읽을 수 없게 하는 인코딩 스타일이다.
    두 번째 예는 SIMD 프로그래밍에 관한 것입니다.SIMD 집행기를 구동하기 위해 코드를 작성하려면 여러 개의 데이터 채널을 동시에 처리해야 합니다.%ymm0은 256bit 레지스터이며 데이터 채널 8개는 32비트에 사용됩니다.
    LBB0_3:
        vpaddd    %ymm5, %ymm1, %ymm8
        vblendvps %ymm7, %ymm8, %ymm1, %ymm1
        vmulps    %ymm0, %ymm3, %ymm7
        vblendvps %ymm6, %ymm7, %ymm3, %ymm3
        vpcmpeqd  %ymm4, %ymm1, %ymm8
        vmovaps   %ymm6, %ymm7
        vpandn    %ymm6, %ymm8, %ymm6
        vpand     %ymm2, %ymm6, %ymm8
        vmovmskps %ymm8, %eax
        testl     %eax, %eax
        jne       LBB0_3
    
    여러 데이터 채널에 동일한 작업을 적용하는 방법을 지정하는 것과 달리, 하나의 데이터 채널을 처리하기 위해 코드를 작성하는 것은 훨씬 간단합니다.
    float powi(float a, int b) {
        float r = 1;
        while (b--)
            r *= a;
        return r;
    }
    
    코드를 SPMD 스타일에서 SIMD 스타일로 컴파일하려면 https://ispc.github.io/ispc.html이 필요합니다. 이것은 등가입니다.
    세 번째 예는 리셋 vs.co 루틴에 관한 것이다
    const verifyUser = function(username, password, callback){
       dataBase.verifyUser(username, password, (error, userInfo) => {
           if (error) {
               callback(error)
           }else{
               dataBase.getRoles(username, (error, roles) => {
                   if (error){
                       callback(error)
                   }else {
                       dataBase.logAccess(username, (error) => {
                           if (error){
                               callback(error);
                           }else{
                               callback(null, userInfo, roles);
                           }
                       })
                   }
               })
           }
       })
    };
    
    ... 과 비교하다
    const verifyUser = async function(username, password){
       try {
           const userInfo = await dataBase.verifyUser(username, password);
           const rolesInfo = await dataBase.getRoles(userInfo);
           const logStatus = await dataBase.logAccess(userInfo);
           return userInfo;
       }catch (e){
           //handle errors as needed
       }
    };
    
    Co 루틴은 논리를 위에서 아래로 선형으로 만든다.리셋 스타일로 작성된 코드, 왼쪽에서 오른쪽으로.하지만 그것들은 등가이다.프로그래밍 언어가 우리가 이렇게 할 수 있도록 허락하는 것을 고려하여 우리는 코드를 더욱 읽을 수 있도록 인코딩 스타일을 선택할 수 있다.

    비국부 논리: 범용


    요약하자면, 너는 반드시 전문화해야 한다.10개의 제품을 지원하는 전용 코드가 필요하면 대부분의 공통 코드를 공유합니다.당신은 얼마나 자주 10개 제품의 논리를 함께 추리해야 합니까?일반적으로 당신은 특정한 제품 유형이 어떻게 작동하는지 생각할 것입니다.그러나 유니버설 코드를 읽을 때 다른 9가지 코드를 건너뛰어야 한다.줄넘기는 진정한 인지적 부담을 일으킬 수 있다.
    여기에 간단한 예가 하나 있다
    public double PrintBill()
    {
        double sum = 0;
        foreach (Drink i in drinks)
        {
            sum += i.price;
        }
        drinks.Clear();
        return sum;
    }
    
    우리는 PrintBill이 너무 유용하다고 생각한다. 우리는 그것을 다시 사용해야 한다.그러나 즐거운 시간에 대해서는 음료수를 할인해야 한다.코드
    interface BillingStrategy
    {
        double GetActPrice(double rawPrice);
    }
    
    // Normal billing strategy (unchanged price)
    class NormalStrategy : BillingStrategy
    {
        public double GetActPrice(Drink drink)
        {
            return drink.price;
        }
    }
    
    // Strategy for Happy hour (50% discount)
    class HappyHourStrategy : BillingStrategy
    {
        public double GetActPrice(Drink drink)
        {
            return drink.price * 0.5;
        }
    }
    
    public double PrintBill(BillingStrategy billingStrategy)
    {
        double sum = 0;
        foreach (Drink i in drinks)
        {
            sum += billingStrategy.GetActPrice(i);
        }
        drinks.Clear();
        return sum;
    }
    
    PrintBill을 즐거운 시간과 정상적인 시간으로 확대하기 위해서 우리는 반드시 비용 계산 전략에 전념해야 한다.통용 코드를 읽기 위해서, 그것은 틀림없이 원시 버전보다 읽을 수 없을 것이다.
    그 밖에 각종 상황을 지원하기 위해 코드는 한 장면만 겨냥할 수 없다.이것은 많은 변화점을 일으킬 것이다.어떤 경우, 변체는 빈틈을 메우는 빈 impl일 가능성이 높다.예를 들어, 만약 우리가 정상적인 시간의 추가 서비스 비용이 필요하다면.코드가 비슷해 보여요.
    interface BillingStrategy
    {
        double GetActPrice(double rawPrice);
    }
    
    // Normal billing strategy (unchanged price)
    class NormalStrategy : BillingStrategy
    {
        public double GetActPrice(Drink drink)
        {
            return drink.price;
        }
        public double GetExtraCharge()
        {
            return 1;
        }
    }
    
    // Strategy for Happy hour (50% discount)
    class HappyHourStrategy : BillingStrategy
    {
        public double GetActPrice(Drink drink)
        {
            return drink.price * 0.5;
        }
        public double GetExtraCharge()
        {
            return 0;
        }
    }
    
    public double PrintBill(BillingStrategy billingStrategy)
    {
        double sum = 0;
        foreach (Drink i in drinks)
        {
            sum += billingStrategy.GetActPrice(i);
        }
        sum += billingStrategy.GetExtraCharge();
        drinks.Clear();
        return sum;
    }
    
    만약 당신이 즐거운 시간의 논리를 견지한다면, sum += billingStrategy.GetExtraCharge();행은 당신과 전혀 무관하다.그러나 어쨌든 너는 그것을 읽어야 한다.
    "분기"코드를 작성할 수 있는 많은 방법도 있다.함수 재부팅, 클래스 템플릿, 대상 다태성, 함수 대상, 점프표와 일반if/else.왜 우리는 이렇게 많은 방식으로 간단한'만약'을 표현해야 합니까?이것은 터무니없는 것이다.

    비국부 논리: 비기능 수요


    기능성 코드와 비기능성 코드가 교차하여 코드를 인류가 해석하기 어렵다.원본 코드의 주요 목표는 모든 사물의 인과 사슬을 묘사하는 것이다.어떤 x가 발생하지만 y는 수시로 일어나지 않습니다. 우리는 그것을 버그로 간주합니다. 이것이 바로 제가 말한 인과사슬입니다.이 인과사슬을 물리 하드웨어에서 실행하기 위해서는 많은 세부 사항을 지정해야 한다.예:
  • 이 값은 창고 또는 더미
  • 에 분배됩니다
  • 매개 변수를 복사본 또는 바늘로
  • 전달
  • 은 우리가 디버깅
  • 에서 거슬러 올라갈 수 있도록 어떻게 실행을 기록합니까
  • 어떻게 단일 스레드, 다중 스레드, 다중 기계에서 여러 날 동안 "업무"절차를 실행하고 발송합니까
  • 다음은 오류 처리 예시이다
    err := json.Unmarshal(input, &gameScores)
    if ShouldLog(LevelTrace) {
      fmt.Println("json.Unmarshal", string(input))
    }
    metrics.Send("unmarshal.stats", err)
    if err != nil {
       fmt.Println("json.Unmarshal failed", err, string(input))
       return fmt.Errorf("failed to read game scores: %v", err.Error())
    }
    
    기능 코드는 json.Unmarshal(input, &gameScores)if err != nil return에 불과합니다.우리는 오류를 처리하기 위해 많은 비기능성 코드를 추가했다.이 코드들을 건너뛰는 것은 매우 중요하다.

    네가 본 것은 네가 얻은 것이 아니다


    코드와 원본 코드의 형식이 크게 다를 때, 우리는 코드가 실행될 때 어떻게 작동하는지 상상하지 않을 수 없다.예:
    public class DataRace extends Thread {
      private static volatile int count = 0; // shared memory
      public void run() {
        int x = count;
        count = x + 1;
      }
    
      public static void main(String args[]) {
        Thread t1 = new DataRace();
        Thread t2 = new DataRace();
        t1.start();
        t2.start();
      }
    }
    
    계수는 항상 x+1이 아닙니다. 다른 라인이 같은 작업을 병행하기 때문에, x+1으로 x+1을 덮어쓸 수 있습니다.
    원 프로그래밍도 큰 상상력을 필요로 한다.함수 호출과 달리 호출된 함수로 이동해서 구축하고 있는 내용을 검사할 수 없습니다.예:
    int main() {
        for(int i = 0; i < 10; i++) {
            char *echo = (char*)malloc(6 * sizeof(char));
            sprintf(echo, "echo %d", i);
            system(echo);
        }
        return 0;
    }
    
    코드는 동적으로 생성된 것이다.생성된 원본 코드의 정확성을 추정하기 위해 간단한 방법이 없습니다.

    익숙하지 않은 개념


    우리는 명칭과 인용을 사용하여 하나의 개념을 다른 개념으로 연결한다.링크가 튼튼하지 않을 때, 우리는 번거로움을 만나, "의미가 없다"고 말할 것이다

    우리는 왼쪽의 경기가 오른쪽의 경기보다 훨씬 쉽다고 생각한다.왜?ladderfire은 현실에 비치는 익숙한 개념이기 때문이다.우리는 기존의 경험을 바탕으로 새로운 경험을 세운다.코드와 수요가 분리되고 현실 생활의 개념과 분리된다면 이해하기 어려울 것이다.
    func getThem(theList [][]int) [][]int {
        var list1 [][]int
        for _, x := range theList {
            if x[0] == 4 {
                list1 = append(list1, x)
            }
        }
        return list1
    }
    
    가독성
    func getFlaggedCells(gameBoard []Cell) []Cell {
        var flaggedCells []Cell
        for _, cell := range gameBoard {
            if cell.isFlagged() {
                flaggedCells = append(flaggedCells, cell)
            }
        }
        return flaggedCells
    }
    
    type Cell []int
    
    func (cell Cell) isFlagged() bool {
        return cell[0] == 4
    }
    
    [][]int은 낯설지만 []Cell은 생활 체험에 반영되기 때문이다.

    "이름"을 통해 개념을 코드에서 생활 체험으로 연결합니다. 이것은 단지 문자열일 뿐입니다.코드 모듈 간의 링크 개념은 "참조"에 의존할 수 있습니다.함수를 정의하고 인용합니다.유형을 정의하고 유형을 참조합니다.IDE에서 ref 를 클릭하여 정의로 이동할 수 있습니다.링크가 강력하고 뚜렷할 때, 우리는 코드를 더 빨리 읽을 수 있다.
    개념은 분해에서 나온다.매번 우리가 분해할 때마다 우리는 그것의 이름을 지어준다.분해는 세 가지가 있다.
  • 공간분해
  • 시간 분해
  • 층분해
  • 우리가 문제가 매우 크다고 느낄 때, 우리는 항상 이 세 가지 방법으로 그것을 분해할 수 있으며, 분해의 개념을 정확하게 명명하기만 하면 된다.

    좋은 웹페이지 즐겨찾기