Skip to content

代码风格

  • 以人为本

前面我也说过编码不仅仅是为了让计算机理解,更多的时候是为了让人能读懂,所以代码风格的目的确实更多是为了美观。但也不仅是为了美观,更重要的是提升可读性、维护性和一致性。良好的风格能使团队内部人员协作时更快理解代码,减少错误,提高协作效率。

然而,我们依然要保持代码的美观性,因为它虽然不会直接影响业务运行,但对程序员而言,它是代码质量的体现,也是对工匠精神的一种追求。

注释规范

类注释

类的注释应该采用标准的javadoc文档注释,并简洁明了地描述类的作用。尤其是当类的功能较为复杂时,还要描述出与其他类的关系。甚至必要时给出示例代码。

基本示例:

java
/**
 * 解释类的作用
 */
public class 类名 { }

/**
 * 解释类的作用
 * 
 * @see 与某类的关系
 */
public class 类名 { }

/**
 * 解释类的作用
 * 
 * 示例代码
 * 
 * @link 介绍某个方法
 */
public class 类名 { }

每个人类注释的风格可能会略有不同,但是核心观念必须一致。我们始终要清楚类注释的作用是什么?是为了简单明了的介绍该类的作用等信息,方便他人去看的时候能一眼读懂。

分享我个人的一些类注释风格:

1.给出设计理由

kotlin
/**
 * @author 开发者姓名
 * @date 2024/9/30 14:27
 * @description Activity基类,鉴于handler在日常开发中使用频率较高,因此在基类中封装了handler的使用
 */
abstract class BaseActivity : AppCompatActivity(), ThreadHandlerAction {

    /**
     * 在工作线程上进行处理的Handler
     */
    private var mThreadHandler: ThreadHandler? = null
    
    // ...
}

/**
 * @author 开发者姓名
 * @date 2024/9/24 16:22
 * @description 模仿Kotlin协程里的 Flow 的数据收集接口
 * @see kotlinx.coroutines.flow.Flow
 */
public interface FlowCollect<T> {
    
    /**
     * 收集数据
     * 
     * @param emitter 数据发射器
     */
    void collect(FlowEmitter<T> emitter);
}

2.简单介绍作用

kotlin
/**
 * @author 开发者姓名
 * @date 2023/4/3
 * @description 服务器超时异常
 */
public final class TimeoutException extends HttpException { }

/**
 * @author 开发者姓名
 * @date 2024/10/8 14:52
 * @description 依赖注入 dependency injection
 *
 * @see dagger.Module 标记这个类是一个 Dagger 模块,负责提供依赖。
 * @see dagger.hilt.InstallIn(SingletonComponent::class) 指定将该模块安装到 SingletonComponent,意味着提供的依赖会在应用生命周期内是单例的。
 * @see SingletonComponent 表示依赖注入的生命周期是应用的整个生命周期。
 * @see javax.inject.Singleton 表示该依赖的实例是单例的,应用中只会有一个实例。
 * @see dagger.Provides 告诉 Dagger 如何提供某个依赖的实例。
 */
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
    
    @Singleton
    @Provides
    fun provideMainRepository(): MainRepository {
        return MainRepositoryImpl()
    }
}

3.介绍有哪些方法功能

kotlin
/**
 * @author 开发者姓名
 * @date 2019-8-6
 * @description 文件工具类
 * 写入数据 {@link #writeFileData(String filename, String content, boolean isCover)}
 * 读取文件内容 {@link #readFileData(String fileName)}
 * 修改文件名称 {@link #reFileName(String sourcePath, String goalPath)}
 * 复制文件到指定目录下 {@link #copyFile(String, String)}
 * 将InputStream写入本地文件 {@link #writeToLocal(String, InputStream)}
 * 删除文件 {@link #delFile(String fileName)}
 * 删除指定文件,如果是文件夹,则递归删除 {@link #deleteFileOrDirectory(File)}
 * 得到文件夹大小 {@link #getDirectoryFormatSize(String)} 得到byte,kb,mb,gb
 * 得到文件大小 {@link #getFileFormatSize(String)}} 得到byte,kb,mb,gb
 * 获取指定文件夹的大小 {@link #getFileSizes(String)}
 * 获取指定文件的可读大小 {@link #getFileAvailable(String)}
 * 读取文件中的每一行内容到集合中去 {@link #readLineToList(String)}
 * 获取某个路径下,某个文件类型的文件集合 {@link #getFileList(String, String)}
 * 获取文件扩展名 {@link #getExtensionName(String)}
 */
public class FileUtil { }

4.添加必要的示例代码

这个是官方的Jetpack库里的ViewModel类 介绍的非常详细,还有代码示例。

java
/**
 * ViewModel is a class that is responsible...省略
 * 
 * <p>
 * Typical usage from an Activity standpoint would be:
 * <pre>
 * public class UserActivity extends Activity {
 *
 *     {@literal @}Override
 *     protected void onCreate(Bundle savedInstanceState) {
 *         super.onCreate(savedInstanceState);
 *         setContentView(R.layout.user_activity_layout);
 *         final UserModel viewModel = new ViewModelProvider(this).get(UserModel.class);
 *         viewModel.getUser().observe(this, new Observer&lt;User&gt;() {
 *             {@literal @}Override
 *             public void onChanged(@Nullable User data) {
 *                 // update ui.
 *             }
 *         });
 *         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
 *             {@literal @}Override
 *             public void onClick(View v) {
 *                  viewModel.doAction();
 *             }
 *         });
 *     }
 * }
 * </pre>
 */
public abstract class ViewModel { }

