一、接口类和抽象类的共性和区别(什么时候用接口,什么时候用抽象类)
先说一些我们可能比较熟知的,抽象类和接口类的特性。
1、抽象类和接口类的特性和共性
抽象类的特性
a.抽象类不能被实例化,只能被继承。
b. 抽象类可以有属性和方法,方法可以有具体实现或者没有,没有具体实现的方法叫抽象方法。抽象类可以没有抽象方法,但抽象方法必须在抽象类(或接口类)中才能存在。
c. 子类继承抽象类,必须实现抽象类中的所有抽象方法。
接口类的特性
a. 接口不能包含属性;
b. 接口只能声明方法,方法不能包含代码实现;
c. 类实现接口的时候,必须实现接口中声明的所有方法。
接口类和抽象类的共性是可以作为一种规范强制要求其下的实现类或子类实现具体的某些方法,而这是普通的父类不具有的能力,这一特性是通过定义抽象方法做到的。
规范方法的好处是让多态特性更加稳定和不容易出错。举个例子:
// PHP实现
class Product{ // 产品类
protected $name;
protected $price;
public function __construct($name, $price)
{
$this->price = $price;
$this->name = $name;
}
// ...产品类的其他方法省略,Product类没有实现culcalPrice方法
}
class FoodProduct extends Product{ // 食物产品类
// ...其他方法省略
public function culcalPrice(){
// 食物产品的价格计算逻辑
$price = $this->price * 0.9;
return $price;
}
}
class MachineProduct extends Product{ // 家电产品类
// ...其他方法省略
public function culcalPrice(){
$price = $this->price * 0.95;
return $price;
}
}
class ProductService{ // 产品相关的业务逻辑类
public function formatProductInfo(Product $product){ // 格式化产品相关的数据
// ...其他逻辑
$price = $product->culcalPrice();
echo $price;
}
}
$food = new FoodProduct("面包1", 10);
$service = new ProductService();
$service->formatProductInfo($food);
现在有产品、食物 和 家电 三个产品类,后二者继承自产品类。产品都有计算价格 calculPrice() 方法,且不同类型产品计算价格的逻辑不同,因此我们可以在食物和家电类实现具体的calculPrice()从而实现多态特性。在产品业务类的格式化产品方法中调用calculPrice方法。
然而,这个示例虽然实现了多态,但有2个缺陷,这两个缺陷让Product的多态性具有不稳定性。
a. 如果我扩展一个化妆品类,但是忘记为其实现calculPrice()方法。那么formatProductInfo()内如果是计算化妆品的价格就会报错。
b. 如果有个产品被一个新同事用Product类实例化,那么formatProductInfo()内也会报错,因为Product类并没有culcalPrice()。
抽象类和接口类的抽象方法强制方法规范化可以解决问题1;抽象类和接口类的不可实例化特性可以解决问题2。因此我们说规范方法的好处是让多态特性更加稳定和不容易出错。如下所示:
abstract class Product{ // 产品类
protected $name;
protected $price;
public function __construct($name, $price)
{
$this->price = $price;
$this->name = $name;
}
abstract public function culcalPrice();
// ...产品类的其他方法省略
}
2、接口类和抽象类的设计理念
接下来再说些我们不太知道或者说平时不太注意到的地方:抽象类和接口类的设计角度和用途。
可能初学者会有个误区(至少我之前就有),认为接口类和抽象类都有指定类的方法规范的作用,因为接口类或者抽象类都要求继承(或实现)自他们的子类必须定义和实现他们的抽象类。因此大家会认为如果要要求 多个类都必须实现某个或某些方法的时候,就可以用抽象类 或者 接口类。
于是乎,抽象类和接口类在用途上貌似就没区别,只不过抽象类由于还可以拥有做了具体实现的公共方法和属性,相比于接口类更灵活且兼具复用性,因此更多情况下我们会使用抽象类而非接口类。其实在多数的业务场景实现上这没什么不妥,但从抽象类和接口类的理念上,“抽象类和接口类在用途上没啥区别”或者说“用抽象类或者接口类都一样”这种想法是不对的。
实际上,接口类设计出来是为了描述一种has-a关系,抽象类则描述一种is-a关系。如果你希望描述类与类间是一种has-a关系就用接口类,如果描述is-a关系就用抽象类,这是如何决定该用抽象类还是接口的准则。
举个例子,例如 FileLogger(文件日志类) 和 MessageQueueLogger(消息队列日志类) 继承 Logger 日志抽象类,Logger 包含 read 和 write 抽象方法。因为FileLogger 和 MessageQueueLogger 都属于“日志类”这一大的范畴,因此是一种is-a关系,用抽象类描述三者之间的关系就很恰当。
我们再看一个例子,FileLogger(文件日志类)有 read / write 方法,BufferPool类(缓冲池类,用于IO操作是暂存累积要读写的数据)也有read/write方法。但你不能为了规范 BufferPool类让它必须实现 read / write 方法而让它继承自 Logger 抽象类,因为BufferPool不是一个日志类,而是一个缓存类,BufferPool和Logger之间没有 is-a 关系。
这就好像你不能说喜鹊会飞,乌鸦也会飞,飞机也会飞,而喜鹊和乌鸦都是鸟,所以飞机也应该是鸟一样。
此时我们可以让 Logger 类 和 BufferPool类实现(implement)一个 ReadWriteAble 接口,来描述他们都有 read 和 write 这两种行为,这是所谓的has-a关系,描述类和类之间有相同的行为(即相同的方法)。
3、抽象类和接口类能带来什么好处
相比于接口类而言,抽象类可继承,因此他能解决代码复用的问题,而接口则不善于代码复用。
而接口更侧重于解耦,继承会让父子之间发生耦合关系(is-a关系),子类要与父类关联。而接口则不要求它的实现类和任何其他类有关,只要它有接口指定的行为就行。
4、如何模拟抽象类和接口类
有些语言是不支持抽象类或者接口类的,我们需要人为模拟他们。模拟抽象类和接口类本质是如果模拟抽象方法。方法很多,这里我说一种最简单的,就是在父类或接口类的方法中抛出一个“必须实现”的异常。
这里以python为例,他就是一种不支持抽象类和接口类的语言。
# Python实现
class Test:
def run(params):
raise NotImplementedError
这么一来如果子类不重新实现run方法,就会抛出异常。
二、面向对象编程的避坑点
1、不要滥用 getter、setter 方法
什么是getter、setter方法?当我们定义一个私有属性,外部是无法访问和修改这个属性,但是我们可以通过定义getter方法返回这个私有属性,通过定义setter方法让外部调用它来修改私有属性的值。类似下面的例子:
<?php
class ItemBox {
private $box=[];
public function pushItem($item){
$itemId = uniqid();
$this->box[$itemId] = $item;
return $itemId;
}
public function removeItem($itemId){
unset($this->box[$itemId]);
}
}
class ShoppingCart {
private $itemsCount;
private $totalPrice;
private $items;
public function __construct()
{
$this->items = new ItemBox();
}
public function getItemsCount() { // $this->itemsCount的getter方法
return $this->itemsCount;
}
public function setItemsCount( $itemsCount) { // $this->itemsCount的setter方法
$this->itemsCount = $itemsCount;
}
public function getTotalPrice() {
return $this->totalPrice;
}
public function setTotalPrice( $totalPrice) {
$this->totalPrice = $totalPrice;
}
public function getItems() {
return $this->items;
}
public function addItem($item) {
$this->items->pushItem($item);
$this->itemsCount++;
$this->totalPrice += $item.getPrice();
}
// ...省略其他方法...
}
$cart = new ShoppingCart();
$items = $cart->getItems();
$items->pushItem("item1");
var_dump($cart->getItems()); // 你会发现在外部修改了$items后,会影响到$cart->items
有了getter方法和setter方法之后,外部就可以很方便的访问或者获取私有属性的值了,很灵活是吧。但是这样做其实违背了面向对象的封装性。
有人会问,违背了就违背了,会怎么样么。你想想看,上面的代码会有什么问题?
a. setter的问题:
$itemsCount、$totalPrice 和 $items 这3个属性是相关联的,从逻辑上说一个属性的改变会引起其他两个属性的改变(例如 往$items加入一个商品,必然会引起产品数量$itemsCount和总价$totalPrice的增加)。
如果我们开放setter方法,调用者用setter只修改了其中一个属性,就会导致数据不一致。
b. getter的问题:
getTotalPrice 和 getItemsCount 这两个getter方法没有问题,但是 getItems() 方法就会有问题,因为该方法返回的属性值是一个对象引用,因此我们如果对 getItems() 的返回值作出改变,会同步影响到 $cart对象内的 $items 属性的内容,从而导致与 $itemsCount、$totalPrice 的数据不一致。
不用getter的话,如果我们要遍历 $cart->items 属性的所有商品怎么办?
简单,在 ShoppingCart 中定义一个getAllGoods() 方法,将所有商品返回给外部,而不是将 $cart->items返回给外部即可。
总结下:
2、慎用静态类、全局变量和静态方法
在面向对象中,常见的全局变量有单例类对象、静态成员变量、常量。常见的全局方法有静态方法。最常见的静态类有放置常量的Constants 类和放置公共方法的 Utils 类。
<?php
class Constants {
public static MYSQL_ADDR_KEY = "mysql_addr";
public static MYSQL_DB_NAME_KEY = "db_name";
public static MYSQL_USERNAME_KEY = "mysql_username";
public static MYSQL_PASSWORD_KEY = "mysql_password";
public static REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
public static REDIS_DEFAULT_MAX_TOTAL = 50;
public static REDIS_DEFAULT_MAX_IDLE = 50;
public static REDIS_DEFAULT_MIN_IDLE = 20;
public static REDIS_DEFAULT_KEY_PREFIX = "rt:";
// ...省略更多的常量定义...
}
首先不是说不能使用 Constants 类 和 Utils 类。而是不能把项目中所有的常量都塞到一个 Constants 类 中,不能把项目中所有的公共方法都塞到 Utils 类中,否则会有以下坏处:
a. 影响代码的可维护性和可读性
往Constants类里不断添加常量,会导致这个类就变得越来越大,查找或修改某个常量也会变得比较费时(影响可读性),还会增加提交代码冲突的概率(影响可维护性)。
b. 增加代码的编译时间
Constants类包含项目所有模块所需的常量的话,意味着所有模块的很多文件都需要引入这个Constants类。每次修改 Constants 类,都会导致依赖它的类文件重新编译,浪费很多编译时间。对于测试效率而言是个致命打击。
c. 可能导致循环引用
一旦Utils类中包含的公共方法多了,就意味着Utils需要引入其他的类,而假设其他的类也需要使用Utils中的公共方法,就容易导致循环引用。这是我亲身经历的,我的Utils类中有个 getThreadRedisCli 方法用来获取当前进程中唯一的redis连接,该方法是通过全局容器来生成和获取redis连接的,因此Utils类需要引入一个Container容器类。
然而 Container类 中需要用到一个字符串处理的公共方法,这个方法也是放在Utils中的,因此Container类也引入 Utils类,于是导致循环引用。
如果是PHP,它自带特殊机制可以允许你这样循环引用类而不报错。但我当时使用python来写的,于是引发了一个循环引用的异常。
如何优化 Constants 和 Utils 类?两个方法:
a. 根据业务或具体模块功能对Constants和Utils类拆分,将大的静态类拆分成小的静态类。
比如跟 MySQL 配置相关的常量,我们放到 MysqlConstants 类中;跟 Redis 配置相关的常量,我们放到 RedisConstants 类中。
比如和url处理相关的公共方法放到 UrlUtils,和文件下载相关的公共方法放到 DownloadUtils。
也就是说,我们要精而美,不要大而全。
b. 将常量内聚到对应的业务类中
意思是,那个类用到了这个常量就将这个常量直接定义在这个类中,而不是将常量放到一个Constants类中。
比如,我有个订单业务处理类 OrderService,会用到订单状态相应的常量,那我就将常量定义到OrderService中,而非OrderConstants。
这样能提升类的内聚性和代码的复用性。
对于Utils类而言就不适合了,我们总不能说把公共方法直接定义在要用到这个公共方法的业务类中,这样复用性等于没有。
3、方法名或者公共类的类名尽可能抽象化以方便扩展,粒度较细的子类或实现类的类名尽可能具象化,满足单一职责。
公共的方法名尽可能抽象化请看上一篇文章阿里云和七牛云的例子。
面向对象和设计模式(二) 面向对象四大特性之封装性/抽象性/继承性/多态性 和 类与类之间6种交互关系
粒度较细的子类或实现类尽可能具象化其好处是可读性强,而且即使其下的方法名抽象,但只要类名具象了,你也能在不读代码的情况下大致知道这个方法会做些啥。