这一节跟代码的坏味道有点相似,都是对一些零散的代码写作中常常被用到或者被忽视的点,在看完这本书以后,我发现,哦!!原来周围的人对代码的注意点说的也不是对的,哈哈哈哈哈,或许他们正如上面的达克效应一样,他们也不知道自己不知道,那么这就很危险了。
看看下面的一些知识点,那些是连你也不知道的。
1.注释
- 作者信息,最后修改时间,SRP等注释不应该加到代码里,有时候觉得自己写了一个需求,洋洋自满,想把自己的信息啥的都注释到代码里,这是不对的。
- 过时、无关或者不正确的注释要及时删除
- 写的每一条注释,就要花时间写出最好的,字字斟酌,正确的语法和拼接,保持简洁。
- 注释的代码立马删除,AS会给我们记忆住,如果需要还可以被找到。
2.函数
- 过多的参数 (三个以上的参数应该避免,立马对象化)
- 布尔参数应该尽量避免
没有被使用的函数应该删除,这也是我们经常不敢删除的原因。
3.常见的问题(从中我确实认识到了自己代码中的一些错误)
1.一个源文件存在多种语言
(避免避免,我们的项目目前是从java向kotlin转化的过程中,不可否认,kotlin在某些方面确实不如java健壮与完善)
2. 不正确的边界行为
代码过程中我们应该尽可能思考全面,不要动不动就你以为,让别人对你产生傲慢与不踏实的印象,这也是你跟大多数程序员产生区别性的标志点
3. 忽视安全
工作后你会发现一个项目的安全与健壮性是最最最最重要的,因为这个跟你的收入直接挂钩,哈哈哈哈哈
4. 避免重复
这个确实挺真实的,我们就是要避免一个函数,一个常量,多次在以不同的名字出现在项目中,新加入项目的人第一是对于项目不熟悉,第二是老的常量,函数的命名不规范,根本找不到有这个函数。就导致了又重新封装了一个,长此以往,一个项目中重复的功能的代码就很大,在你有避免重复的意识后,每次封装功能性的函数,才会先去看看项目中是否有这种函数。但是由于实现方式跟命名的问题,重复还是不可避免。
本人最近在项目中有一个数据打点的需求,对于页面展示,这个key,整个项目中已经有3个不同名称常量来代表此页面,由于此数据产品是新来的,缺乏对以前的了解,他就会在自己的认知内,再次针对这个页面起一个新的打点key,那就是第四个数据的产生。显得整个项目很臃肿,相关的人员很业余,实在是令人抓马。所以作为实现者,我们要具有工程师思维,帮助他们去优化,避免重复,打造专业团队。
5. 在错误的抽象层级上的代码(这个名字有点绕)
要求就是分离位于不同层级的概念,将他们放到不同容器,通俗点讲就是归类,类似于MVC,Model,view,Controller,可以看下下面的例子:
我的理解就是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. 拒绝写鬼才看的懂的名称
12. 代码位置放置正确
创建常量都放到一个工具类里,为了图方便,全局可以调用到,这是很严重的错误。
14. 什么时候方法应该封装成静态?
工具函数
辅助函数
单例模式: 在单例模式中,静态方法通常用于获取类的唯一实例。
工厂方法: 静态方法可以用作工厂方法,用于创建类的实例
15. 什么时候方法应该封装成非静态?
函数(或者方法)是否应该封装成非静态的方法取决于其是否需要访问类的实例变量(成员变量)以及其行为是否取决于特定对象的状态。以下是一些情况下,考虑将函数封装成非静态方法:
public class Car {
private int speed;
public void accelerate(int amount) {
speed += amount;
}
}
16. 解释性变量的重要作用
假如写成下面的样子,那第二个人根本不知道你要干嘛。
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. 封装条件
我一个朋友的领导就是因为 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. 避免时序耦合
时序耦合这件事情,在大型项目比较常见,特别是一些线上项目,应该特别注意时序耦合,前几天就是因为个别同事,在调用某些方法时,从中截取了时机来调用方法,而方法里面的常量是由前面的时序初始化的,这就导致了大量线上崩溃。教训深刻,直接上书中例子。
不过这样就是在一些对象初始化的时候好一些,假如说里面初始化一些常量或者封装的很深,确实很难发现。对于这种直接调用别的类里的方法,或者是private方法,一定要慎重查看,该方法是否有时序问题。
27. 边界条件进行封装
虽然上面的只是一个小的封装,但是我们主要强调的是代码规范。
28. 函数应该只在一个抽象层级上
public void initAnimals(){
initflyAnimals();
initrunAnimals();
initdog();
initswimAnimals();
}
可以看到上面的函数初始化了天上飞的,地上跑的,水里游的动物,但是里面也初始化了dog,按照常识,dog的初始化应该在runAnimals动物里面初始化。
29. 在高层级放置通用变量
30. 避免传递浏览
4. Java相关
4.1 避免使用过长的导入清单
4.2 不要继承常量
4.3 常量和枚举
5. 名称
5.1 采用描述性名称
名称随着类的函数的增加,功能的更改,也要重新评估变量的名称
5.2 名称应该与抽象层级相符
public void initAnimals(){
initflyAnimals();
initrunAnimals();
initdog();
initswimAnimals();
}