类注释模版

类注释模版 《同类教程》

  1. 在Android Studio中打开File > Settings > Editor > Live Templates界面。
  2. 点击加好号新建Template Group。
  3. 选中新建的Group后,新建Live Template,设置快捷名称等参数。
  4. 添加模板为:
java
/**
 * @author 你的名字
 * @date $date$ $time$
 * @description 
 */

添加完模板后,点击Edit Variables设置date和time的默认值,这样就会快速生成当前时间。


✅️ File and Code Templates设置界面中还可以设置类创建时候的一些自定义模板。

字段注释

字段注释多注重于用途目的和设计意图。以下是一些常见的字段注释类型及其示例。

1.目的性注释

这种注释主要描述变量或字段的用途和为什么要使用特定的值。

正例:注释解释了“为什么”这样设计

java
/** 设置重试次数为 3 次,防止无限循环卡住线程 */
private int mRetryAttempts = 3;

/** 使用 60 秒超时,符合业务需求限制,避免请求阻塞 */
private int mTimeoutSeconds = 60;

反例:注释解释了代码的细节,但不解释原因

java
/** 初始化变量默认为3 */
private int mRetryAttempts = 3;

2.单位说明注释

在数值变量旁边注明单位,特别是当字段表示一个物理量或时间时,可以避免误解。

java
/** 文件大小限制,单位为 MB */
private int mMaxFileSize = 100;

3.值范围注释

如果变量的值有特定的范围,应该通过注释进行说明,以免误用。比如布尔值的含义、整数的有效范围等。

java
/** 设置透明度,取值范围在0-255 */
private int mAreaAlpha = 100;

/** 表示用户状态:true 表示激活,false 表示未激活 */
private boolean isActive = true;

4.设计意图注释

这种注释专注于解释代码设计的原因、思路和目的。

java
/**
 * 因为日志列表的设计是只记录同一条报警日志的开始时间和结束时间,
 * 所以这个Map只用来存储和解除某个报警的数据开始状态。
 * 
 * 收到开始报警状态时,用于查询是否已经存在加入的开始报警数据。
 * 收到结束报警状态时,用于删除Map中的数据,并更新至数据库。
 */
private final Map<String, LogDataModel> startAlarmDataMap = new HashMap<>();

方法注释

我认为方法注释应详细描述方法的功能、输入参数、返回值和可能的副作用。

例举一些示例:

kotlin
/**
 * 在主线程上运行任务
 *
 * @param task 要执行的任务
 * @param duration 延迟执行的时间(毫秒)
 */
fun runOnUiThreadTask(task: Runnable?, duration: Long) {
    task ?: return
    getThreadHandler()?.runOnUiThread(task, duration)
}

/**
 * 在工作线程中延迟执行
 *
 * @param task 要添加到队列中的任务
 * @param delayMillis 延迟执行的时间(毫秒)
 */
fun runOnWorkThread(task: Runnable?, delayMillis: Long = 0) {
    task ?: return
    getThreadHandler()?.runOnWorkThread(task, delayMillis)
}
java
/**
 * 注册网络回调监听器
 *
 * registerNetworkCallback 支持Android 5.0以上的版本
 *
 * 5.0以下建议注册广播接收器实现onReceive方法:
 *
 *     override fun onReceive(context: Context, intent: Intent) {
 *         val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 *         val networkInfo = connectivityManager.activeNetworkInfo
 *         if (networkInfo != null && networkInfo.isConnected) {
 *             // 网络已连接,执行相应的操作
 *         } else {
 *             // 网络未连接,执行相应的操作
 *         }
 *     }
 *     
 *    最后静态或动态注册广播接收器:
 *
 *     <!-- 静态注册-->
 *     <receiver android:name=".NetworkChangeReceiver">
 *         <intent-filter>
 *             <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
 *         </intent-filter>
 *     </receiver>
 *
 *     // 在你的 Activity 或 Service 中动态注册广播接收器
 *     val filter = IntentFilter()
 *     filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
 *     registerReceiver(NetworkChangeReceiver(), filter)
 */
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun register() {
    // 创建一个 NetworkRequest.Builder 对象,并添加网络连接能力
    val builder = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)

    // 注册网络回调监听器
    mConnectivityManager.registerNetworkCallback(builder.build(), mNetworkCallback)
}

↑ 这种注释在编辑器里切换成渲染视图效果以后也是非常美观的。

java
/**
 * 远程API调用方法。
 * 该方法同步执行远程API请求,并将其结果通过 Flow 机制发射不同状态的数据流,包括加载中、成功和失败的状态。
 *
 * @param apiCall    API调用的逻辑,传入的Callable会返回ResponseData对象,表示API请求结果。
 * @param <T>        API调用返回的数据类型。
 * @return FlowCollect<Resource<T>> 返回包含资源状态的数据流,可能是加载中、成功或失败。
 * 
 * <p>
 * FlowCollect对象会收集三个不同的状态
 * @see Resource.Loading 加载中状态
 * @see Resource.Success 请求成功状态 (包含成功的数据)
 * @see Resource.Fail 请求失败状态 (包含错误信息或异常)
 */
