代码整洁之道 之:味道与启发

144 阅读13分钟

image.png

这一节跟代码的坏味道有点相似,都是对一些零散的代码写作中常常被用到或者被忽视的点,在看完这本书以后,我发现,哦!!原来周围的人对代码的注意点说的也不是对的,哈哈哈哈哈,或许他们正如上面的达克效应一样,他们也不知道自己不知道,那么这就很危险了。

看看下面的一些知识点,那些是连你也不知道的。

1.注释

  1. 作者信息,最后修改时间,SRP等注释不应该加到代码里,有时候觉得自己写了一个需求,洋洋自满,想把自己的信息啥的都注释到代码里,这是不对的。
  2. 过时、无关或者不正确的注释要及时删除
  3. 写的每一条注释,就要花时间写出最好的,字字斟酌,正确的语法和拼接,保持简洁。
  4. 注释的代码立马删除,AS会给我们记忆住,如果需要还可以被找到。

2.函数

  1. 过多的参数 (三个以上的参数应该避免,立马对象化)
  2. 布尔参数应该尽量避免
  3. 没有被使用的函数应该删除,这也是我们经常不敢删除的原因。

3.常见的问题(从中我确实认识到了自己代码中的一些错误)

1.一个源文件存在多种语言

(避免避免,我们的项目目前是从java向kotlin转化的过程中,不可否认,kotlin在某些方面确实不如java健壮与完善)

2. 不正确的边界行为

代码过程中我们应该尽可能思考全面,不要动不动就你以为,让别人对你产生傲慢与不踏实的印象,这也是你跟大多数程序员产生区别性的标志点

3. 忽视安全

工作后你会发现一个项目的安全与健壮性是最最最最重要的,因为这个跟你的收入直接挂钩,哈哈哈哈哈

4. 避免重复

这个确实挺真实的,我们就是要避免一个函数,一个常量,多次在以不同的名字出现在项目中,新加入项目的人第一是对于项目不熟悉,第二是老的常量,函数的命名不规范,根本找不到有这个函数。就导致了又重新封装了一个,长此以往,一个项目中重复的功能的代码就很大,在你有避免重复的意识后,每次封装功能性的函数,才会先去看看项目中是否有这种函数。但是由于实现方式跟命名的问题,重复还是不可避免。

本人最近在项目中有一个数据打点的需求,对于页面展示,这个key,整个项目中已经有3个不同名称常量来代表此页面,由于此数据产品是新来的,缺乏对以前的了解,他就会在自己的认知内,再次针对这个页面起一个新的打点key,那就是第四个数据的产生。显得整个项目很臃肿,相关的人员很业余,实在是令人抓马。所以作为实现者,我们要具有工程师思维,帮助他们去优化,避免重复,打造专业团队。

5. 在错误的抽象层级上的代码(这个名字有点绕)

要求就是分离位于不同层级的概念,将他们放到不同容器,通俗点讲就是归类,类似于MVC,Model,view,Controller,可以看下下面的例子:

image.png 我的理解就是percentFull()位于错误的抽象层级。

6. 基类依赖派生类()

基类(父类或超类)

  • 基类是一个通用的类,其中包含一组通用的属性和方法。
  • 它定义了一些通用的特性,而不涉及特定的实例。
  • 基类提供了一种模板或蓝图,用于创建派生类。

派生类(子类或子类)

  • 派生类是从基类继承的类,它可以具有基类的属性和方法,并且可以扩展或修改这些属性和方法。
  • 派生类可以引入新的属性和方法,或者覆盖基类的方法,以提供特定于派生类的实现。
  • 派生类可以有多层继承,即一个派生类可以成为另一个派生类的基类。

这应该就是要说明基类和子类的区别,要求我们在设计以及代码抽象过程中,理清楚基类和子类的关系。

7. 信息过多

如果耦合度高,那就造成你每次基础一个基类,都要实现大量的你必须调用的接口,特别臃肿。 隐藏数据,

