设计模式——类与对象设计的六大基本原则
本文介绍了设计模式中的六大基本原则:单一职责原则、开放封闭原则、子类替换父类原则、接口调用原则、类对象交互原则和接口隔离原则。通过具体代码示例,阐述了每个原则的核心思想和应用场景,旨在提高代码的可维护性和灵活性。
摘要
本文介绍了设计模式中的六大基本原则:单一职责原则、开放封闭原则、子类替换父类原则、接口调用原则、类对象交互原则和接口隔离原则。通过具体代码示例,阐述了每个原则的核心思想和应用场景,旨在提高代码的可维护性和灵活性。
1. 单一职责原则
1.1. 核心思想
一个类应该有且仅有一个原因引起它的变更。说人话:一个类只负责一项功能或一类相似的功能。
这句话这样说可能不太容易理解,解释一下:类 T 负责两个不同的职责(可以理解为功能):职责 P1、职责 P2。当由于职责 P1 需求发生改变而需要修改类 T 时,有可能会导致原本运行正常的职责 P2 功能发生故障。这就不符合单一职责原则,这时就应该将类 T 拆分成两个类 T1、T2,使 T1 完成职责 P1 功能,T2 完成职责 P2 功能。这样,当修改类 T1 时,不会使职责 P2 发生故障风险;同理,当修改 T2 时,也不会使职责 P1 发生故障风险。
当然这个“一”并不是绝对的,应该理解为一个类只负责尽可能独立的一项功能,尽可能少的职责。就好比一个人,我们的精力、时间都是有限的;如果我们什么事情都做,那什么事情都做不好;而应该集中精力做一件事,才能把事情做好。
1.2. 不遵守单一职责的例子
以下是一个没有遵循单一职责原则的例子:
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void calculatePay() {
System.out.println("Calculating pay for " + name);
}
public void saveToDatabase() {
System.out.println("Saving " + name + " to database");
}
}
问题:
Employee
类负责两件事情:计算薪资和保存员工信息到数据库。- 如果薪资计算规则改变或者数据库保存逻辑改变,都需要修改
Employee
类,这就导致类的职责过于复杂,增加了维护的难度。
1.3. 遵守单一职责的改进
将职责拆分为多个类,每个类只负责一个职责:
// 员工类,只负责保存员工信息
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
}
// 负责薪资计算的类
class SalaryCalculator {
public void calculatePay(Employee employee) {
System.out.println("Calculating pay for " + employee.getName());
}
}
// 负责数据持久化的类
class EmployeeRepository {
public void saveToDatabase(Employee employee) {
System.out.println("Saving " + employee.getName() + " to database");
}
}
在使用时,将职责分开:
public class Main {
public static void main(String[] args) {
Employee employee = new Employee("John", 5000);
SalaryCalculator calculator = new SalaryCalculator();
calculator.calculatePay(employee);
EmployeeRepository repository = new EmployeeRepository();
repository.saveToDatabase(employee);
}
}
2. 开放封闭原则
2.1. 核心思想
软件实体(如类、模块、函数等)应该对拓展开放,对修改关闭。
说人话:在一个软件产品的生命周期内,不可避免的会有一些业务和需求变化。我们在设计代码的时候应该尽可能地考虑这些变化,在增加一个功能时,应当尽可能地不去改动已有的代码;当修改一个模块时不应该影响到其他的模块。
2.2. 开放封闭原则例子
在一个软件产品的生命周期内,不可避免的会有一些业务和需求变化。我们在设计代码的时候应该尽可能地考虑这些变化,在增加一个功能时,应当尽可能地不去改动已有的代码;当修改一个模块时不应该影响到其他的模块。
package com.zhuangxiaoyan.hyxftest.designpattern;
import java.util.ArrayList;
import java.util.List;
/**
* Main
*
* @author xjl
* @version 2024/12/01 19:48
**/
// 抽象类 Animal
abstract class Animal implements AnimalMoving {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void moving();
}
interface AnimalMoving{
void moving();
}
// 陆生动物类 TerrestrialAnimal
class TerrestrialAnimal extends Animal {
public TerrestrialAnimal(String name) {
super(name);
}
@Override
public void moving() {
System.out.println(name + " 在陆上跑...");
}
public void checkFood(Food food) {
// 假设 food 的类别判断逻辑
if (food.getCategory().equals("植物")) {
System.out.println(name + " 可以吃这个食物");
} else {
System.out.println(name + " 不喜欢这个食物");
}
}
}
// 水生动物类 AquaticAnimal
class AquaticAnimal extends Animal {
public AquaticAnimal(String name) {
super(name);
}
@Override
public void moving() {
System.out.println(name + " 在水里游...");
}
}
// 鸟类动物类 BirdAnimal
class BirdAnimal extends Animal {
public BirdAnimal(String name) {
super(name);
}
@Override
public void moving() {
System.out.println(name + " 在天空飞...");
}
}
// 猴子类 Monkey 继承自 TerrestrialAnimal
class Monkey extends TerrestrialAnimal {
public Monkey(String name) {
super(name);
}
public void climbing() {
System.out.println(name + " 在爬树,动作灵活轻盈...");
}
}
class BigMonkey extends Monkey{
public BigMonkey(String name) {
super(name);
}
public void moving() {
System.out.println(name + "跳舞...");
}
public void climbing() {
System.out.println(name + "跳跳跳跳...");
}
}
// 食物类,用于示例
class Food {
private String category;
public Food(String category) {
this.category = category;
}
public String getCategory() {
return category;
}
}
// 动物园类 Zoo
class Zoo {
private List<Animal> animals = new ArrayList();
public void addAnimal(Animal animal) {
animals.add(animal);
}
public void displayActivity() {
System.out.println("观察每一种动物的活动方式:");
for (Animal animal : animals) {
animal.moving();
}
}
}
// 主类测试
public class Main {
public static void main(String[] args) {
Zoo zoo = new Zoo();
// 创建动物
Animal tiger = new TerrestrialAnimal("老虎");
Animal fish = new AquaticAnimal("金鱼");
Animal eagle = new BirdAnimal("老鹰");
Monkey monkey = new Monkey("猴子");
BigMonkey bigMonkey = new BigMonkey("大猩猩");
// 添加动物到动物园
zoo.addAnimal(tiger);
zoo.addAnimal(fish);
zoo.addAnimal(eagle);
zoo.addAnimal(monkey);
zoo.addAnimal(bigMonkey);
// 显示动物活动
zoo.displayActivity();
// 检查猴子是否能吃食物
Food food = new Food("植物");
monkey.checkFood(food);
monkey.climbing();
}
}
3. 子类替换父类原则
3.1. 核心思想:
所有能引用基类的地方必须能透明地使用其子类的对象。有一个类 T 有两个子类 T1、T2,能够使用 T 的对象的地方,就能用 T1 的对象或 T2 的对象,因为子类拥有父类的所有属性和行为。
说人话:只要父类能出现的地方子类就能出现(就可以用子类来替换他),反之,子类能出现的地方父类就不一定能出现(子类拥有父类的所有属性和行为,但子类拓展了更多的功能)。
3.2. 子类替换父类原则示例
package com.zhuangxiaoyan.hyxftest.designpattern;
import java.util.ArrayList;
import java.util.List;
/**
* Main
*
* @author xjl
* @version 2024/12/01 19:48
**/
// 抽象类 Animal
abstract class Animal implements AnimalMoving {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void moving();
}
interface AnimalMoving{
void moving();
}
// 陆生动物类 TerrestrialAnimal
class TerrestrialAnimal extends Animal {
public TerrestrialAnimal(String name) {
super(name);
}
@Override
public void moving() {
System.out.println(name + " 在陆上跑...");
}
public void checkFood(Food food) {
// 假设 food 的类别判断逻辑
if (food.getCategory().equals("植物")) {
System.out.println(name + " 可以吃这个食物");
} else {
System.out.println(name + " 不喜欢这个食物");
}
}
}
// 水生动物类 AquaticAnimal
class AquaticAnimal extends Animal {
public AquaticAnimal(String name) {
super(name);
}
@Override
public void moving() {
System.out.println(name + " 在水里游...");
}
}
// 鸟类动物类 BirdAnimal
class BirdAnimal extends Animal {
public BirdAnimal(String name) {
super(name);
}
@Override
public void moving() {
System.out.println(name + " 在天空飞...");
}
}
// 猴子类 Monkey 继承自 TerrestrialAnimal
class Monkey extends TerrestrialAnimal {
public Monkey(String name) {
super(name);
}
public void climbing() {
System.out.println(name + " 在爬树,动作灵活轻盈...");
}
}
class BigMonkey extends Monkey{
public BigMonkey(String name) {
super(name);
}
public void moving() {
System.out.println(name + "跳舞...");
}
public void climbing() {
System.out.println(name + "跳跳跳跳跳跳跳跳跳跳跳跳...");
}
}
// 食物类,用于示例
class Food {
private String category;
public Food(String category) {
this.category = category;
}
public String getCategory() {
return category;
}
}
// 动物园类 Zoo
class Zoo {
private List<Animal> animals = new ArrayList();
public void addAnimal(Animal animal) {
animals.add(animal);
}
public void displayActivity() {
System.out.println("观察每一种动物的活动方式:");
for (Animal animal : animals) {
animal.moving();
}
}
public void monkeyClimbing(Monkey monkey) {
monkey.climbing();
}
}
// 主类测试
public class Main {
public static void main(String[] args) {
Zoo zoo = new Zoo();
// 创建动物
Animal tiger = new TerrestrialAnimal("老虎");
Animal fish = new AquaticAnimal("金鱼");
Animal eagle = new BirdAnimal("老鹰");
Monkey monkey = new Monkey("猴子");
BigMonkey bigMonkey = new BigMonkey("大猩猩");
// 添加动物到动物园
zoo.addAnimal(tiger);
zoo.addAnimal(fish);
zoo.addAnimal(eagle);
zoo.addAnimal(monkey);
zoo.addAnimal(bigMonkey);
// 显示动物活动
zoo.displayActivity();
// 检查猴子是否能吃食物
Food food = new Food("植物");
monkey.checkFood(food);
monkey.climbing();
// 测试猴子
zoo.monkeyClimbing(monkey);
// 测试大猩猩
zoo.monkeyClimbing(bigMonkey);
}
}
4. 接口调用原则
4.1. 核心思想
高层模块就是调用端,低层模块就是具体实现类,抽象就是指接口或抽象类,细节也是指具体的实现类,也就是说我们只依赖抽象编程。
说人话:把具有相同特征或相似功能的类,抽象成接口或抽象类,让具体的实现类继承这个抽象类(或实现对应的接口)。抽象类(接口)负责定义统一的方法,实现类负责实现具体功能的实现。
4.2. 接口调用原则示例
// 定义抽象类 Animal
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
// 吃食物的方法
public void eat(Food food) {
if (checkFood(food)) {
System.out.println(name + "进食" + food.getName());
} else {
System.out.println(name + "不吃" + food.getName());
}
}
// 抽象方法:检查是否能吃某种食物
protected abstract boolean checkFood(Food food);
}
// 狗类,继承 Animal
class Dog extends Animal {
public Dog() {
super("狗");
}
@Override
protected boolean checkFood(Food food) {
return "肉类".equals(food.category());
}
}
// 燕子类,继承 Animal
class Swallow extends Animal {
public Swallow() {
super("燕子");
}
@Override
protected boolean checkFood(Food food) {
return "昆虫".equals(food.category());
}
}
// 定义抽象类 Food
abstract class Food {
protected String name;
public Food(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 抽象方法:返回食物的类别
public abstract String category();
// 抽象方法:返回食物的营养成分
public abstract String nutrient();
}
// 肉类食物,继承 Food
class Meat extends Food {
public Meat() {
super("肉");
}
@Override
public String category() {
return "肉类";
}
@Override
public String nutrient() {
return "蛋白质、脂肪";
}
}
// 虫子类食物,继承 Food
class Worm extends Food {
public Worm() {
super("虫子");
}
@Override
public String category() {
return "昆虫";
}
@Override
public String nutrient() {
return "蛋白质、微量元素";
}
}
// 测试类
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal swallow = new Swallow();
Food meat = new Meat();
Food worm = new Worm();
// 狗吃肉
dog.eat(meat); // 输出:狗进食肉
// 狗吃虫子
dog.eat(worm); // 输出:狗不吃虫子
// 燕子吃肉
swallow.eat(meat); // 输出:燕子不吃肉
// 燕子吃虫子
swallow.eat(worm); // 输出:燕子进食虫子
}
}
在上面的例子中,动物抽象出一个父类 Animal,食物也抽象出一个抽象类 Food。Animal 抽象来不依赖于细节(具体的食物类),具体的动物(如 Dog),也不依赖于细节(具体的食物类)。
5. 类对象交互原则
5.1. 核心思想:
如类 A 中有类B的对象;类 B 中有类 C 的对象,调用方有一个类 A 的对象 a,这时要访问 C 对象的属性,不要采用类似下面的写法:
a.getB().getC().getProperties()
而应该是:
a.getCProperties()
至于 getCProperties 怎么实现是类 A 要负责的事情,我只和我直接的对象 a 进行交互,不访问我不了解的对象。
说人话:一个类对自己依赖的类知道的越少越好,只需要和直接的对象进行交互,而不用在乎这个对象的内部组成结构。
大家都知道大熊猫是我们国家的国宝,为数不多的熊猫大部分都存活在动物动物园中。而动物园内种类繁多,布局复杂,如有鸟类馆、熊猫馆。假设某国外领导人来访华,参观我们的动物园,他想知道动物园内叫“贝贝”大熊猫年龄多大,体重多少。他难道要先去调取熊猫馆的信息,再去查找叫“贝贝”的这只大熊猫,再去看他的信息吗?显然不用,他只要问一下动物园的馆长就可以了。动物园的馆长会告诉他所有需要的信息,因为他只认识动物园的馆长,并不了解动物园的内部结构,也不需要去了解。
5.2. 类对象交互原则示例
zooAdmin.getPandaBeiBeiInfo()
6. 接口隔离原则
6.1. 核心思想
类 A 通过接口 interface 依赖类 C,类 B 通过接口 interface 依赖类 D,如果接口 interface 对于类 A 和类 B 来说不是最小接口,则类 C 和类 D 必须去实现他们不需要的方法。
说人话:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口尽量小,但是要有限度 。当发现一个接口过于臃肿时,就要对这个接口进行适当的拆分,但是如果过小,则会造成接口数量过多,使设计复杂化;所以一定要适度。
6.2. 接口隔离原则示例
我们知道在生物分类学中,从高到低有界、门(含亚门)、纲、目、科、属、种七个等级的分类。脊椎动物就是脊索动物的一个亚门,是万千动物世界中数量最多、结构最复杂的一个门类。哺乳动物(也称兽类)、鸟类、鱼类是脊椎动物中最重要的三个子分类;哺乳动物大都生活于陆地,鱼类都生活在水里,而鸟类大都能飞行。
但这些特性并不是绝对的,如蝙蝠是哺乳动物,但它却能飞行;鲸鱼也是哺乳动物,却生活在海中;天鹅是鸟类,能在天上飞,也能在水里游,还能在地上走。所以我们上面的示例中将动物根据活动场所分为水生动物、陆生动物和飞行动物是不够准确的,因为奔跑、游泳、飞翔是动物的一种行为,应该抽象成接口,而且有些动物可能同时具有多种行为。 我们应该根据生理特征来分类,如哺乳类、鸟类、鱼类;哺乳类动物具有恒温,胎生,哺乳等生理特征;鸟类动物具有恒温,卵生,前肢成翅等生理特征;鱼类动物具有流线型体形,用鳃呼吸等生理特征。
这里将奔跑、游泳、飞翔抽象成接口就是对接口的一种细粒度拆分,提高程序设计灵活性。代码的实现如下:
// 定义抽象类 Animal
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 生理特征
public abstract void feature();
// 活动方式
public abstract void moving();
}
// 定义接口 IRunnable:奔跑行为
interface IRunnable {
void running();
}
// 定义接口 IFlyable:飞行行为
interface IFlyable {
void flying();
}
// 定义接口 INatatory:游泳行为
interface INatatory {
void swimming();
}
// 哺乳动物类,继承 Animal,实现奔跑接口 IRunnable
class MammalAnimal extends Animal implements IRunnable {
public MammalAnimal(String name) {
super(name);
}
@Override
public void feature() {
System.out.println(name + "的生理特征:恒温,胎生,哺乳。");
}
@Override
public void running() {
System.out.println("在陆上跑...");
}
@Override
public void moving() {
System.out.print(name + "的活动方式:");
running();
}
}
// 鸟类动物类,继承 Animal,实现飞行接口 IFlyable
class BirdAnimal extends Animal implements IFlyable {
public BirdAnimal(String name) {
super(name);
}
@Override
public void feature() {
System.out.println(name + "的生理特征:恒温,卵生,前肢成翅。");
}
@Override
public void flying() {
System.out.println("在天空飞...");
}
@Override
public void moving() {
System.out.print(name + "的活动方式:");
flying();
}
}
// 鱼类动物类,继承 Animal,实现游泳接口 INatatory
class FishAnimal extends Animal implements INatatory {
public FishAnimal(String name) {
super(name);
}
@Override
public void feature() {
System.out.println(name + "的生理特征:流线型体形,用鳃呼吸。");
}
@Override
public void swimming() {
System.out.println("在水里游...");
}
@Override
public void moving() {
System.out.print(name + "的活动方式:");
swimming();
}
}
// 蝙蝠类,继承 MammalAnimal,同时实现飞行接口 IFlyable
class Bat extends MammalAnimal implements IFlyable {
public Bat(String name) {
super(name);
}
@Override
public void running() {
System.out.println("行走功能已经退化。");
}
@Override
public void flying() {
System.out.println("在天空飞...");
}
@Override
public void moving() {
System.out.print(name + "的活动方式:");
flying();
running();
}
}
// 天鹅类,继承 BirdAnimal,同时实现奔跑接口 IRunnable 和游泳接口 INatatory
class Swan extends BirdAnimal implements IRunnable, INatatory {
public Swan(String name) {
super(name);
}
@Override
public void running() {
System.out.print("在陆上跑...");
}
@Override
public void swimming() {
System.out.print("在水里游...");
}
@Override
public void moving() {
System.out.print(name + "的活动方式:");
running();
System.out.print(" ");
swimming();
System.out.print(" ");
flying();
}
}
// 鲫鱼类,继承 FishAnimal
class CrucianCarp extends FishAnimal {
public CrucianCarp(String name) {
super(name);
}
}
// 测试类
public class Main {
public static void main(String[] args) {
// 哺乳动物
MammalAnimal mammal = new MammalAnimal("老虎");
mammal.feature();
mammal.moving();
System.out.println();
// 鸟类动物
BirdAnimal bird = new BirdAnimal("麻雀");
bird.feature();
bird.moving();
System.out.println();
// 鱼类动物
FishAnimal fish = new FishAnimal("鲤鱼");
fish.feature();
fish.moving();
System.out.println();
// 蝙蝠
Bat bat = new Bat("蝙蝠");
bat.feature();
bat.moving();
System.out.println();
// 天鹅
Swan swan = new Swan("天鹅");
swan.feature();
swan.moving();
System.out.println();
// 鲫鱼
CrucianCarp crucianCarp = new CrucianCarp("鲫鱼");
crucianCarp.feature();
crucianCarp.moving();
}
}
接口的抽象行为:
IRunnable
表示奔跑行为。IFlyable
表示飞行行为。INatatory
表示游泳行为。
接口解耦动物的分类与其行为,实现灵活组合。
类的生理分类:
- 动物分类基于生理特征,
MammalAnimal
、BirdAnimal
和FishAnimal
是具体的分类。
多行为的灵活实现:
- 天鹅实现了多个行为接口(
IRunnable
和INatatory
),支持多种行为组合。 - 蝙蝠继承
MammalAnimal
并实现IFlyable
,体现其特殊性。
抽象与多态:
- 通过抽象类和接口,方法调用使用多态实现(如
moving()
方法根据子类行为动态调用)。
博文参考
《软件设计模式》
更多推荐
所有评论(0)