@SuppressWarnings("unchecked")
@WorkerThread
public <T> FlowCollect<Resource<T>> safeRemoteApiCall(Callable<ResponseData> apiCall) {
    return emitter -> {
        emitter.emit(new Resource.Loading<>());
        try {
            ResponseData responseData = apiCall.call();
            // 检查ResponseData是否成功
            if (responseData.isSuccess() && (responseData.getCode() == 0 || responseData.getCode() == 200)) {
                // 请求成功,发送成功状态和数据
                emitter.emit(new Resource.Success<>((T) responseData.getData()));
            } else {
                // 请求失败,发送失败状态和错误信息
                emitter.emit(new Resource.Fail<>(new Exception(responseData.getMessage())));
            }
        } catch (Exception e) {
            // 请求异常,发送失败状态和异常信息
            emitter.emit(new Resource.Fail<>(e));
        }
    };
}

关键行注释

关键行注释需要谨记一些原则,没有具体的规范限制。因为行注释本身只有在逻辑复杂、意图不明显或需要长期维护的情况下才会去添加注释。

1.上下文提示注释

对于较为复杂的操作,提供上下文联系或边界条件的相关注释,帮助理解该操作的必要性。比如:

java
// 判断数据表中的数据是否大于5万条,如果是,则删除最老的1000条数据,防止数据过多和存储占用过多。
NewHistoryDataDaoOpen.checkDataCount(getMyAppContext());

if (alarmCode >= 0 && alarmCode <= 3) { // 报警码在0-3之间
    // ...
} else { // 预警报警皆正常,判断..
    // ...
}

switch (alarmCode) {
    case 0: // 下限报警
        // 如果存在下限预警开始的状态则结束
        // ...
    break;
    case 1: // 下限预警
        // 如果存在下限报警开始的状态则结束
        // ...
    break;
    case 2: // 上限预警
        // 如果存在上限报警开始的状态则结束
        // ...
    break;
    case 3: // 上限报警
        // 如果存在上限预警开始的状态则结束
        // ...
    break;
}

2.定期更新

随着代码的修改,注释也应该同步更新,避免注释与实际代码行为不一致。

3.尽量解释原因而非描述操作

注释应该更多的去解释 “为什么” 而不是 “做了什么”,因为代码本身已经表达了操作的内容。注释重点在于设计意图、约束条件、或选择方案的原因。

标记注释

标记注释是一种特殊的注释方式,主要用来指引代码阅读者或后续接收的开发者关注某些特殊内容,或为未来的修改留下一些标记。

常见标记:

  • TODO 通常是记录一些未完成的实现的部分,但 TODO 不仅限于表示未完成任务,也可以带有提示性信息
  • FIXME 记录需要修复的代码或存在问题的部分。

下面是一些示例:

kotlin
/**
 * @author 开发者姓名
 * @date 2024/7/17 18:37
 * @description FromRightAnimator 是一个自定义的 RecyclerView 条目动画器,用于为新增的条目应用动画效果。
 *
 * @param context 上下文对象,用于加载动画资源。
 * TODO 未完(暂不建议使用):还未设计不同动画的自由切换逻辑。
 */
class FromRightAnimator(private val context: Context) : RecyclerView.ItemAnimator() { }
java
/**
 * @author 开发者姓名
 * @date 2024/4/16 14:34
 * @description 原始源数据的缓存密钥 + 任何请求的签名。
 * A cache key for original source data + any requested signature.
 * <p>
 * TODO 该类来源于[com.bumptech.glide.load.engine.DataCacheKey],其主要作用是将url生成一个缓存密钥,用于本地缓存文件的名称。
 * <p>
 * TODO 因源码中该类为final且没有被public修饰,导致无法访问,所以单独复制出来。
 */
final class DataCacheKey implements Key { }

/**
 * @author 开发者姓名
 * @date 2024/7/10 21:35
 * @description VerticalScrollTextView 是一个自定义的 TextView,允许用户调整纵向滚动的灵敏度。
 * FIXME 已知BUG(暂未修复):1.在父布局为ConstraintLayout的情况下getLayout().getHeight()会获取不准确。
 * FIXME 已知BUG(暂未修复):2.链接文本在横向末端处点击空白区域也会触发点击事件。
 */
public class VerticalScrollTextView extends AppCompatTextView { }

/**
 * 随机获取一条网抑云音乐热评
 * 
 * @param request 请求参数
 */
public void requestNeteaseComment(NeteaseCommentRequest request) {
    // 通过okhttp线程池管理器获取线程池执行任务 
    // TODO 若是在kotlin中可直接采用ViewModel协程作用域
    OkHttpConfig.getInstance().getClient().dispatcher().executorService().execute(() -> {
        mMainRepository.getNeteaseComment(request).collect(mNeteaseComment::postValue);
    });
}
kotlin
override fun showToast(params: ToastParams) {
    when (mShowStrategyType) {
        SHOW_STRATEGY_TYPE_IMMEDIATELY -> {
            // ....
        }
        SHOW_STRATEGY_TYPE_QUEUE -> {
            // TODO 为了代码的完美性,后期会采用自定义队列来实现。记录日期:【2024-10-10 9:39】。
            // ....
        }
    }
}

标记注释也是可以自定义的

  1. 打开设置:在 Android Studio 中,点击 File > Settings > Editor > TODO。
  2. 添加自定义标签:点击 + 号添加新的标记注释。可以指定文本(如 REVIEW)和正则表达式的匹配规则,还可以选择区分大小写,从而对特定格式的注释就可以进行高亮显示。

专事专办

标记注释的使用一定要合理,因为每一个标记我们都赋予了它不同的含义。

代码排版

首先,代码排版的这些规则并非是一种束缚,而是为代码的美观和逻辑清晰度提供更好支持。

类成员的顺序

为了提高类的可读性,建议类成员按照以下顺序组织。

从上往下排序

  • 1.常量 (kotlin的话 第一个就是伴生对象)
  • 2.静态变量
  • 3.静态方法
  • 4.类字段
  • 5.构造函数
  • 6.普通方法
  • 7.内部类
  • 8.接口类