public class MyClass {
    private int data;

    public void setData(int newData) {
        // 可以在这里进行一些验证或处理
        this.data = newData;
    }

    public int getData() {
        return this.data;
    }
}

隐藏工具函数

public class MyClass {
    private int data;

    public void doSomething() {
        // 只在类内部使用的工具函数
        int result = helperFunction();
        // 其他操作
    }

    private int helperFunction() {
        // 只在类内部使用的工具函数的具体实现
        return 42;
    }
}

隐藏临时变量, 不要太多的方法或者实体变量的类。

8. 及时删除没有用的代码

没有用的代码就像是家里落得灰尘,一直不打扫,时间久了,给人一种肮脏感。删除没有用的代码,注释,函数,在本书中多次提到,足以引起重视。

9. 垂直分割

就是将就代码中,常量与常量,常量与函数,函数与函数,以及函数内部应该格式优美,留有间距。


public class Calculator {

    // Constructors

    public Calculator() {
        // Constructor logic here
    }

    // Public methods

    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    // Private methods

    private void validateInput(int a, int b) {
        // Validation logic here
    }

    private int performOperation(int a, int b, Operation operation) {
        // Operation logic here
        return 0;
    }
}

10. 特性依恋

指的是某个代码片段过度关注或依赖于一个特定的特性、类、模块或库。这可能导致代码的紧密耦合,使得代码难以理解、修改和测试。

// 特性依恋的例子
public class OrderProcessor {
    public void processOrder(Order order) {
        // 过度关注于特定的支付实现
        PaymentProcessor paymentProcessor = new PayPalPaymentProcessor();
        paymentProcessor.processPayment(order);
    }
}

一个类过度依赖于另一个类的具体实现,而不是依赖于接口或抽象类。这使得一个类的修改可能会导致另一个类的修改,增加了代码的脆弱性。

// 改进后的例子
public class OrderProcessor {
    private PaymentProcessor paymentProcessor;

    public OrderProcessor(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void processOrder(Order order) {
        paymentProcessor.processPayment(order);
    }
}

上面的例子最重要的区别是PayPalPaymentProcessor这个对象在OrderProcessor 里面进行初始化,假如,PaymentProcessor的构造函数发生改变或者增加参数,那么OrderProcessor类也需要进行修改,增加了2个类之间的耦合。

11. 拒绝写鬼才看的懂的名称

image.png

12. 代码位置放置正确

创建常量都放到一个工具类里,为了图方便,全局可以调用到,这是很严重的错误。

14. 什么时候方法应该封装成静态

工具函数

辅助函数

单例模式: 在单例模式中,静态方法通常用于获取类的唯一实例。

工厂方法: 静态方法可以用作工厂方法,用于创建类的实例

15. 什么时候方法应该封装成非静态

函数(或者方法)是否应该封装成非静态的方法取决于其是否需要访问类的实例变量(成员变量)以及其行为是否取决于特定对象的状态。以下是一些情况下,考虑将函数封装成非静态方法:

public class Car {
    private int speed;

    public void accelerate(int amount) {
        speed += amount;
    }
}

16. 解释性变量的重要作用

image.png

假如写成下面的样子,那第二个人根本不知道你要干嘛。

String v1= match.group(1);
String v2= match.group(2);

17,逻辑依赖改成物理依赖(减少物理依赖)

逻辑依赖(Logical Dependency):

  • 定义: 逻辑依赖表示代码模块之间的关系,即一个模块的行为是否依赖于另一个模块。这与代码中的调用关系和数据流有关。

  • 示例: 如果模块 A 中的函数调用了模块 B 中的函数,那么 A 逻辑上依赖于 B。这种依赖关系是通过函数调用、数据传递等方式实现的。

物理依赖(Physical Dependency):

