前言
在最初学习Java的时候,我们都听到过一句话,Java是面向对象语言。每当提到面向对象的时候,许多开发者也嗤之以鼻:都什么年代了,谁还不知道面向对象。
重学设计模式后,请回答,你真的面向对象了吗?
你真的了解面向对象吗
一般情况下,我们会将面向对象的特性分为四大特性,分别是:封装、抽象、继承、多态。以这四大特性作为代码设计规范的编程风格我们一般称之为面向对象编程。
我们都知道Java语言是面向对象语言,那么用Java语言实现的代码就是面向对象编程吗?答案是否定的。在了解这个原因之前,首先我们需要需要知道面向对象四大特性分别可以解决什么问题。
封装
封装特性说白了就是数据访问限制或者叫数据访问保护,这一特性需要依赖语言本身具有访问权限机制。比如在Java中 使用private、public、protect等修饰符修复变量来控制变量读、写的权限控制,这一点是最容易被开发者忽略也是开发者最不在意或者容易使用错误的一点。这一点我们后续会详细讲解。
抽象
抽象特性主要用来隐藏方法的具体实现。也有一种说法将上面提到的四大特性中的抽象这一特性排除在外,这是因为函数本身就是一种抽象,函数内部包含具体的实现逻辑对调用者来说是不需要关注具体实现方式的。在Java语言中除了函数本身,通常使用interface接口和abstract抽象关键字来实现,抽象更像是一种理论指导,许多代码设计原则都是基于抽象理论来实现的。
举个具体的例子🌰,在Android开发中我们经常会使用到地图业务,以使用百度地图为例,开发者可能为了模块的通用性,会定义一系列的接口,代码如下所示:
public interface BaiduMapApi { /** * 加载地图 */ void loadBaiduMap(); /** * 销毁地图 */ void destoryBaiduMap(); }
按照抽象特性和代码设计原则来说,其实这套设计是有些瑕疵的。抽象要将具体实现隐藏起来,如果以后业务中的百度地图更改成了高德地图,那么这一套接口命名设计就会产生歧义。并且可能会为后人埋坑。
较为合理的设计代码如下所示:
public interface MapApi { /** * 加载地图 */ void loadMap(); /** * 销毁地图 */ void destoryMap(); }
这样一来,接口的设计遵循了抽象原则,更便于开发者后续的扩展和维护。
继承
继承用来表示类之间is-a的关系,比如:猫是动物、狗是动物,动物都会吃饭、睡觉,我们则会创建一个动物类,代码如下所示:
public class Animal { private void eat(){ System.out.println("--eat--"); } private void sleep(){ System.out.println("--eat---"); } }
然后再创建两个子类继承自Animal类,代码如下所示:
public class Brid extends Animal{ @Override public void eat() { super.eat(); } @Override public void sleep() { super.sleep(); } }
public class Dog extends Animal{ @Override public void eat() { super.eat(); } @Override public void sleep() { super.sleep(); } }
继承的最大好处就是实现代码复用,Java语言中一个类是无法继承多个父类的,那么原因是什么呢?这是因为继承多个问题会出现”钻石问题“,感兴趣的可自行了解,这里不做过多解释了。
继承虽然可以实现代码复用,但是过度使用继承会导致嵌套过深,代码难以阅读和维护,所以在设计原则中也会说组合方式优于继承。
多态
接着来看最后一个特性:多态。多态是许多设计模式和设计原则实现的基础,比如常用的策略模式和里式替换原则等。简单的说,多态就是子类可以替换父类,举个例子:
比如在业务中,需要提供一个方法实现设备信息打印功能,设备中类有A、B等多种,代码如下所以:
public class PrintUtil { private void print(A a){ } private void print(B b){ } }
按照一般实现方式,每增加一种设备类型,都需要在PrintUtil新增一个打印方法,且逻辑都在PrintUtil类中使得难以扩展和维护。依赖多态的特性,我们可以这样来实现,首先定义一个接口,代码如下所示:
public interface PrintInterface { void print(); }
使A、B类都继承PrintInterface接口,代码如下所示:
public class A implements PrintInterface{ @Override public void print() { System.out.println("-A设备的打印-"); } }
public class B implements PrintInterface { @Override public void print() { System.out.println("-B设备的打印-"); } }
修改PrintUtil类中的方法如下所示:
public class PrintUtil { public void print(PrintInterface printInterface) { printInterface.print(); } }
需要打印设备信息时,可直接采用如下方式:
public static void main(String[] args) { PrintUtil printUtil = new PrintUtil(); A a = new A(); printUtil.print(a); B b = new B(); printUtil.print(b); }
这样,当增加一种设备时,我们只需要将设备类继承自PrintInterface接口,并在类内部实现自己的打印规则即可,不需要改动PrintUtil中的代码,提高了代码的可扩展性。
了解了面向对象的四大特性后,接着来看你真的面向对象了吗?
你真的面向对象了吗?
与面向对象并列的是面向过程,很多时候,我们使用面向对象语言写出来的代码可能都是面向过程的,但如果想让项目中完全没有面向过程风格的代码,这一点是非常不切实际的。但了解错误的使用方式可以指导我们在以后的编码过程中写出更易理解、更易扩展的代码。
正确设计各种Util工具类
Util工具类
在Android开发中,相信每个每个项目中都有一推Util工具类,这一些工具类也常被我们认为是好用的轮子,比如经常设计的UserUtil、FileUtil、DeviceUtil,用来在不同类之间调用相同的方法。如果一个Util工具类中仅有若干静态方法没有任何属性,那么这个工具类我们完全可以称之为是面向过程的。
在设计工具类的时候,我们要尽量保持”单一职责“原则,比如一个DeviceUtil中定义了各种获取设备参数的方法也定义了和文件有关的方法,那么这个类就没有遵循单一职责原则,所以我们要尽量避免设计大而全的工具类,要按照实际功能,让类的职责尽可能的保持单一。
Config配置文件
除了Util工具类之外,Config文件也是Android开发者经常会使用到的,在组件化的开发中,我们会为每个模块配置路由文件,写出的代码可能如下所示:
public class ArouteConfig { public String AModuleMainActivity = "A/MainActivity"; public String AModuleSetActivity = "A/SetActivity"; public String BModuleMainActivity = "B/MainActivity"; public String BModuleSetActivity = "B/SetActivity"; }
ArouteConfig类中定义了A、B等module的路由配置变量,这样设计在功能实现中是完全没问题的,但是设想一下,一来 组件化的目的就是为了模块解耦开发,不同模块的负责人都会修改这个配置文件,很有可能导致冲突和难以维护,二来 如果另一个项目中同样用到了B module,这个时候我们会把B moudle和ArouteConfig类迁移到另一个项目中,如此一来,ArouteConfig中便定义了许多冗余的变量且不符合单一职责原则。
所以在设计中,我们可以考虑将配置文件拆分更细粒,分别新建AMoudleArouteConfig与BModuleArouteConfig,这样对应模块的负责人只需维护对应模块的路由配置不会导致冲突,也提高了类设计的内聚性和代码的复用性。
不要盲目的定义各种配置文件
对Android开发工程师而言,我们可能会比较排斥 将一些静态变量定义在Activity中,都会直接抽取一个配置文件,写在配置文件中,如果这些静态变量仅在某一个Activity中使用到了,那完全没有必要单独定义一个配置文件的,如果你确定需要,那就尽快去定义吧!只要适合项目需要即可。
反思使用GsonFormat随意生成get、set方法
Android开发工程师或Java开发工程师经常会使用编辑器中复写方法,给所有的变量生成get、set方法,尤其是Android开发工程师,拿到后台返回的json数据后,直接使用GsonFormat生成对应的实体类,简直不要太爽~
比如,服务器返回用户数据结构如下所示:
{ "userName":"HuangLinqing", "age":27, "birthday":"819561600" }
使用GsonFormat或编辑器快捷键自动生成的实体类如下所示:
public class User { private String userName; private int age; private String birthday; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getBirthday() { return birthday; } public void setBirthday(String birthday) { this.birthday = birthday; } }
一般情况下,这样编写也不会有什么问题。但仔细来看,这段代码显然违反了面向对象中的封装特性,这是因为出生日期、和年龄是相关联的,而出生日期和年龄都暴露了set方法,如果某个开发的同事在使用错误的情况调用了 setBirthday方法,会导致通过出生日期计算的年龄和返回年龄不符的情况。所以正确的做法是,如果给出生日期提供了对外设置的方法,那么年龄就不应该对外暴露设置的方法,且要自动计算,修改后的代码如下所示:
public class User { private String userName; private int age; private String birthday; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public String getBirthday() { return birthday; } public void setBirthday(String birthday) { this.birthday = birthday; age = (当前时间 - birthday); } }
我猜你肯定会说,谁闲着没事会设置那个方法,我们确保都不用不就行了吗?是的,没错,但团队间的协作标准需要用规范去衡量而不能以口头的保证作为依据,万一那个大废就是你自己呢?
写在最后
除了本文中所提到的,其实还有好多经常遇到却不以为意的坑。好的代码需要使用规范标准去说话,当然这里的规范只要适合你们的项目就是最好的。重学设计模式之后,请回答,你真的面向对象了吗?
题外话
就像近期在Android圈经常讨论到的,Google官方推荐的架构由MVVM变成MVI,大家就都去说MVI怎么怎么好,MVVM的缺陷是怎样的。就像MVVM刚出来时,大家对MVP的评判是一样的。在业务开发中可以这么说:只要适合项目本身,所有的架构都是值得学习和使用的!