1.常量 优先定义常量,若在Kotlin中常量要放在伴生对象里。

java
private static final int MAX_VALUE = 100;
public static final String APP_NAME = "MyApp";
kotlin
companion object {
    // 伴生对象优先在前
}

2.静态成员

将静态变量和方法放在非静态成员之前,如果是kotlin的话,这些成员是放在伴生对象里的。

java
private static int instanceCount = 0;

public static int getInstanceCount() {
        return instanceCount;
        }
kotlin
companion object {
    private val instanceCount: Int = 0
    
    fun getInstanceCount(): Int {
        return instanceCount
    }
}

3.类字段

一般访问权限同类型的字段,可以按照执行顺序或者重要程度去摆放顺序,不同权限的类型字段按照访问权限从高到低排列(如private放在最上面)

java
private int id;
protected String name;
public double price;

4.构造函数

构造函数(包括构造重载函数)通常放在成员变量之后,初始化实例时第一个被调用。

java
public MyClass() {
    // ...
}

kotlin中的init

在kotlin中init方法要比构造函数先执行,所以一定要放在构造函数之前。

kotlin
init {

}

constructor() {
    // ...
}

5.普通方法

普通方法的摆放顺序一般是公有方法的优先,私有方法在后,尤其是核心功能方法,便于快速定位。

但是这个规则并非强制,根据实际情况而定。

✅️ 重要的是无论是私有方法、受保护的方法还是公有方法,都最好去按执行顺序或者重要程度去摆放。就像Activity生命周期方法重写,都是按照 onCreateonDestroy 的一个顺序。

方法摆放顺序示例(请意会!):

kotlin
/**
 * @author 开发者姓名
 * @date 2024/10/8 10:05
 * @description 线程处理ThreadHandler意图
 */
interface ThreadHandlerAction {

    /**
     * 获取工作线程上处理的 Handler
     */
    fun getThreadHandler(): ThreadHandler? {
        return ThreadHandler.createHandler()
    }

    /**
     * 在主线程上运行任务
     *
     * @param task 要执行的任务
     */
    fun runOnUiThreadTask(task: Runnable?) {
        task ?: return
        getThreadHandler()?.runOnUiThread(task)
    }

    /**
     * 在主线程上运行延迟任务
     *
     * @param task 要执行的任务
     * @param duration 延迟执行的时间(毫秒)
     */
    fun runOnUiThreadTask(task: Runnable?, duration: Long) {
        task ?: return
        getThreadHandler()?.runOnUiThread(task, duration)
    }

    /**
     * 从主线程中移除指定的任务
     *
     * @param task 要移除的任务
     */
    fun removeFromUiThread(task: Runnable?) {
        task ?: return
        getThreadHandler()?.removeFromUiThread(task)
    }

    /**
     * 设置Ui线程的Handler回调接口
     */
    fun setUiHandlerCallback(callback: ThreadHandler.IUiHandlerCallback) {
        getThreadHandler()?.setUiHandlerCallback(callback)
    }
    
    /**
     * 发送消息到Ui线程的消息队列中
     *
     * @param msg 要发送的消息对象
     */
    fun sendUiThreadMessage(msg: Message) {
        getThreadHandler()?.sendUiThreadMessage(msg)
    }
    
    /**
     * 发送线程延迟消息到Ui线程的消息队列中
     *
     * @param msg 要发送的消息对象
     * @param delayMillis 延迟执行的时间(毫秒)
     */
    fun sendUiThreadMessageDelayed(msg: Message, delayMillis: Long = 0) {
        getThreadHandler()?.sendUiThreadMessageDelayed(msg, delayMillis)
    }
    
    /**
     * 移除Ui线程中消息队列中的所有任务和消息
     */
    fun removeUiCallbacksAndMessages() {
        getThreadHandler()?.removeUiCallbacksAndMessages()
    }

    /**
     * 在工作线程中执行
     *
     * @param task 要添加到队列中的任务
     */
    fun runOnWorkThread(task: Runnable?) {
        task ?: return
        getThreadHandler()?.runOnWorkThread(task)
    }

    /**
     * 在工作线程中延迟执行
     *
     * @param task 要添加到队列中的任务
     * @param delayMillis 延迟执行的时间(毫秒)
     */
    fun runOnWorkThread(task: Runnable?, delayMillis: Long = 0) {
        task ?: return
        getThreadHandler()?.runOnWorkThread(task, delayMillis)
    }

    /**
     * 从工作线程中移除指定的任务
     *
     * @param task 要移除的任务
     */
    fun removeFromWorkThread(task: Runnable?) {
        task ?: return
        getThreadHandler()?.removeFromWorkThread(task)
    }

    /**
     * 发送消息到工作线程的消息队列中
     *
     * @param msg 要发送的消息对象
     */
    fun sendWorkThreadMessage(msg: Message) {
        getThreadHandler()?.sendWorkThreadMessage(msg)
    }
    
    /**
     * 发送线程延迟消息到工作线程的消息队列中
     *
     * @param msg 要发送的消息对象
     * @param delayMillis 延迟执行的时间(毫秒)
     */
    fun sendWorkThreadMessageDelayed(msg: Message, delayMillis: Long = 0) {
        getThreadHandler()?.sendWorkThreadMessageDelayed(msg, delayMillis)
    }
    
    /**
     * 移除工作线程中消息队列中的所有任务和消息
     */
    fun removeWorkCallbacksAndMessages() {
        getThreadHandler()?.removeWorkCallbacksAndMessages()
    }
}