  • 定义: 物理依赖表示代码文件之间的关系,即一个文件的更改是否会影响到另一个文件。这与代码文件之间的引用关系有关。
  • 示例: 如果模块 A 的实现中导入了模块 B 的类,那么 A 物理上依赖于 B。这种依赖关系是通过文件的引入、包的依赖等方式实现的。
18. 尽量使用多态替代if/else或者Switch/Cse

使用多态替代switch/case语句的建议主要是出于以下几个原因:

a. 可维护性(Maintainability):

  • switch/case 语句通常随着条件的增多而变得冗长。当你需要添加新的条件时,必须修改 switch 语句的代码,容易导致错误并且不易维护。
  • 使用多态,尤其是通过面向对象的方式,可以通过添加新的子类来扩展系统,而无需修改现有的代码。每个子类负责处理自己的逻辑,使代码更加模块化和易于维护。

b. 可扩展性(Extensibility):

  • 多态性允许你以更灵活的方式扩展系统,而不是通过修改一个巨大的 switch 语句。这使得系统更容易适应新的需求或业务规则。
  • 新的功能可以通过添加新的子类和覆盖现有方法来实现,而无需改动已经存在的代码。

c. 松耦合(Loose Coupling):

  • 使用多态性可以将客户端代码与特定的实现细节解耦。客户端只需要知道基类或接口,而不需要知道具体的子类。
  • switch/case 语句通常将所有逻辑放在一个地方,导致高耦合。多态性允许将逻辑分布在不同的类中,降低了模块之间的依赖关系。

d. 易读性和清晰性(Readability and Clarity):

  • 多态性提供了一种更自然、更面向对象的方法来表示条件逻辑。通过使用多态,代码可以更直观地表达不同类型的行为。
  • switch/case 语句可能需要花费更多的心智努力来理解,特别是当它变得很长时。

e. 遵循设计原则(Following Design Principles):

  • 使用多态性有助于遵循面向对象设计的一些基本原则,如开放/封闭原则(对扩展开放,对修改封闭)和单一职责原则(每个类负责一项职责)。

说那么多,不如来个例子。

public class AnimalSoundPlayer {
    public void playSound(String animalType) {
        switch (animalType) {
            case "dog":
                System.out.println("Dog barks");
                break;
            case "cat":
                System.out.println("Cat meows");
                break;
            case "duck":
                System.out.println("Duck quacks");
                break;
            default:
                System.out.println("Unknown animal");
        }
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        AnimalSoundPlayer player = new AnimalSoundPlayer();

        player.playSound("dog");   // 输出:Dog barks
        player.playSound("cat");   // 输出:Cat meows
        player.playSound("duck");  // 输出:Duck quacks
        player.playSound("lion");  // 输出:Unknown animal
    }
}

使用 switch/case 的问题在于,当你添加新的动物类型时,你必须修改 AnimalSoundPlayer 类的代码。这样的代码不够灵活,容易产生错误,并且不符合面向对象编程的开闭原则(对扩展开放,对修改关闭)。而使用多态性,你只需要添加新的类实现 Animal 接口,而无需修改播放器的代码。这样,你就可以更轻松地扩展系统,而不破坏现有的代码。

// Animal 接口
interface Animal {
    void makeSound();
}

// 具体的动物类
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

class Duck implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Duck quacks");
    }
}

// AnimalSoundPlayer 类,负责播放动物的声音
class AnimalSoundPlayer {
    public void playSound(Animal animal) {
        animal.makeSound();
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        AnimalSoundPlayer player = new AnimalSoundPlayer();

        Animal dog = new Dog();
        Animal cat = new Cat();
        Animal duck = new Duck();

        player.playSound(dog);   // 输出:Dog barks
        player.playSound(cat);   // 输出:Cat meows
        player.playSound(duck);  // 输出:Duck quacks
    }
}
19. 遵循项目内通用约定
20. 用常量代替硬编码
public class MathUtils {
    
    public static int add(int a) {
        return a + 24; 
    } 
}
public class MathUtils {

    //常量要全部大写,下划线分割
    Private final int HOUR_OF_DAY = "24";
    
