Android中如何优雅的定义常量

本篇博客是笔者的第一篇博客,其实很早之前就有了写博客的想法。但是奈何万事开头难,一直没有下定决心。随着自己积累的知识不断增多,如果不进行总结,不进行思考归纳,有些知识会经常遗忘。对于某些难题,可能当时有了解决方案,但是如果不做笔记,不做记录,过不了多久就会忘记当时的思路,笔者深有体会。这会大大降低我们的工作效率。所以笔者下定决心,坚持写博客,通过博客来记录自己平时的知识积累,通过博客也让自己的知识能够分享给更多的人,同时对自己也是一种督促,更能发现自己对知识点理解的错误。今天第一篇博客,笔者想总结在Android中如何更优雅的定义常量。

在Java中定义常量的方式

1.在类中定义

public final class Constants {
    private Constants() {}
    public static final int A = 0;
    public static final int B = 1;
    public static final int C = 2;
    public static final int D = 3;
}

2.在接口中定义

public interface Constants {
    int A = 0;
    int B = 1;
    int C = 2;
    int D = 3;
}

那么这两种方式区别在哪里呢?
1.类定义常量,需要定义成final且定义一个private的构造方法, 这样做是为了不让其他类继承, 禁止实例化此类,调用时直接以"类.常量"的方式调用。
2. 接口中定义的"变量",其实就是常量,接口中的"变量"默认都是 “public static final"类型,即为常量, 因此接口可以省略"public static final"而直接写成 “type variable”。
3.用如上的类定义常量,不能实例化也不能被继承, 看起了完美无缺。
4.接口定义常量, 虽不能实例化, 确可以被其他类实现。因此有这么一种设计模式"The constant interface pattern”,所谓的"常量接口模式"就使用这些常量的类实现这个接口,以避免用类名修饰常量名。其实这是对接口的不良使用。接口中定义的常量应为所有类频繁使用的常量,但并不是每个类都使用了接口中定义的所有常量,因而导入了不必要的常量到这个类中,并且导入后这个类的子类也会导入基础的常量, 这样会导致混乱,应当避免此种用法。并且Effective Java中第19条也明确指出接口应该只被用来定义类型,他们不应该被用来导出常量。但是呢我们大可不必拘泥于接口这一含义他是对行为的一种抽象。如果该接口仅仅是作为定义常量使用,那么我们可以直接通过"接口名.变量名"调用。这样也就避免了实现改接口,从而将接口中不必要的常量导入其中,但是这也取决于我们合理的使用。
5. 在interface和class中定义相同的常量,interface生成的class文件比class生成的class文件会更小,而且更简洁, 效率更高。
小结:

  1. 不要使用"常量接口模式",此模式会导致类中的常量混乱,增加维护难度。
  2. 不要使用静态导入,import static **。我非常不赞同这种使用常量的方法,因为import static会导致可维护性下降,维护的人看代码时, 不是那么清楚或者不那么迅速的知道这个常量位于哪个文件中。 建议使用常量的地方直接 “接口.常量名” 的方式使用。
  3. 对于用是用interface定义常量还是使用class定义常量,看个人喜好,个人觉得interface定义常量更为优美, 代码更简洁,生成的class文件更小, JVM不要考虑类的附加特性, 如方法重载等, 因而更为高效。虽然这是一种反模式的用法, 很多人不喜欢这种用法,如果我们知道它的优缺点,延长避短, 也是无可厚非的。还有一点是不要把这种用法用成"常量接口模式" , 个人觉得"常量接口模式"确实是一种对interface的"pool use"。

使用枚举代替常量,简化工作!

需求:有一个任务(Task),服务端定义了一个int status来标记任务的状态,包括 未开始、进行中、已完成 这三个状态,分别用 0、1、2 来标记。我们拿到这个status之后,需要根据状态的不同,显示不同的文本(需求仍在挖掘中)。需求很明确,一般我们会这么做:
在全局的常量类中定义定义3个静态常量表示状态常量:

public class Constants {
    public static final int STATUS_UN_START = 0;
    public static final int STATUS_PROGRESSING = 1;
    public static final int STATUS_COMPLETED = 2;
}

接下来在strings.xml定义状态值:未开始、进行中、已完成

<resources>
    <string name="status_un_start">未开始</string>
    <string name="status_progressing">进行中</string>
    <string name="status_completed">已完成</string>
</resources>

最后,在代码中拿到status之后,我们会这么做:

  switch (status) {
            case Constants.STATUS_UN_START:
                textView.setText(getResources().getString(R.string.status_un_start));
                break;
            case Constants.STATUS_PROGRESSING:
                textView.setText(getResources().getString(R.string.status_progressing));
                break;
            case Constants.STATUS_COMPLETED:
                textView.setText(getResources().getString(R.string.status_completed));
                break;
            default:
                break;
        }

这样写,所有的状态和状态值都统一了,规范的很,效果也不错,毫无违和感!
突然有一天,产品汪找到你:Hi,我们把文本前面的颜色设置成动态的吧,不同的状态颜色不一样,你根据效果图改一下吧…”。还好这个简单,分分钟的事。在colors.xml中,加几个色值:

<resources>
    <color name="status_un_start">#3F51B5</color>
    <color name="status_progressing">#303F9F</color>
    <color name="status_completed">#FF4081</color>
</resources>

然后修改switch语句,这样:

switch (status) {
            case Constants.STATUS_UN_START:
                view.setBackgroundResource(R.color.status_un_start);
                textView.setText(getResources().getString(R.string.status_un_start));
                break;
            case Constants.STATUS_PROGRESSING:
                view.setBackgroundResource(R.color.status_progressing);
                textView.setText(getResources().getString(R.string.status_progressing));
                break;
            case Constants.STATUS_COMPLETED:
                view.setBackgroundResource(R.color.status_completed);
                textView.setText(getResources().getString(R.string.status_completed));
                break;
            default:
                break;
        }

So easy,很快解决了问题!慢慢的,随着开发的深入,你发现许多地方需要这么显示,于是就封装了一个自定义ViewGroup,一切都朝着好的方向发展。但除此之外,在其他地方,也需要判断这个状态值,以需要获取该状态下的属性,万一哪天产品汪跟你说:“我要多加一个状态,未知(UN_KONW)。服务端和iOS已经同步了,你这边赶紧吧”,意味着之前涉及状态判断的都需要找到加上,岂不傻眼了?
所以,大多数人还是会选择将这一部分写到全局的静态方法中,这样加字段、加状态再也不怕了,在对应文件中加几个常量【status、颜色、状态文本】,改一下方法就搞定!由于这块不是本文重点,就不贴代码了。至此,我们的程序一步步完善了,一些基本的套路已经无法难倒我们了,完美!功能基本实现了,但是回过头来想想,这样做并不优雅!一方面需求变动时我们还是需要改很多地方,另一方面我们并不能很直观的看出状态值status与状态文本、色值之间的关系,比如 0代表未开始,颜色为黄色。当然,可以向上文一样,依靠命名规范去解决这个问题。可是,在命名规范的基础上,有没有更好的办法进一步强化他们之间的关系呢?当然有!接下来,就到了今天的主题,一起来借助枚举完成这一任务。

—————–枚举登场—————————–

刚开始,我们可以定义一个枚举类,这么写

public enum TaskStatus {

    UN_START(0, "未开始"),
    PROGRESSING(1, "进行中"),
    COMPLETED(2, "已完成");

    private int status;
    private String desc;

    TaskStatus(int status, String desc) {
        this.status = status;
        this.desc = desc;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public static TaskStatus getStatus(int status) {
        for (TaskStatus taskStatus : values()) {
            if (status == taskStatus.status) {
                return taskStatus;
            }
        }
        return UN_START;
    }
}

用的时候简直不要太简单(status是从服务端获取的状态值):

 TaskStatus taskStatus = TaskStatus.getStatus(status);
 textView.setText(taskStatus.getDesc());

然后后来产品说加一个色值,你很坦然就加上了。这么写:

public enum TaskStatus {

    UN_START(0, "未开始", "#3F51B5"),
    PROGRESSING(1, "进行中", "#303F9F"),
    COMPLETED(2, "已完成", "#FF4081");
    private int status;
    private String desc;
    private String color;

    TaskStatus(int status, String desc, String color) {
        this.status = status;
        this.desc = desc;
        this.color = color;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public static TaskStatus getStatus(int status) {
        for (TaskStatus taskStatus : values()) {
            if (status == taskStatus.status) {
                return taskStatus;
            }
        }
        return UN_START;
    }
}

用的时候毫不影响:

 TaskStatus taskStatus = TaskStatus.getStatus(status);
 textView.setText(taskStatus.getDesc());
 view.setBackgroundColor(Color.parseColor(taskStatus.getColor()));   
                          

重点来了,该到加未知(UN_KNOW)状态的时候了,按照上文的做法,我们要改、加的东西很多,但现在只需要加一行代码,是的,一行代码:

public enum TaskStatus {
    UN_KNOWN(-1, "未知", "#34D6B5"),
    UN_START(0, "未开始", "#3F51B5"),
    PROGRESSING(1, "进行中", "#303F9F"),
    COMPLETED(2, "已完成", "#FF4081");
    private int status;
    private String desc;
    private String color;

    TaskStatus(int status, String desc, String color) {
        this.status = status;
        this.desc = desc;
        this.color = color;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public static TaskStatus getStatus(int status) {
        for (TaskStatus taskStatus : values()) {
            if (status == taskStatus.status) {
                return taskStatus;
            }
        }
        return UN_KNOWN;
    }
}

就是这么简单!同时,这么写的话,就不需要在 string.xml、colors.xml、常量类中加一堆值了,直接放在枚举类中很直观有木有?!当然,有时候我们需要在某个地方使用常量来判断,下图是常见的做法。现在我们把状态值写到枚举里面了,还能这么直观的用吗?答案是肯定的!