6.内部类和接口类

内部类和接口类就不多说了,总之内部类在前,接口类在后。

缩进与空行

1.空格和空行的合理使用

注意代码之间应该有适当的空格,不能多也不能太少。

正例:

java
// 缩进一致,适量空格
/**
 * 发送报警日志广播
 *
 * @param logDataModel 日志数据模型
 */
private void sendAlarmLogBroadcast(LogDataModel logDataModel) {
    // 发送本地广播更新报警日志界面的列表UI
    Intent intent = new Intent(Const.ACTION_ALARM_LOG_UPDATE);
    intent.putExtra(IntentKey.LOG_DATA, logDataModel);
    LocalBroadcastUtil.sendLocalBroadcast(intent);
}

// 适当空行,层次分明
@Override
public void run() {
    RealTimeCurveTask timeCurveTask = mRealTimeCurveTask.get();
    if (timeCurveTask == null) {
        return;
    }

    // 刷新曲线图数据
    timeCurveTask.refresh(mEntity);

    // 获取一次新的延迟时间,继续循环。
    int second = getDelayedTime(mEntity);
    timeCurveTask.postDelayed(this, second * 1000L);
}

反例:

java
// 缩进不一致,缺少空格

//发送报警日志广播
private void   sendAlarmLogBroadcast(LogDataModel logDataModel){
//发送本地广播更新报警日志界面的列表UI
Intent intent=new   Intent(Const.ACTION_ALARM_LOG_UPDATE);
intent.putExtra(IntentKey.LOG_DATA,logDataModel);
LocalBroadcastUtil.sendLocalBroadcast(intent);
}

// 代码紧凑,层次模糊
@Override
public void run() {
    RealTimeCurveTask timeCurveTask = mRealTimeCurveTask.get();
    if (timeCurveTask == null) {
    return;
    }
    timeCurveTask.refresh(mEntity);
    int second = getDelayedTime(mEntity);
    timeCurveTask.postDelayed(this, second * 1000L);
}

空格空行的好处

  • 空格:代码段内的空格能有效分隔代码的结构,便于阅读。
  • 空行:合适的空行分隔逻辑单元,使代码层次分明。

2.大括号位置

左大括号不需要单独一行,与前面代码同一行并用空格隔开。因为这样使结构更加紧凑,才符合Java的通用规范。

正例:

java
if (value == null || value.isEmpty()) {
    return; 
}

反例:

java
if (value == null || value.isEmpty()) 
{ 
    return; 
}

3.单行代码括号规范

每个判断语句都需要加左右括号,即便是单行语句,也应使用大括号,以减少未来更改或添加代码时出错的风险。

正例:

java
if (!isSuccess) {
    return; 
}

反例:

java
if (!isSuccess) return;

4.适当换行

当一行代码行过长时,可以适当换行提高可读性。

正例:

java
boolean isSuccess = sendMainBoardReadRegCmd03(BOARD, name, (byte) 3,
        qLmodbusCmdReg.getRegAddress(), qLmodbusCmdReg.getDataUseRegCount(), 3);

反例:

java
boolean isSuccess = sendMainBoardReadRegCmd03(BOARD, name, (byte) 3, qLmodbusCmdReg.getRegAddress(), qLmodbusCmdReg.getDataUseRegCount(), 3);

5.链式调用规范

链式调用时,每个函数应单独一行,换行处就在 . 之前,保持整齐易读。

正例:

kotlin
       // 刷新头部是否跟随内容偏移
layout.setEnableHeaderTranslationContent(true) 
        // 刷新尾部是否跟随内容偏移
        .setEnableFooterTranslationContent(true) 
        // 加载更多是否跟随内容偏移
        .setEnableFooterFollowWhenNoMoreData(true) 
        // 内容不满一页时是否可以上拉加载更多
        .setEnableLoadMoreWhenContentNotFull(false) 
        // 仿苹果越界效果开关
        .setEnableOverScrollDrag(false)

反例:

kotlin
// 参数配置...
layout.setEnableHeaderTranslationContent(true).setEnableFooterTranslationContent(true).setEnableFooterFollowWhenNoMoreData(true).setEnableLoadMoreWhenContentNotFull(false).setEnableOverScrollDrag(false)

函数设计规范

单一职责

一个函数应该仅承担一个职责,完成一项具体的任务。如果一个函数的功能过于复杂或处理多个不同的任务,就容易导致代码难以理解、修改和测试。函数的职责越单一、越简短、就越容易维护和扩展。

例如,一个函数如果既处理数据验证,又进行数据存储,可能会导致代码逻辑的混乱,难以排查和修改问题。应该考虑拆分成多个职责清晰的函数。

正例:

java
// 获取用户的输入并进行验证
public boolean validateUserInput(String input) {
    // 具体的验证逻辑
}

// 存储用户数据
public void storeUserData(User user) {
    // 存储数据的逻辑
}

反例:

java
// 一个函数既验证输入又存储数据,职责混乱
public void validateAndStoreUserData(String input, User user) {
    // 验证输入
    // 存储用户数据
}

说一个我自身的例子

早期做开发的时候,有一次我做一个列表条目的点击事件回调方法,按照常见的逻辑就是拿到条目的下标或对应的实体类去执行一个逻辑相同的方法。

但是我那个列表特殊,每一个条目都做了不相同的事情,就会导致我在回调方法里即处理某些相同逻辑,又处理某些不同逻辑,代码结构类似这样:

java
@Override
public void onItemClick(RecyclerView recyclerView, View itemView, int position) {
    if (position == 0) {
        // ....
    }
    if (position == 1 && position == 3) {
        // ....
       if () // ...
    }
    // 弄了一大堆的if判断和逻辑嵌套
}

导致后续我在维护的时候异常繁琐和困难。

怎么优化这套繁琐的逻辑(⊙o⊙)?我的做法是把事件封装到实体类中。

比如:

java
class MyModel {
    private Runnable onClickAction;

    public void setOnClickAction(Runnable onClickAction) {
        this.onClickAction = onClickAction;
    }

    public Runnable getOnClickAction() {
        return onClickAction;
    }
}

MyModel item = new MyModel();
item.setOnClickAction(() -> {
    // 编写具体的逻辑功能
});
mAdapter.appendBean(item);

@Override
public void onItemClick(RecyclerView recyclerView, View itemView, int position) {
    mAdapter.getListBeans(position).getOnClickAction().run();
}

也可以自己定义传参数等,也可以自定义接口啥的,可扩展的方案非常多。当然,优化的方式也绝不止这一种,还有策略模式,视图绑定等等都可以解决,但要根据实际情况而定。

最后我想说,无论如何都请保持函数单一职责的原则标准。让函数尽可能地聚焦于单一的功能,也能有效避免函数内部的复杂逻辑和过多的判断,减少出错的机会。

我一直都认为保持函数的单一职责原则是编写高质量代码的核心之一。

函数长度

正如单一职责原则中提到的函数应该保持简短和聚焦,所以也应该避免过长。

推荐控制在 20行以内,尽量不超过 50行。因为过长的函数往往执行了太多的任务,导致逻辑复杂,也增加了未来修改的难度。如果一个函数非常长,而且你确保它就是在做单一职责的事情,那可能是时候考虑拆分成多个较小的函数。

然后拆分的标准应当根据函数内的逻辑进行划分,确保每个函数都能够清晰地表达其意图和行为。

正例(按逻辑单元划分更细小的函数,结构非常清晰)

kotlin
override fun onClick(v: View?) {
    super.onClick(v)
    when (v?.id) {
        R.id.iv_recording_start -> { // 开始录音
            showUpdateRecordingUIState()
            onStartRecord() // 开始录制
            setStartRecordTime(System.currentTimeMillis())
            myCountDownTimer?.start() // 开启倒计时
        }
    }
}

/**
 * 录音中的UI状态
 */
private fun showUpdateRecordingUIState() {
    binding.ivRecordingStart.visibility = View.GONE
    binding.tvRecordingStop.visibility = View.VISIBLE
    binding.ivVoicePlay.visibility = View.GONE
    binding.ivVoiceStop.visibility = View.GONE
    binding.ivVoiceOff.visibility = View.GONE
    binding.tvVoiceSend.visibility = View.INVISIBLE
    binding.tvRecordingTimer.visibility = View.VISIBLE
}

/**
 * 记录录音开始的时间
 */
private fun setStartRecordTime(time: Long) {
    mStartRecordTime = time
}

/**
 * 开始录音
 */
private fun onStartRecord() {
    try {
        mRecorder?.start()
        isRecorderStart = true
    } catch (ex: Exception) {
        ex.printStackTrace()
    }
}

反例(逻辑单元没有拆分)

kotlin
override fun onClick(v: View?) {
    super.onClick(v)
    when (v?.id) {
        R.id.iv_recording_start -> { // 开始录音
            // 录音中的UI状态
            binding.ivRecordingStart.visibility = View.GONE
            binding.tvRecordingStop.visibility = View.VISIBLE
            binding.ivVoicePlay.visibility = View.GONE
            binding.ivVoiceStop.visibility = View.GONE
            binding.ivVoiceOff.visibility = View.GONE
            binding.tvVoiceSend.visibility = View.INVISIBLE
            binding.tvRecordingTimer.visibility = View.VISIBLE
            // 开始录制
            try {
                mRecorder?.start()
                isRecorderStart = true
            } catch (ex: Exception) {
                ex.printStackTrace()
            }
            // 记录录音开始的时间
            mStartRecordTime = time
            setStartRecordTime(System.currentTimeMillis())
            // 开启倒计时
            myCountDownTimer?.start()
        }
    }
}

虽然你可能会觉得就算没拆分,这段代码块也不大🤣🤣🤣。但其实这里的示例是我删减版的,而且示例只为了演示拆分的好处。

实际项目中,随着代码复杂性增加,使用拆分的小函数能极大提高可读性和可维护性。

参数数量控制

一个函数的参数数量应尽量保持简洁,参数越少越好。如果参数过多,函数的调用和理解可能变得复杂。 此时,可以考虑将相关参数封装成一个对象或数据结构,减少参数数量,提升函数的可读性和可维护性。

可读性不用多说,提高的维护性可以什么地方体现出来(⊙o⊙)?

比如某个业务接口方法传参:

java
public class NeteaseCommentRequest {

    /**
     * 接口数据格式 text/json
     */
    private String format;

    public String getFormat() {
        return format;
    }

    public void setFormat(String format) {
        this.format = format;
    }
}

    /**
     * 随机获取一条网抑云音乐热评
     *
     * @param request 请求参数
     */
    @WorkerThread
    ResponseData getNeteaseComment(NeteaseCommentRequest request);

我们将一个接口的参数封装到model类中,在定义接口的时候,参数只有一个request。这样做的好处是首先体现在维护性上:未来需求改变,需要增加或修改参数时,只需要修改Request类的字段,而不必修改整个调用链。其次就是美观,简洁。

因材施教