    public static int add24Hour(int a) {
        return a + HOUR_OF_DAY; 
    } 
}
21. 准确
22. 结构甚于约定

用到命名良好的枚举,其扩展性比抽象方法要弱很多;没有团队会强烈要求命名必须枚举,即使有,你也试图去询问为什么不可以抽象类来替代,来说服自己。这也是程序员的一个必要品质。

23. 封装条件

image.png

我一个朋友的领导就是因为 C9高校应届生 多次把if语句里面的判断条件写的十分长,在多次review代码,还是没有改正后,被开掉了。 这不仅仅是代码规范的问题,也是态度的问题,当然,真实性有待考证,哈哈哈哈哈。

24. 避免否定式条件(很多人都会犯)

肯定式要比否定式好理解一些,所以尽可能将条件表达为肯定式。

private static String getTopicId() { 

    String region = RegionUtils.getRegion(); 
    
    //此处命名多次反转,表达不直接
    if (!LogUtils.isnotDebug()) { 
        LogUtils.d("caoshuchen", "isdebug : " + LogUtils.isDebug()); 
        return TRACK_TOPIC_TEST; 
    } 
    
    if (!TextUtils.isEmpty(region)) { 
        return TRACK_TOPIC; 
    } 
    
    LogUtils.d(TAG, "getTopic error : " + region); 
    
    return TRACK_TOPIC_TEST; 
}
25. 方法只该做一件事

我写方法老喜欢把代码都堆到一个方法里,这是省力气且不需要思考的,但是面临的后果就是代码很臃肿并且不易读,具体例子请看下面。

public void pay() {
    for(Employee e : employee) {
        if(e.isPay()){
            Money pay = e.calculatePay();
            e.deliverPay(pay);
        }
    }
}

这个代码做了三件事, 遍历员工,检查是否已经支付工资,支付工资。优化后如下:

public void pay(){
    for(Employee e : employee) {
        payIfNecessary(e);
    }
}

public void payIfNecessary(Employee e){
    if(e.isPay()){
       calculateAndDeliverPay(e);
     }
}

public void calculateAndDeliverPay(Employee e){
    Money pay = e.calculatePay();
    e.deliverPay(pay);
}

26. 避免时序耦合

时序耦合这件事情,在大型项目比较常见,特别是一些线上项目,应该特别注意时序耦合,前几天就是因为个别同事,在调用某些方法时,从中截取了时机来调用方法,而方法里面的常量是由前面的时序初始化的,这就导致了大量线上崩溃。教训深刻,直接上书中例子。

image.png

image.png

不过这样就是在一些对象初始化的时候好一些,假如说里面初始化一些常量或者封装的很深,确实很难发现。对于这种直接调用别的类里的方法,或者是private方法,一定要慎重查看,该方法是否有时序问题。

27. 边界条件进行封装

image.png

image.png

虽然上面的只是一个小的封装,但是我们主要强调的是代码规范。

28. 函数应该只在一个抽象层级上
    public void initAnimals(){
        initflyAnimals();
        initrunAnimals();
        initdog();
        initswimAnimals();
    }

可以看到上面的函数初始化了天上飞的,地上跑的,水里游的动物,但是里面也初始化了dog,按照常识,dog的初始化应该在runAnimals动物里面初始化。

29. 在高层级放置通用变量

image.png

30. 避免传递浏览

image.png

4. Java相关

4.1 避免使用过长的导入清单
4.2 不要继承常量
4.3 常量和枚举

image.png

5. 名称

5.1 采用描述性名称

名称随着类的函数的增加,功能的更改,也要重新评估变量的名称

image.png

5.2 名称应该与抽象层级相符
    public void initAnimals(){
        initflyAnimals();
        initrunAnimals();
        initdog();
        initswimAnimals();
    }

image.png

5.3 尽可能使用标准命名法 &&无歧义的命名

image.png

5.4 为较大的作用范围选用较长的名称
5.5 避免编码
5.5 名称应该说明副作用

image.png