面向对象的7大设计原则 之举例
一、面向对象的7大设计原则
- 开闭原则
- 对扩展开放,对更改关闭
- 类模块应该是可扩展的,但是不可修改。
- 里氏代换原则
- 子类必须能够替换它们的基类(IS-A)
- 继承表达类型抽象
- 迪米特原则
- 要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则(Least Knowledge Principle, LKP)
- 单一职责原则
- 一个类应该仅有一个引起它变化的原因
- 变化的方向隐含着类的责任
- 接口分离原则
- 不应该强迫客户程序依赖它们不用的方法
- 接口应该小而完备
- 依赖倒置原则
- 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定)
- 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)
- 组合/聚合复用原则
- 尽量使用组合/聚合,不要使用类继承
- 在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的
二、一句话理解七大原则
- 开闭原则
/**
* 定义课程接口
*/
public interface ICourse {
String getName(); // 获取课程名称
Double getPrice(); // 获取课程价格
Integer getType(); // 获取课程类型
}
/**
* 英语课程接口实现
*/
public class EnglishCourse implements ICourse {
private String name;
private Double price;
private Integer type;
public EnglishCourse(String name, Double price, Integer type) {
this.name = name;
this.price = price;
this.type = type;
}
@Override
public String getName() {
return null;
}
@Override
public Double getPrice() {
return null;
}
@Override
public Integer getType() {
return null;
}
}
// 测试
public class Main {
public static void main(String[] args) {
ICourse course = new EnglishCourse("小学英语", 199D, "Mr.Zhang");
System.out.println(
"课程名字:"+course.getName() + " " +
"课程价格:"+course.getPrice() + " " +
"课程作者:"+course.getAuthor()
);
}
}
如果课程要打折,有一种方法是往接口中添加方法;
public interface ICourse {
// 获取课程名称
String getName();
// 获取课程价格
Double getPrice();
// 获取课程类型
String getAuthor();
// 新增:打折接口
Double getSalePrice();
}
这就违反了开闭原则,会导致所有的其他课程都需要实现打折接口,更好的方法是用扩展代替修改;
public class SaleEnglishCourse extends EnglishCourse {
public SaleEnglishCourse(String name, Double price, String author) {
super(name, price, author);
}
@Override
public Double getPrice() {
return super.getPrice() * 0.85;
}
}
- 里氏代换原则
里氏替换原则强调的是设计和实现要依赖于抽象而非具体;子类只能去扩展基类,而不是隐藏或者覆盖基类,它包含以下4层含义- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆写或实现父类的方法时输出结果可以被缩小
- 迪米特原则
通过老师要求班长告知班级人数为例,讲解迪米特原则。先来看一下违反迪米特法则的设计,代码如下
public class Student {
private Integer id;
private String name;
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
}
public class Teacher {
public void call(Monitor monitor) {
List<Student> sts = new ArrayList<>();
for (int i = 0; i < 10; i++) {
sts.add(new Student(i + 1, "name" + i));
}
monitor.getSize(sts);
}
}
public class Monitor {
public void getSize(List list) {
System.out.println("班级人数:" + list.size());
}
}
从逻辑上讲 Teacher 只与 Monitor 耦合就行了,与 Student 并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特原则,应该避免类中出现这样非直接朋友关系的耦合。
public class Student {
private Integer id;
private String name;
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
}
public class Teacher {
public void call(Monitor monitor) {
monitor.getSize();
}
}
public class Monitor {
public void getSize() {
List<Student> sts = new ArrayList<>();
for (int i = 0; i < 10; i++) {
sts.add(new Student(i + 1, "name" + i));
}
System.out.println("班级人数" + sts.size());
}
}
- 单一职责原则
用一个类描述动物呼吸这个场景:
class Animal {
func breathe(animal: String) {
print("\(animal)呼吸空气")
}
}
let animal = Animal()
animal.breathe(animal: "牛")
animal.breathe(animal: "羊")
animal.breathe(animal: "猪")
程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将 Animal 类细分为陆生动物类 Terrestrial,水生动物 Aquatic,代码如下:
class Terrestrial {
func breathe(animal: String) {
print("\(animal)呼吸空气")
}
}
class Aquatic {
func breathe(animal: String) {
print("\(animal)呼吸水")
}
}
let terrestrial = Terrestrial()
terrestrial.breathe(animal: "牛")
terrestrial.breathe(animal: "羊")
terrestrial.breathe(animal: "猪")
let aquatic = Aquatic()
aquatic.breathe(animal: "鱼")
违反单一职责的修改:
class Animal {
func breathe(animal: String) {
if (animal == "鱼") {
print("\(animal)呼吸水")
} else {
print("\(animal)呼吸空气")
}
}
}
let animal = Animal()
animal.breathe(animal: "牛")
animal.breathe(animal: "羊")
animal.breathe(animal: "猪")
animal.breathe(animal: "鱼")
// 这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用 “猪” “牛” “羊” 等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。
- 接口分离原则
- 接口中的方法应该尽量少,不要使接口过于臃肿,不要有很多不相关的逻辑方法。
- 依赖倒置原则
场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:
class Book {
func getContent() -> String{
return "很久很久以前有一个阿拉伯的故事……"
}
}
class Mother {
func narrate(book: Book) {
print("妈妈开始讲故事");
print("\(book.getContent())")
}
}
let mother = Mother()
mother.narrate(book: Book())
运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:
class NewsPaper {
func getContent() -> String {
return "70 周年庆..."
}
}
只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:
protocol IReader {
func getContent() -> String
}
class Book: IReader {
func getContent() -> String{
return "很久很久以前有一个阿拉伯的故事……"
}
}
class NewsPaper: IReader{
func getContent() -> String {
return "70 周年庆..."
}
}
class Mother{
func narrate(reader: IReader) {
print("妈妈开始讲故事");
print("\(reader.getContent())")
}
}
let mother = Mother()
mother.narrate(reader: Book())
mother.narrate(reader: NewsPaper())
这样修改后,无论以后怎样扩展,都不需要再修改 Mother 类了。
- 组合/聚合复用原则
- 类之间有三种基本关系,分别是:关联(聚合和组合)、泛化(与继承同一概念)、依赖。
- 聚合,用来表示整体与部分的关系或者 “拥有” 关系。其中,代表部分的对象可能会被代表多个整体的对象所拥有,但是并不一定会随着整体对象的销毁而销毁,部分的生命周期可能会超越整体。好比班级和学生,班级销毁或解散后学生还是存在的,学生可以继续存在某个培训机构或步入社会,生命周期不同于班级甚至大于班级。
- 合成,用来表示一种强得多的 “拥有” 关系。其中,部分和整体的生命周期是一致的,一个合成的新的对象完全拥有对其组成部分的支配权,包括创建和泯灭。好比人的各个器官组成人一样,一旦某个器官衰竭,人也不复存在,这是一种 “强” 关联。
- 类之间有三种基本关系,分别是:关联(聚合和组合)、泛化(与继承同一概念)、依赖。