当然,不是所有函数我们都要刻意的去减少参数,但至少我们在做抉择时,一定要考虑它的合理性。

异常处理

异常处理是保障代码在错误发生时能够有序退场或恢复的关键。以下是一些异常处理的规范和注意事项:

1.避免捕获后不处理

首先避免以下这个写法:

java
try {
    // 可能抛出异常的代码
} catch (IOException e) { }

不要只是捕获异常后忽略它(即不处理异常)。即使没有处理异常的相关逻辑,我们也应至少记录日志或打印堆栈信息,确保问题不会被悄悄吞掉。

2.尽量在底层抛出异常

异常处理应尽量在业务逻辑层进行,将原始异常在底层代码中通过 throw 向上抛出,而不要在底层直接处理。因为这种方式有助于在业务逻辑层有更好的上下文信息用于判断是否继续处理或记录日志。

正例(在底层抛出异常,由业务逻辑层处理)

java
// 底层方法:不处理异常,直接抛出
public String readFileContent(String filePath) throws IOException {
    FileInputStream fileInputStream = new FileInputStream(filePath);
    // 读取文件内容
    // ...
    return "File content";  // 假设这里是文件内容
}

// 业务逻辑层:捕获并处理异常
public void processFileData(String filePath) {
    try {
        String content = readFileContent(filePath);
        // ...
    } catch (IOException e) {
        // 业务逻辑层决定如何处理异常
        // ...
    }
}

在这个例子中,底层的 readFileContent 方法不直接处理异常,而是将异常通过 throws 向上抛出。业务逻辑层的 processFileData 方法捕获并处理异常。

这样设计的好处是,底层方法专注于数据处理,业务逻辑层有更丰富的上下文信息来决定如何应对异常,例如记录日志或提示用户。

反例(在底层处理异常)

java
// 底层方法
public String readFileContent(String filePath) {
    try {
        FileInputStream fileInputStream = new FileInputStream(filePath);
        // 读取文件内容
        // ...
        return "File content";
    } catch (IOException e) {
        return null; // 返回空值
    }
}

// 业务逻辑层
public void processFileData(String filePath) {
    String content = readFileContent(filePath);
    if (content == null) {
        // ...
    } else {
        // ...
    }
}

在反例中,底层方法 readFileContent 捕获并处理了异常。由于底层没有完整的业务上下文,所以处理逻辑就并不合适,错误信息也没有被日志记录。导致调用方 processFileData 无法知道异常的具体原因,甚至不清楚是否发生了异常。

这种代码,也会使定位和调试问题变得困难。而且,返回 null 会增加调用方的处理负担,因为调用方必须额外检查是否有内容可用,代码也会因此不够简洁。

控制流结构

简化条件语句

条件语句:条件语句尽量保持简洁,复杂条件可以用临时变量或布尔函数来表示。

推荐:

java
// 方式一
boolean isUserAuthorized = user != null && user.hasPermission(Const.PERFORM_ACTION);
if (isUserAuthorized) {
   performAction();
}
        
// 方式二
if (isUserAuthorized(user)) {
    performAction();
}

避免:

java
if (user != null && user.hasPermission(Const.PERFORM_ACTION)) {
    performAction();
}

💯 要避免复杂嵌套的条件表达式,使代码更加简洁、美观和易读。

避免嵌套过深

对于嵌套的if语句,尽量使用return或continue减少嵌套层数。

推荐:

java
if (!condition) {
    return;
}
performAction();

避免:

java
if (condition) {
   performAction();
}

循环

优先使用 for-each 或迭代器,避免手动管理索引。对于无限循环,确保在合适的条件下中断。 在用不到 for 循环 i 下标情况下优先使用增强for循环(for-each)来处理集合,提高代码简洁性和可读性。

推荐:

java
for (String item : itemList) {
    process(item);
}

避免

java
for (int i = 0; i < itemList.size(); i++){
    process(itemList.get(i));
}

why?

1.性能优化

在传统 for 循环中,itemList.size() 在每次循环时都会调用,这在一些实现上可能会影响性能(例如当itemList是LinkedList类型时,因为计算大小需要遍历整个链表)。而增强 for 循环完全避免了这个问题。

2.减少出错的机会

增强 for 循环自动处理元素的迭代,而传统的 for 循环可能因为索引错误而导致意外的逻辑问题或运行时错误。

硬编码规范

首先,硬编码就是指将数值、字符串或其他具体值直接写在代码中,这样随着项目的迭代,就会导致代码可维护性、可读性和扩展性较差。所以,我们应当尽量避免硬编码常量,并在项目中使用集中式的配置方案。

避免硬编码常量

硬编码常量分散在代码中,就会导致如果需要修改或调整,必须找到所有的并逐个更改,易造成遗漏和错误。硬编码的内容也会让代码意图不明确,导致可读性差。

正例:

java
// 压力
String pressure = getParamRealValue(Const.PRESSURE);
// 盐度
String salinity = getParamRealValue(Const.SALINITY);
java
if (view.getVisibility() != View.VISIBLE) {
    return;
}
kotlin
val mIntent = Intent(AppContext.mMainContext, BotWebSocketService::class.java)
mIntent.putExtra(IntentKey.CLIENT_ID, clientId)
mIntent.putExtra(IntentKey.SOCKET_SERVICE_MASSAGE_HANDLE, parcelable)

反例:

java
// 压力
String pressure = getParamRealValue("压力");
// 盐度
String salinity = getParamRealValue("盐度");
java
if (view.getVisibility() != 0) {
    return;
}
kotlin
val mIntent = Intent(AppContext.mMainContext, BotWebSocketService::class.java)
mIntent.putExtra("client_id", clientId)
mIntent.putExtra("socket_service_massage_handle", parcelable)