        //使用常量
        if(status==Constants.STATUS_PROGRESSING){
            //do something
        }
        //使用枚举
        if(status=TaskStatus.PROGRESSING.getStatus()){
            //do something
        }

声明一点,我所说的使用枚举替换常量,是针对类似于“常量之间存在关联”的情况,如状态值status与状态文本、色值,此时使用枚举能大大简化我们的工作,并不是说以后所有常量都写成枚举,毕竟官方是不推荐使用枚举的:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
但也只是尽量避免使用!所以在实际开发中,还需要根据实际使用场景去斟酌,杜绝滥用。像本文描述的场景,建议使用,战斗力翻倍!

Android使用枚举注解代替枚举

需求:我们仅仅需要根据一组状态中的某一状态执行相应的操作,不需要多个常量之间相互对应。你自然会想到枚举的简化版:

public enum TaskStatus {
    UN_KNOW,
    UN_START,
    PROGRESSING,
    COMPLETED
}

然后定义了一个任务辅助类:

import com.ruicb.enumdemo.TaskStatus;
public class TaskHelper {

    public static void doSth(TaskStatus status){
        switch (status){
            case UN_KNOW:
                //do something
                break;
            case UN_START:
                break;
            case PROGRESSING:
                break;
            case COMPLETED:
                break;
        }
    }
}

然后如此调用:

TaskHelper.doSth(TaskStatus.COMPLETED);

这样使用枚举,十分优雅:保证了类型安全:调用者无法随意传一个 int 值;代码可读性非常高;但是…枚举对于性能的损耗,事实上你也知道,官方是不建议使用枚举的,这个在文章开头简单说了。想详细了解的,可以参考该博文:http://blog.csdn.net/hp910315/article/details/48975655。总之,与静态常量相比,枚举会增大应用程序编译后的 dex 文件,同时应用在运行时的内存占用也会升高。在资源有限的移动设备上,大量的使用枚举无疑是致命的。
替代方案
既然如此,我们不使用枚举不就行了吗?反正只是简单的单一状态,我单独定义一个常量类,实现分组:

/**
 * 任务状态常量
 */
public class TsakContants {
    public static final int UN_KNOW = -1;
    public static final int UN_START = 0;
    public static final int PROGRESSING = 1;
    public static final int COMPLETED = 2;
}

然后修改 doSth() 方法:

public class TaskHelper {

    public static void doSth(int status){
        switch (status){
            case TsakContants.UN_KNOW:
                //do something
                break;
            case TsakContants.UN_START:
                break;
            case TsakContants.PROGRESSING:
                break;
            case TsakContants.COMPLETED:
                break;
        }
    }
}

调用起来也没有区别:

TaskHelper.doSth(TsakContants.COMPLETED);

其次代码可读性很差,IDE 只是提示传入 int 类型的参数。此时如果不看方法体,调用者根本不知道该传什么值。那么问题来了?在一组常量的情况下,我们使用枚举太重,使用常量不安全、可读性差,我们该怎么办?当然是使用注解了!哈哈,兜了一圈终于轮到注解,是时候为其正名啦。

@IntDef(定义int) @StringDef(定义String类型) 是Android提供的注解

@IntDef({
        TaskStatus.UN_KNOW,
        TaskStatus.UN_START,
        TaskStatus.PROGRESSING,
        TaskStatus.COMPLETED
})
@Retention(RetentionPolicy.SOURCE)
public @interface TaskStatus {
    int UN_KNOW = -1;
    int UN_START = 0;
    int PROGRESSING = 1;
    int COMPLETED = 2;
}

修改 doSth() 方法:

public class TaskHelper {

    public static void doSth(@TaskStatus int status){
        switch (status){
            case TaskStatus.UN_KNOW:
                //do something
                break;
            case TaskStatus.UN_START:
                break;
            case TaskStatus.PROGRESSING:
                break;
            case TaskStatus.COMPLETED:
                break;
        }
    }
}

可以看出,doSth() 方法的参数是 int 类型的,但是使用 @TaskStatus 进行了注解,这样外界就无法传递 TaskStatus 之外的成员作为参数了。
调用

TaskHelper.doSth(TaskStatus.UN_START);

在调用时,IDE 会提示 @TaskStatus int status,提醒我们传入 TaskStatus 类型的值。同时,调用者如果再随便传入一个 int 值,虽然可以运行,但代码会爆红,lint 检查将会给与警告。

总结

现在看完本篇文章之后再思考,你应该清楚什么场景下该使用那种方案了吧。在类中定义常量可能更规范一点,但是书写起来以及class文件大小也要考虑。在接口中定义常量更加方便,class文件也相对较小,但是避免写成常量接口模式,要合理使用。所以两者的取舍看个人喜好吧,没有谁对谁错。此外如果需要定义一组常量,如果我们使用枚举,阅读性很好,但是性能缺损。如果单纯的使用常量,类型安全、可读性差,这时候我们可以考虑Android中提供的枚举注解,相当的优雅。此外如果定义多组常量,我们可以考虑枚举定义,这样阅读性和扩展性也是非常好的,虽然有性能缺损,但是我们也要度之。在性能还行的情况下,考虑使用。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值