面向对象编程¶
面向对象编程的英文全称是Object Oriented Programming,它是一种编程范式或者说编程风格。具体来说,就是以类或对象作为组织代码的基本单元,并将封装、继承、抽象、多态这四种特性作为代码设计和实现的基石。
四大特性¶
封装¶
封装是对信息的隐藏,类通过暴露有限的接口,授权外部只能通过类本身提供的方式来访问或者修改数据。
例如,我们要做一个钱包系统,它的基础代码可能是这样的:
public class Wallet {
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime;
public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
对于钱包的四个属性,从业务角度讲,id和createTime应该是创建钱包的时候就确定好不会变的,所以只在构造函数中确定下来,且没有相应的方法去修改它们。而对于余额balance属性,只能增或者减,而不会重新设置,所以我们提供了increaseBalance和decreaseBalance方法。对于balanceLastModifiedTime属性,只有在余额增减时,它才会变动,所以也是绑定在余额属性修改的方法中的。
这段代码用Java实现,能很好的利用语言提供的private、public关键字来达到访问权限控制的目的。而对于Python等语言,就没办法完全意义上的控制访问权限了。
如果我们对类中的属性不加以访问控制,那么外部的所有代码都可以访问、修改这些属性。这样看起来虽然更灵活,但过度灵活的代价就是代码不可控。属性被随意修改并且隐蔽在各个代码的角落,必然会影响代码的可读性和可维护性。
抽象¶
抽象是对方法的具体实现的隐藏,让调用者只需要关心方法做了什么事情,而不需要关心是怎么做的。面向对象语言中,通常使用接口(如java中的interface)或抽象类来实现抽象特性。
实际上,抽象这个概念是很通用的设计思想,在面对复杂系统的时候,人脑能承受的信息复杂度是有限的,因此我们需要过滤掉一些非关键的实现细节,抽象的设计思想能很好的帮助我们。所以抽象有时也会被排除在面向对象的特性之外,正是因为它几乎应用在所有的编程场景下,函数本身就是一种抽象。
继承¶
继承用来表示类之间is-a的关系,分为单继承和多继承,它最大的好处就是代码复用。假设两个类有一些相同的属性或方法,我们可以考虑把这些共有的东西提取到父类中,并让这两个类继承父类。
但是,过度的使用继承也会导致代码的可读性、可维护性变差。过多的代码层级,导致查找一个属性时可能要不断的追查其父类;子类和父类的高度耦合,导致修改父类时可能影响到子类等都是问题。所以也有少用继承、多用组合的说法。
多态¶
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。
而对于多态的实现,有多种方式,常见的有继承加方法重写、接口类、ducking-type三种。
public class Animal {
public void eat() {
System.out.println("动物吃饭");
}
}
public class Cat extends Animal {
public void eat() {
System.out.println("猫吃饭");
}
}
public static void main(){
Animal am = new Cat();
am.eat();
}
public interface Iterator {
String hasNext();
String next();
}
public class Array implements Iterator {
private String[] data;
public String hasNext() { ... }
public String next() { ... }
}
public class LinkedList implements Iterator {
private LinkedListNode head;
public String hasNext() { ... }
public String next() { ... }
}
public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}
class Logger:
def record(self):
print("I write a log into file.")
class DB:
def record(self):
print("I insert data into db.")
def test(recorder):
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)
多态特性主要能提升代码的可扩展性和复用性,例如Iterator这个例子,我们可以不改变原有代码的情况下,添加其他的数据结构像HashMap等,也同样能够传入print()函数。如果不使用多态特性,我们可能要定义print(Array array)、print(LinkedList linkedList)等。
SOLID¶
随着面向对象编程的发展,人们总结出了很多设计原则,其中最出名的当属SOLID,它代表五条原则。如果遵循这5条设计原则,就更可能写出可扩展、易于修改的代码。相反,如果不断违反其中的一条或多条原则,那么很快你的代码就会变得不可扩展、难以维护。
单一职责¶
即Single Responsibility Principle,一个类或者模块只负责完成一个职责(或功能)。这个原则看似不太难用,但在实际情况中,往往很难判断一个类的职责是否是单一的。比如在一个社交产品中,我们用一个UserInfo类来记录用户的信息:
public class UserInfo{
private long userId;
private String username;
private String email;
private String telephone;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 区
private String detailedAddress; // 详细地址
// ... 省略其他属性和方法...
}
如果违反单一职责,有什么坏处呢?假设某个类包含了10多个功能点,那意味着维护成本的增高,我们可能经常去因为各种原因修改它,导致不同的功能之间相互影响,此外,也不利于代码的复用。
那是否为了满足单一职责原则,把类拆的越细越好?例如当前有一个Serialization类实现一个协议的序列化和反序列化功能:
/**
* Protocol format: identifier-string;{gson string}
* For example: UEUEUE;{"a":"A","b":"B"}
*/
public class Serialization {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Serialization() {
this.gson = new Gson();
}
public String serialize(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
public Map<String, String> deserialize(String text) {
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}
public class Serializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Serializer() {
this.gson = new Gson();
}
public String serialize(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
}
public class Deserializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Deserializer() {
this.gson = new Gson();
}
public Map<String, String> deserialize(String text) {
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}
开闭原则¶
即Open Closed Principle,表示对扩展开放、对修改关闭,当我们添加一个功能的时候,应该在已有代码的基础上扩展(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
比如我们在写一个爬虫的小程序,用户对爬取结果中关于github的内容感兴趣,因此代码变成了这样:
class HNTopPostsSpider:
# <... 已省略 ...>
def fetch(self) -> Generator[Post, None, None]:
# <... 已省略 ...>
for item in items:
# <... 已省略 ...>
link = node_title.get('href')
# 只关注来自 github.com 的内容
if 'github' in link.lower():
yield Post(... ...)
if 'github' in link.lower()
这段,这显然是违反开闭原则的。那么如何修改,有这样几种思路:
利用继承修改
使用这种方法的关键点在于,找到父类中会变动的部分,将其抽象成新的方法或属性,最终允许子类重写来改变这些行为。
class HNTopPostsSpider:
# <... 已省略 ...>
def fetch(self) -> Generator[Post, None, None]:
# <... 已省略 ...>
for item in items:
# <... 已省略 ...>
post = Post( ... ... )
# 使用测试方法来判断是否返回该帖子
if self.interested_in_post(post):
yield post
def interested_in_post(self, post: Post) -> bool:
return True
class GithubOnlyHNTopPostsSpider(HNTopPostsSpider):
def interested_in_post(self, post: Post) -> bool:
return 'github' in post.link.lower()
interested_in_post
方法中的内容即可,而不需要修改类本身。
利用组合与依赖注入修改
另一种方法是通过依赖注入,在我们对类实例化的时候,通过参数传入一个帖子过滤的算法到实例中。
class PostFilter(metaclass=ABCMeta):
@abstractmethod
def validate(self, post: Post) -> bool:
"""判断帖子是否应该被保留"""
class DefaultPostFilter(PostFilter):
def validate(self, post: Post) -> bool:
return True
class HNTopPostsSpider:
def __init__(self, post_filter: Optional[PostFilter] = None):
self.post_filter = post_filter or DefaultPostFilter()
def fetch(self) -> Generator[Post, None, None]:
# <... 已省略 ...>
for item in items:
# <... 已省略 ...>
post = Post( ... ... )
# 使用测试方法来判断是否返回该帖子
if self.post_filter.validate(post):
yield post
class GithubTwitterFilter(PostFilter):
def validate(self, post: Post) -> bool:
if 'github' in post.link.lower() or 'twitter' in post.link.lower():
return True
return False
def main():
crawler = HNTopPostsSpider(post_filter=GithubTwitterFilter())
crawler.fetch()
使用数据驱动来修改
这种方式的核心思想在于,将经常变动的东西,以数据的方式抽离出来,需求变动时只去改动数据。和依赖注入很像,只不过依赖注入传入的是类,这个传入的是数据。
class HNTopPostsSpider:
def __init__(self,
filter_by_link_keywords: Optional[List[str]] = None):
self.filter_by_link_keywords = filter_by_link_keywords
def fetch(self) -> Generator[Post, None, None]:
# <... 已省略 ...>
for item in items:
# <... 已省略 ...>
post = Post( ... ... )
if self.filter_by_link_keywords is None:
yield post
# 当 link 中出现任意一个关键词时,返回结果
elif any(keyword in post.link for keyword in self.filter_by_link_keywords):
yield post
def main():
# link_keywords = None
link_keywords = [
'github.com',
'twitter.com'
]
crawler = HNTopPostsSpider(filter_by_link_keywords=link_keywords)
crawler.fetch()
.com
结尾的过滤条件,这种方式就无能为力了。
里氏替换¶
里氏替换原则是一条非常具体的、和继承有关的原则,是用它的发明者Liskov命名的,具体指在你使用继承时,子类对象应该能够替代父类对象的使用,还不破坏程序本身的功能。
我们来看一个具体的例子:
class User(Model):
def __init__(self, username: str):
self.username = username
def deactivate(self):
# 停用当前用户
self.is_active = False
self.save()
class Admin(User):
def deactivate(self):
# 管理员用户不允许被停用
raise RuntimeError('admin can not be deactivated!')
如何去修改呢?如果我们在deactive_users函数中添加if isinstance(user, Admin)
这样的判断显然是不合适的,未来如果有更多的需要不允许被停用的User子类,例如VIP、公司员工等,又需要修改该函数,这就违反了开闭原则。我们可以这样修改:
class User(Model):
def __init__(self, username: str):
self.username = username
def allow_deactivate(self) -> bool:
# 是否允许被停用
return True
def deactivate(self):
self.is_active = True
self.save()
class Admin(User):
def allow_deactivate(self) -> bool:
# 管理员用户不允许被停用
return False
def deactivate_users(users: Iterable[User]):
for user in users:
if not user.allow_deactivate():
logger.info(f'user {user.username} does not allow deactivating, skip.')
continue
user.deactivate()