全局配置

首先在定义常量的时候,要分得清全局常量和局部常量,如果一个常量只在某一个类里有用,那就不用定义到全局常量中。

定义常量时,也要根据不用的作用范围去分类。

全局常量类:

kotlin
object Const {
    // flutter引擎ID
    const val MY_ENGINE_ID = "MY_ENGINE_ID"
    // 数据名db名称
    const val DB_NAME = "knowledge_db"
}

全局Intent传值常量类:

kotlin
object IntentKey {
    // 文章实体类对象传值标识
    const val ESSAY_MODEL = "ESSAY_MODEL"
}

全局接口地址常量类:

java
object NetURL {
    // 服务器地址
    const val SERVER_HOST: String = "http://127.0.0.1"
    // 检测app升级
    const val URL_CHECK_APP_UPDATED: String = "/toolApp/updated"
}

像一些未来可能会调整的参数,允许通过配置文件注入,避免将业务逻辑直接写入代码中。比如:

properties配置:

properties
# 强制升级包
FORCED_UPGRADE=true

# 开机自启动
START_UP_SELF=true

Gradle配置文件中:

kotlin
defaultConfig {
    // ...
    
    manifestPlaceholders.apply {
        put("FORCED_UPGRADE", project.property("FORCED_UPGRADE") as String)
        put("START_UP_SELF", project.property("START_UP_SELF") as String)
    }
}

定义全局常量:

kotlin
object IntentKey {
    const val FORCED_UPGRADE: String = "FORCED_UPGRADE" // 是否强制升级包
    const val START_UP_SELF: String = "START_UP_SELF"// 是否自启动应用
}

业务逻辑层:

kotlin
/** 是否自启动 */
private var mSelfStartUp: Boolean? = null

/**
 * 是否自启动
 */
fun isSelfStartUp(): Boolean {
    return mSelfStartUp ?: AppContext.sApplication.packageManager.getApplicationInfo(
            AppContext.sApplication.packageName,
            PackageManager.GET_META_DATA
    ).metaData.getBoolean(IntentKey.START_UP_SELF).also { mSelfStartUp = it }
}

if (isSelfStartUp()) {
    // ...
}

提示

但也不是所有的固定数值都得常量化,有些就使用了一次的固定值,也没有必要去常量化,因为意义也不大。比如:

kotlin
layoutParams.width = DisplayUtil.dip2px(8F) // 这8F写死就写死了

所以说,当一个数值被使用了多次,或者它有特定意义时,就一定要常量化定义出来。

清单文件

关于 Android 的 AndroidManifest 文件,首先要注意添加权限时的注释言名。

示例:

xml
<!-- 连网-->
<uses-permission android:name="android.permission.INTERNET" />
        <!-- 网络状态-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
        <!-- wifi-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

        <!-- 适配高版本的新权限,低版本会自动向下兼容 -->
        <!-- 请求用户选择的图片和视频的权限,适用于 Android 14+ -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
        <!-- 读取音频权限(Android 13.0 新增的权限)-->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
        <!-- 在 Android 10 及以下版本写入外部存储的权限 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />

其次不光是权限标签,任何标签都应该有注释言名其添加原因。

比如:

xml
<application
    android:name=".app.BaseApplication"
    android:allowBackup="true"
    android:requestLegacyExternalStorage="true"
    android:networkSecurityConfig="@xml/network_security_config"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.ImBot"
    android:usesCleartextTraffic="true"
    tools:targetApi="n">

    <!-- 适配 Android 7.0 文件意图 -->
    <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">

        <!-- 通过 file_paths.xml 配置文件定义 FileProvider 允许共享的文件路径。
        file_paths.xml 文件位于 `res/xml/` 目录中,通常用于指定应用内部存储、外部存储等路径,
        允许应用在文件访问受限的环境中安全共享文件。-->
        <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
    </provider>

    <!-- android:enabled:指定服务是否应该在应用程序启动时自动启用。默认值为 true。-->
    <!-- android:exported:指示服务是否可以由其他应用程序组件通过隐式 Intent 启动或绑定到。默认值为 false。-->
    <service android:name=".service.BotWebSocketService"
             android:enabled="true"
             android:exported="false"
    />

    <!-- 初始化界面-->
    <activity
            android:name=".ui.activity.InitActivity"
            android:exported="true"
            android:screenOrientation="portrait"
            android:theme="@style/SplashTheme"
            tools:ignore="LockedOrientationActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <!-- 主页activity-->
    <activity
            android:name=".ui.activity.MainActivity"
            android:launchMode="singleTask" />

    <!-- 图片选择页面-->
    <activity android:name=".ui.activity.ImageSelectActivity"/>

    <!-- 图片预览界面-->
    <activity android:name=".ui.activity.ImagePreviewActivity"/>

    <!-- 聊天框界面-->
    <activity android:name=".ui.activity.ChatActivity"/>

    <!-- 公告页面-->
    <activity android:name=".ui.activity.AnnouncementActivity" />

    <!-- QQ登录页面-->
    <activity android:name=".ui.activity.QQLoginActivity" />
</application>

保持一种爱加注释的风格习惯,因为注释不仅仅是我们对代码的补充说明,更是对未来的自己和团队成员的一种指引。每段清晰的注释,都是我们为自己和团队埋下的善意伏笔,为未来的维护和优化节省了大量的时间和精力。

在 Apache-2.0 许可证下发布。