常见的编程范式或者说编程风格有三种,面向过程编程、面向对象编程、函数式编程,而面向对象编程又是这其中最主流的编程范式。现现在,大部分编程语言都是面向对象编程语言,大部分软件都是基于面向对象编程这种编程范式来开发的。前端
不过,在实际的开发工做中,总觉得把全部代码都塞到类里,天然就是在进行面向对象编程了。实际上,这样的认识是不正确的。有时候,从表面上看似是面向对象编程风格的代码,从本质上看倒是面向过程编程风格的。mysql
在用面向对象编程语言进行软件开发的时候,咱们有时候会写出面向过程风格的代码。有些是有意为之,并没有不妥;而有些是无心为之,会影响到代码的质量。算法
1. 滥用 getter
、setter
方法sql
它违反了面向对象编程的封装特性,至关于将面向对象编程风格退化成了面向过程编程风格。经过下面这个例子来给你解释一下这句话。编程
public class ShoppingCart {
private int itemsCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemsCount() {
return this.itemsCount;
}
public void setItemsCount(int itemsCount) {
this.itemsCount = itemsCount;
}
public double getTotalPrice() {
return this.totalPrice;
}
public void setTotalPrice(double totalPrice) {
this.totalPrice = totalPrice;
}
public List<ShoppingCartItem> getItems() {
return this.items;
}
public void addItem(ShoppingCartItem item) {
items.add(item);
itemsCount++;
totalPrice += item.getPrice();
}
// ...省略其余方法...
}复制代码
在这段代码中,ShoppingCart
是一个简化后的购物车类,有三个私有(private
)属性:itemsCount
、totalPrice
、items
。对于 itemsCount
、totalPrice
两个属性,咱们定义了它们的 getter
、setter
方法。对于 items
属性,咱们定义了它的 getter
方法和 addItem()
方法。代码很简单,理解起来不难。那你有没有发现,这段代码有什么问题呢?小程序
咱们先来看前两个属性,itemsCount
和 totalPrice
。虽然咱们将它们定义成 private
私有属性,可是提供了 public
的 getter
、setter
方法,这就跟将这两个属性定义为 public
公有属性,没有什么两样了。外部能够经过 setter
方法随意地修改这两个属性的值。除此以外,任何代码均可以随意调用 setter
方法,来从新设置 itemsCount
、totalPrice
属性的值,这也会致使其跟 items
属性的值不一致。后端
而面向对象封装的定义是:经过访问权限控制,隐藏内部数据,外部仅能经过类提供的有限的接口访问、修改内部数据。因此,暴露不该该暴露的 setter
方法,明显违反了面向对象的封装特性。数据没有访问权限控制,任何代码均可以随意修改它,代码就退化成了面向过程编程风格的了。安全
看完了前两个属性,咱们再来看 items
这个属性。对于 items
这个属性,咱们定义了它的 getter
方法和 addItem()
方法,并无定义它的 setter
方法。这样的设计貌似看起来没有什么问题,但实际上并非。前后端分离
对于 itemsCount
和 totalPrice
这两个属性来讲,定义一个 public
的 getter
方法,确实无伤大雅,毕竟 getter
方法不会修改数据。可是,对于 items
属性就不同了,这是由于 items
属性的 getter
方法,返回的是一个 List
集合容器。外部调用者在拿到这个容器以后,是能够操做容器内部数据的,也就是说,外部代码仍是能修改 items
中的数据。好比像下面这样:编程语言
ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空购物车复制代码
你可能会说,清空购物车这样的功能需求看起来合情合理啊,上面的代码没有什么不妥啊。你说得没错,需求是合理的,可是这样的代码写法,会致使 itemsCount
、totalPrice
、items
三者数据不一致。咱们不该该将清空购物车的业务逻辑暴露给上层代码。正确的作法应该是,在 ShoppingCart
类中定义一个 clear()
方法,将清空购物车的业务逻辑封装在里面,透明地给调用者使用。ShoppingCart
类的 clear()
方法的具体代码实现以下:
public class ShoppingCart {
// ...省略其余代码...
public void clear() {
items.clear();
itemsCount = 0;
totalPrice = 0.0;
}
}复制代码
你可能还会说,我有一个需求,须要查看购物车中都买了啥,那这个时候,ShoppingCart
类不得不提供 items
属性的 getter
方法了,那又该怎么办才好呢?
若是你熟悉 Java
语言,那解决这个问题的方法仍是挺简单的。咱们能够经过 Java
提供的 Collections.unmodifiableList()
方法,让 getter
方法返回一个不可被修改的 UnmodifiableList
集合容器,而这个容器类重写了 List
容器中跟修改数据相关的方法,好比 add()
、clear()
等方法。一旦咱们调用这些修改数据的方法,代码就会抛出 UnsupportedOperationException
异常,这样就避免了容器中的数据被修改。具体的代码实现以下所示。
public class ShoppingCart {
// ...省略其余代码...
public List<ShoppingCartItem> getItems() {
return Collections.unmodifiableList(this.items);
}
}
public class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
// ...省略其余代码...
}
ShoppingCart cart = new ShoppingCart();
List<ShoppingCartItem> items = cart.getItems();
items.clear();//抛出UnsupportedOperationException异常复制代码
不过,这样的实现思路仍是有点问题。由于当调用者经过 ShoppingCart
的 getItems()
获取到 items
以后,虽然咱们无法修改容器中的数据,但咱们仍然能够修改容器中每一个对象(ShoppingCartItem
)的数据。听起来有点绕,看看下面这几行代码你就明白了。
ShoppingCart cart = new ShoppingCart();
cart.add(new ShoppingCartItem(...));
List<ShoppingCartItem> items = cart.getItems();
ShoppingCartItem item = items.get(0);
item.setPrice(19.0); // 这里修改了item的价格属性复制代码
总结一下,在设计实现类的时候,除非真的须要,不然,尽可能不要给属性定义 setter
方法。除此以外,尽管 getter
方法相对 setter
方法要安全些,可是若是返回的是集合容器(好比例子中的 List
容器),也要防范集合内部数据被修改的危险。
2. 滥用全局变量和全局方法
在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。单例类对象在全局代码中只有一份,因此,它至关于一个全局变量。静态成员变量归属于类上的数据,被全部的实例化对象所共享,也至关于必定程度上的全局变量。而常量是一种很是常见的全局变量,好比一些代码中的配置参数,通常都设置为常量,放到一个 Constants
类中。静态方法通常用来操做静态变量或者外部数据。你能够联想一下咱们经常使用的各类 Utils
类,里面的方法通常都会定义成静态方法,能够在不用建立对象的状况下,直接拿来使用。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。
在刚刚介绍的这些全局变量和全局方法中,Constants
类和 Utils
类最经常使用到。如今,咱们就结合这两个几乎在每一个软件开发中都会用到的类,来深刻探讨一下全局变量和全局方法的利与弊。
咱们先来看一下,在我过去参与的项目中,一种常见的 Constants
类的定义方法。
public class Constants {
public static final String MYSQL_ADDR_KEY = "mysql_addr";
public static final String MYSQL_DB_NAME_KEY = "db_name";
public static final String MYSQL_USERNAME_KEY = "mysql_username";
public static final String MYSQL_PASSWORD_KEY = "mysql_password";
public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
public static final int REDIS_DEFAULT_MAX_IDLE = 50;
public static final int REDIS_DEFAULT_MIN_IDLE = 20;
public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
// ...省略更多的常量定义...
}复制代码
在这段代码中,咱们把程序中全部用到的常量,都集中地放到这个 Constants
类中。不过,定义一个如此大而全的 Constants
类,并非一种很好的设计思路。为何这么说呢?缘由主要有如下几点。
首先,这样的设计会影响代码的可维护性。
若是参与开发同一个项目的工程师有不少,在开发过程当中,可能都要涉及修改这个类,好比往这个类里添加常量,那这个类就会变得愈来愈大,成百上千行都有可能,查找修改某个常量也会变得比较费时,并且还会增长提交代码冲突的几率。
其次,这样的设计还会增长代码的编译时间。
当 Constants
类中包含不少常量定义的时候,依赖这个类的代码就会不少。那每次修改 Constants
类,都会致使依赖它的类文件从新编译,所以会浪费不少没必要要的编译时间。不要小看编译花费的时间,对于一个很是大的工程项目来讲,编译一次项目花费的时间多是几分钟,甚至几十分钟。而咱们在开发过程当中,每次运行单元测试,都会触发一次编译的过程,这个编译时间就有可能会影响到咱们的开发效率。
最后,这样的设计还会影响代码的复用性。
若是咱们要在另外一个项目中,复用本项目开发的某个类,而这个类又依赖 Constants
类。即使这个类只依赖 Constants
类中的一小部分常量,咱们仍然须要把整个 Constants
类也一并引入,也就引入了不少无关的常量到新的项目中。
那如何改进 Constants
类的设计呢?这里有两种思路能够借鉴。
第一种是将 Constants
类拆解为功能更加单一的多个类,好比跟 MySQL
配置相关的常量,咱们放到 MysqlConstants
类中;跟 Redis
配置相关的常量,咱们放到 RedisConstants
类中。固然,还有一种以为更好的设计思路,那就是并不单独地设计 Constants
常量类,而是哪一个类用到了某个常量,咱们就把这个常量定义到这个类中。好比,RedisConfig
类用到了 Redis
配置相关的常量,那咱们就直接将这些常量定义在 RedisConfig
中,这样也提升了类设计的内聚性和代码的复用性。
讲完了 Constants
类,咱们再来讨论一下 Utils
类。首先,想问你这样一个问题,咱们为何须要 Utils
类?Utils
类存在的意义是什么?
在讲面向对象特性的时候,讲过继承能够实现代码复用。利用继承特性,咱们把相同的属性和方法,抽取出来,定义到父类中。子类复用父类中的属性和方法,达到代码复用的目的。可是,有的时候,从业务含义上,A
类和 B
类并不必定具备继承关系,好比 Crawler
类和 PageAnalyzer
类,它们都用到了 URL
拼接和分割的功能,但并不具备继承关系(既不是父子关系,也不是兄弟关系)。仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。若是不熟悉背后设计思路的同事,发现 Crawler
类和 PageAnalyzer
类继承同一个父类,而父类中定义的倒是 URL
相关的操做,会以为这个代码写得莫名其妙,理解不了。
既然继承不能解决这个问题,咱们能够定义一个新的类,实现 URL
拼接和分割的方法。而拼接和分割两个方法,不须要共享任何数据,因此新的类不须要定义任何属性,这个时候,咱们就能够把它定义为只包含静态方法的 Utils
类了。
实际上,只包含静态方法不包含任何属性的 Utils
类,是不折不扣的面向过程的编程风格。但这并非说,咱们就要杜绝使用 Utils
类了。实际上,从刚刚讲的 Utils
类存在的目的来看,它在软件开发中仍是挺有用的,能解决代码复用问题。因此,这里并非说彻底不能用 Utils
类,而是说,要尽可能避免滥用,不要不加思考地随意去定义 Utils
类。
在定义 Utils
类以前,你要问一下本身,你真的须要单独定义这样一个 Utils
类吗?是否能够把 Utils
类中的某些方法定义到其余类中呢?若是在回答完这些问题以后,你仍是以为确实有必要去定义这样一个 Utils
类,那就大胆地去定义它吧。由于即使在面向对象编程中,咱们也并非彻底排斥面向过程风格的代码。只要它能为咱们写出好的代码贡献力量,咱们就能够适度地去使用。
除此以外,类比 Constants
类的设计,咱们设计 Utils
类的时候,最好也能细化一下,针对不一样的功能,设计不一样的 Utils
类,好比 FileUtils
、IOUtils
、StringUtils
、UrlUtils
等,不要设计一个过于大而全的 Utils
类。
3. 定义数据和方法分离的类
最后一种面向对象编程过程当中,常见的面向过程风格的代码。那就是,数据定义在一个类中,方法定义在另外一个类中。
传统的 MVC
结构分为 Model
层、Controller
层、View
层这三层。不过,在作先后端分离以后,三层结构在后端开发中,会稍微有些调整,被分为 Controller
层、Service
层、Repository
层。Controller
层负责暴露接口给前端调用,Service
层负责核心业务逻辑,Repository
层负责数据读写。而在每一层中,咱们又会定义相应的 VO
(View Object
)、BO
(Business Object
)、Entity
。通常状况下,VO
、BO
、Entity
中只会定义数据,不会定义方法,全部操做这些数据的业务逻辑都定义在对应的 Controller
类、Service
类、Repository
类中。这就是典型的面向过程的编程风格。
实际上,这种开发模式叫做基于贫血模型的开发模式,也是咱们如今很是经常使用的一种 Web
项目的开发模式。
能够联想一下,在生活中,你去完成一个任务,你通常都会思考,应该先作什么、后作什么,如何一步一步地顺序执行一系列操做,最后完成整个任务。面向过程编程风格偏偏符合人的这种流程化思惟方式。而面向对象编程风格正好相反。它是一种自底向上的思考方式。它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。咱们在上一节课讲到了,这样的思考路径比较适合复杂程序的开发,但并非特别符合人类的思考习惯。
除此以外,面向对象编程要比面向过程编程难一些。在面向对象编程中,类的设计仍是挺须要技巧,挺须要必定设计经验的。你要去思考如何封装合适的数据和方法到一个类里,如何设计类之间的关系,如何设计类之间的交互等等诸多设计问题。
因此,基于这两点缘由,不少工程师在开发的过程,更倾向于用不太须要动脑子的方式去实现需求,也就情不自禁地就将代码写成面向过程风格的了。
前面咱们有讲到,若是咱们开发的是微小程序,或者是一个数据处理相关的代码,以算法为主,数据为辅,那脚本式的面向过程的编程风格就更适合一些。固然,面向过程编程的用武之地还不止这些。实际上,面向过程编程是面向对象编程的基础,面向对象编程离不开基础的面向过程编程。为何这么说?咱们仔细想一想,类中每一个方法的实现逻辑,不就是面向过程风格的代码吗?
除此以外,面向对象和面向过程两种编程风格,也并非非黑即白、彻底对立的。在用面向对象编程语言开发的软件中,面向过程风格的代码并很多见,甚至在一些标准的开发库(好比 JDK
、Apache Commons
、Google Guava
)中,也有不少面向过程风格的代码。
无论使用面向过程仍是面向对象哪一种风格来写代码,咱们最终的目的仍是写出易维护、易读、易复用、易扩展的高质量代码。只要咱们能避免面向过程编程风格的一些弊端,控制好它的反作用,在掌控范围内为咱们所用,咱们就大可不用避讳在面向对象编程中写面向过程风格的代码。
1. 滥用 getter
、setter
方法
在设计实现类的时候,除非真的须要,不然尽可能不要给属性定义 setter
方法。除此以外,尽管 getter
方法相对 setter
方法要安全些,可是若是返回的是集合容器,那也要防范集合内部数据被修改的风险。
2.Constants
类、Utils
类的设计问题
对于这两种类的设计,咱们尽可能能作到职责单一,定义一些细化的小类,好比 RedisConstants
、FileUtils
,而不是定义一个大而全的 Constants
类、Utils
类。除此以外,若是能将这些类中的属性和方法,划分归并到其余业务类中,那是最好不过的了,能极大地提升类的内聚性和代码的可复用性。
讲了为何这种开发模式是不折不扣的面向过程编程风格的。这是由于数据和操做是分开定义在 VO/BO/Entity
和 Controler/Service/Repository
中的。
本文由博客一文多发平台 OpenWrite 发布!
更多内容请点击个人博客沐晨