• 【笔记】Clean Code(持续更新)


    这个暑假出来实习,第一次体会到在一个团队中开发的体验,与网上的网站看到的大为不同,以前看网上说什么程序员写了屎山代码,写了一堆模糊的注释或者说垃圾代码不写注释。

    但在我的实习体验中,代码虽然看起来很多,但大多都一目了然,第一天去的时候我问了我的实习导师,为什么公司代码没有注释?他说公司的编码全部都是按照代码整洁之道,只写整洁易懂的代码,让人一目了然。我在看了几天代码之后就很快就明白了部分代码含义,并且完成了几项任务。简洁的代码确实会让人如沐春风,公司的三本必读书籍之一就是《Clean Code》。

    如果说你想写出优秀的程序,一定要去看《Clean Code》这本书!

    1.有意义的命名

    1.1 名副其实

    变量选择要体现本意的名称,能让人更容易理解和修改代码。

    例如:

    int d ; //消逝的时间,以日计
    
    int elapsedTimeInDays;
    int daysSinceCreation;
    int fileAgeInDays;
    

    好的命名习惯可以让代码块变得易读,无关于简洁度,而是可以减少代码的模糊度。

    比如下面这段代码。

    public List<int []> getThem(){
        List<int []> list1 = new ArrayList<>();
        for(int[] x :  theList){
            if (x[0] == 4){
                list1.add(x);
            }
        }
        return list1 ;
    }
    

    在看这段代码时有很多问题不理解,如theList是什么类型的东西,theList下标0条目的意义是什么?值4的意义是什么?我怎么使用返回列表?

    假设这是一款扫雷游戏,盘面是theList的单元格列表,那就将其名称改为gameBoard。盘面上每个单元格都用一个简单数组表示,0下标条目是状态栏,而状态值4代表为“已标记”。现在我们可以把代码改为有意义的名称。

    public List<int []> getFlaggedCells(){
        List<int []> flaggedCells = new ArrayList<int []>();
        for(int[] cell : gameboard){
            if (cell[STATUS_VALUE] == FLAGGED){
                flaggedCells.add(cell);
            }
        }
        return flaggedCells;
    }
    

    也可以把cell封装为一个类,在判断FLAGGED时,调用判断方法来掩盖这个数字。

    1.2 避免误导

    我们必须避免留下掩盖代码本意的错误线索,避免使用与本意相悖的词。比如accountList来指称一组账号,除非它真的是List类型。如果说他包含的一组账号并不是List类型,就会引起错误的判断。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。

    另外需要提防使用不同之处较小的名称,如XYZControllerForEfficientHandlingOfStrings和另一处的XYZControllerForEfficientStorageOfStrings,辨别需要花费多少时间呢?

    1.3 做有意义的区分

    对于一些变量,只是在其后面添加数字用以区分并不规范,只是滥竽充数,这只能让编译器满意,以数字系列命名(a1,a2,a3,a4...)是依义命名的对立面,完全没有提供正确的信息:没有提供导向作者意图的线索。

    public static void copyChars(char a1[],char a2[]){
        for(int i=0; i<a1.length; i++){
            a2[i] = a1[i] ;
        }
    }
    

    如果参数名改为sourcedestination,这个函数就会像样很多。

    1.4 使用读得出的名称

    如果你定义的名称是一串你读出不来的字符,无疑在你阅读时会增加你大脑的负荷。

    比如说本书作者曾经遇到的,一家公司在程序里面写了个genymdhms(生成日期、年、月、日、时、分、秒),他们读作gen why emm dee aich emm es,而作者却照读为gen-yah-mudda-hims,后来设计师和分析师也这样读。当我们给其他人解释这个名称时。他们总是读作自己的自造词。

    1.5 使用可搜索的名称

    当你使用单字母名称和数字常量时,就很难从一大篇文字中找到他。

    但你找MAX_CLASSES_PER_STUDENT就很容易了,但数字7就很难找到了。

    若变量或常量可能在代码中多处使用,则应赋其值便于搜索的名称。

    比较

    for(int j=0;j<34;j++){
        s+=(t[j]*4)/5;
    }
    

    int realDaysPerIdealDay = 4; 
    const int WORK_DAYS_PER_WEEK =5;
    int sum = 0;
    for(int j=0;j<NUMBER_OF_TASKS;j++){
        int realTaskDays = taskEstimate[j] * realDaysPerIdealDay ;
        int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
        sum += realTaskWeeks;
    }
    

    上述代码中,sum并非有特别用意的名称,但他可以搜索到。采用能表达意图的名称虽然拉长了函数代码,但WORK_DAYS_PER_WEEK确实比数字5要好找。

    1.6 避免使用编码

    编码已经太多了,不需要把作用域和类型编码进名称中,这徒然增加了解码的负担,所以不要使用变量名增加前缀的方式,如m_这种前缀。

    public class Part{
        private String m_dsc ;
        public Part(String name){
            m_dsc = name ;
        }
    }
    
    public class Part{
        String description;
        public Part(String descripation){
            this.description = description ;
        }
    }
    

    学会无前缀的方式,人们就不用去看到没有意义的废料前缀了。

    1.7 避免思维映射

    不要让读者在脑中把你的名称翻译为他们熟知的名称,这种问题经常出现在是选择使用问题领域术语还是解决方案领域术语时。

    单字母变量就是这个问题,在没有冲突的情况下,循环计数器可能会命名为i或者j,这种情况下,k这个变量可能就会在读者脑中有其他的意义,它是不是也是用于计数的?

    明确才是王道。

    1.8 类名

    类名和对象名应该是名词或名词短语,如CustomerWikiPageAccountAddressParser,避免使用ManagerProcesserDataInfo这样的类名。类名不应该是动词。

    1.9 方法名

    方法名应该是动词或动词短语。如postPaymentdeletePagesave。属性访问器、修改器和断言应该根据其值命名,并依据JavaBean标准加上getsetis前缀。

    String name = employee.getName();
    Customer.setName("mike");
    if(paycheck.isPosted())...
    

    重载构造器时,使用描述了参数的静态工厂方法名。如

    Complex fulcrumPoint = Complex.FromRealNumber(23.0);
    

    通常好于

    Complex fulcrumPoint = new Complex(23.0);
    

    可以考虑将构造器设置为private,强制使用这种命名手段。

    1.10 别扮可爱

    命名不要太皮,不要耍大宝,如HolyHandGrenade(神圣手雷)这个函数是干什么的谁知道呢,不如使用DeleteItems(删除条目)这个名称。还有如whack(劈砍)这种名称,不如使用kill(杀死)

    1.11 每个概念对应一个词

    给每一个抽象概念选择一个词,比如在一堆代码中有Controller,又有Manager,还有Driver,就会让人很困惑,DeviceManager和Protocol-Controller有什么区别? 为什么不全用Controllers或者Managers?

    1.12 别用双关语

    避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关了,比如add方法,许多类中都有add方法,该方法用来增加或连接现存值来获得新值。但对于把单个参数放到集群中,这个方法叫做add吗?这样虽然看似和其他add保持一致了,但语义却是不同,应该使用insert或append之类的词来命名才对。

    2. 函数

    2.1 短小,只做一件事

    让人明了的函数应该简短,函数体只做一件事,并且通过函数名称就可以看出所做。比如以丑长闻名的Swing程序,把它写成每个函数只有3-4行无疑可以让每一步都一目了然。

    应该如何短小。应该例如下面的例子:

    public static String readerPageWithSetupsAndTeardowns(PageData pageData,boolean isSuite) throws Exception{
        if(isTestPage(pageData)){
            includeSetupAndTeardownPages(pageData,isSuite);
        }
        return pageData.getHtml();
    }
    

    函数应该只做一件事。做好这件事,只做这件事。

    2.2 每个函数一个抽象层级

    每个函数一个抽象层级的目的就是为了确保一个函数只做一件事。

    这一条可以理解为不同抽象集的函数一层层连接,每一级只去负责自己的任务。

    2.3 switch语句

    switch天生要做N件事,而且我们很难写出简短的switch语句。但是我们还是可以利用多态将每个switch都放在较低的抽象层级,而且永不重复。

    switch (e.type){
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        ...
    }
    

    这种写法太长了,而且出现新的雇员在添加时会变得更长,其次它做了不止一件事,违反了单一权责原则,对雇员添加又违反了开放封闭原则,而且最麻烦的是可能到处都有类似结构的函数。

    该问题的解决方法是将switch埋到抽象工厂底下,不被任何人看到,工厂使用Employee的派生物创建适当的实体,而不同的函数如calculatePay、isPayday和deliverPay等,则由Employee接口多态地接收。

    public abstract class Employee{
        public abstract boolean isPayday();
        public abstract Money calculatePay();
        public abstract void deliverPay(Money pay);
    }
    
    public interface EmployeeFactory{
        public Employee makeEmployee(EmployeeRecord r) throw InvalidEmployeeType;
    }
    
    public class EmployeeFactoryImpl implements EmployeeFactory{
        public Employee makeEmployee(EmployeeRecord r){
            switch(r.type){
                case COMMISSIONED:
                    return new CommissionedEmployee(r);
                case :
                    return new CommissionedEmployee(r);
            }
        }
    }
    
    2.4 函数参数

    最理想的参数数量是零,其次是一,再其次是二,应当避免三参数函数。

    参数是阅读函数时最关键的部分,所以应当做到精简!

  • 相关阅读:
    关于程序与语言
    最新笔记请查看
    MySQL 性能优化
    k8s flannel无法跨主机ping通pod的解决方案
    k8s 使用kubeadm部署k8s集群初体验
    MySQL 锁和可重复读的隔离级别结合起来的一个示例(来自MySQL45讲第8章)
    MySQL 可重复读 vs 读提交
    Jenkins配置Linux节点,通过ssh方式在Linux节点自动拉取github代码并执行
    AppScan 使用
    Linux 动态链接库和静态库示例
  • 原文地址:https://www.cnblogs.com/LexMoon/p/cleancode.html
Copyright © 2020-2023  润新知