编写代码像屎,用英雄联盟例子学习设计模式原则
java设计原则,有很多小伙伴都不知道这个学了可以干什么,我现在来详细解释一下:
比如说在学习或者工作中明明已经用代码实现了功能但是还是会给批评
”你这代码写的和一坨屎一样“。
为什么呢?因为你的代码太耦合了。可是什么是耦合呢?
从耦合度高到低有这些种类:
1. 内容耦合 (Content Coupling)
最高耦合。一个模块直接访问或修改另一个模块的内部数据或代码。这是最糟糕的耦合形式,破坏了模块的封装性,导致修改一个模块会影响另一个模块。
class ModuleA {
public int data = 10;
}
class ModuleB {
public void modifyModuleA(ModuleA a) {
// ModuleB 直接修改了 ModuleA 的内部数据
a.data = 20;
}
}
2. 公共耦合 (Common Coupling)
多个模块共享全局变量。当任何模块修改全局数据时,其他所有模块都会受到影响。这种耦合也非常高,因为所有使用全局变量的模块都依赖于这个共享状态。
class Global {
public static int sharedData = 10;
}
class ModuleA {
public void updateData() {
// 修改全局变量
Global.sharedData = 20;
}
}
class ModuleB {
public void printData() {
// 访问全局变量
System.out.println(Global.sharedData);
}
}
3. 外部耦合 (External Coupling)
模块依赖于外部的接口、硬件或外部系统。这种耦合通常存在于与操作系统、数据库或外部API的交互中,外部系统的变化可能影响模块的行为。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
class DatabaseModule {
public void connect() {
try {
// 依赖外部数据库系统
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
System.out.println("连接成功");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4. 控制耦合 (Control Coupling)
一个模块通过参数传递来控制另一个模块的执行流,例如通过传递布尔值或标志位来影响另一个模块的行为。这种耦合限制了模块的独立性,因为一个模块必须了解另一个模块的内部逻辑。
class ModuleA {
public void process(boolean flag) {
if (flag) {
System.out.println("执行第一种操作");
} else {
System.out.println("执行第二种操作");
}
}
}
class ModuleB {
public void callProcess() {
// 通过传递控制参数影响 ModuleA 的行为
ModuleA a = new ModuleA();
a.process(true); // 控制耦合
}
}
5. 标记耦合 (Stamp Coupling)
模块之间传递复杂的数据结构(如对象或结构体),而不是简单的原始数据类型。传递数据结构中的多余信息会导致依赖过多的数据细节。
class Data {
public int x;
public int y;
public int z;
}
class ModuleA {
public void process(Data data) {
// 修改传递进来的对象内容
data.x = data.x * 2;
data.y = data.y + 10;
System.out.println("数据已修改: x=" + data.x + ", y=" + data.y);
}
}
class ModuleB {
public void callProcess() {
Data data = new Data();
data.x = 10;
data.y = 20;
ModuleA a = new ModuleA();
a.process(data); // 标记耦合,传递复杂数据结构
}
}
public class Main {
public static void main(String[] args) {
ModuleB b = new ModuleB();
b.callProcess();
}
}
6. 数据耦合 (Data Coupling)
模块通过参数传递进行交互,但这些参数仅仅是数据,且没有影响另一个模块的执行逻辑。数据耦合是一种较低的耦合形式,模块之间仅共享数据,而不会干扰彼此的行为。
class ModuleA {
public void calculate(int value) {
System.out.println("计算结果:" + (value * 2));
}
}
class ModuleB {
public void callCalculate() {
ModuleA a = new ModuleA();
// 仅通过数据进行交互
a.calculate(5);
}
}
有人就会说啦,这个标记耦合和数据耦合不是一样的吗?
其实标记耦合是传递一整个对象为形参,数据耦合是将基本类型参数作为形参,这个区别会导致两点不同:
1.标记耦合传递对象时可能会将无用的信息传递到另外一个类,但是数据耦合传递基本类型参数就不会比如例子里面的z参数就是无用的
2.标记耦合传递对象的时候在另外一个类里修改形参里的数据会导致实参数据变化,而数据耦合传递基本类型对象不会修改到实参的值
7. 无耦合 (No Coupling)
最低耦合。模块之间完全没有依赖或交互,它们可以独立工作并没有任何直接的关系。此时模块的独立性最高,最容易维护和复用。
class ModuleA {
public void display() {
System.out.println("ModuleA 独立运行");
}
}
class ModuleB {
public void execute() {
System.out.println("ModuleB 独立运行");
}
}
public class Main {
public static void main(String[] args) {
ModuleA a = new ModuleA();
ModuleB b = new ModuleB();
a.display(); // 无耦合
b.execute(); // 无耦合
}
}
说人话就是当已经写好的功能代码,当需求改变的时候代码的改动量,改动难度越大耦合度越高,改动难度越小耦合度越低。还有就是已经写好的代码能够很好的在其他地方复用那么耦合度越低,反之越高
那么是不是耦合度越低越好呢?
并不是的耦合度和代码的复杂度呈正相关,会导致系统架构过于复杂,系统开销过大(因为间接调用开销,和反射的开销)
设计模式六大原则
好了,我们已经介绍完什么样子的代码耦合度高,什么样子的代码耦合度低,其实有一种原则,我们只要尽量遵守这些原则就可以避免写出屎山代码
开闭原则
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
开闭原则的目标是增强系统的灵活性和可维护性。通过遵循这一原则,代码变得更加容易维护,减少了修改已存在代码时可能引入的错误风险,并且使得功能扩展变得更方便。
实现方式:我们需要使用抽象和多态来完成这个原则
抽象(如使用接口或抽象类):通过定义抽象来隔离实现细节,使得可以轻松扩展新的实现。
多态性:使用继承和多态来扩展类的行为,而不需要修改原有类。
举个例子如果我们不遵守开闭原则的代码
遵守前
class Garen {
public void showSkill() {
System.out.println("盖伦的技能:决胜时刻");
}
}
class Ahri {
public void showSkill() {
System.out.println("阿狸的技能:魅惑");
}
}
class HeroSkillDisplay {
public void displaySkill(Object hero) {
if (hero instanceof Garen) {
((Garen) hero).showSkill();
} else if (hero instanceof Ahri) {
((Ahri) hero).showSkill();
}
}
}
//这时候我们运行主方法
public class Main {
public static void main(String[] args) {
HeroSkillDisplay heroSkillDisplay = new HeroSkillDisplay();
Garen garen = new Garen();
heroSkillDisplay.displaySkill(garen);
}
}
这时候需要添加一个盲僧李青的类我们就需要修改和增加代码
class LeeSin {
public void showSkill() {
System.out.println("盲僧的技能:神龙摆尾");
}
}
class HeroSkillDisplay {
public void displaySkill(Object hero) {
if (hero instanceof Garen) {
((Garen) hero).showSkill();
} else if (hero instanceof Ahri) {
((Ahri) hero).showSkill();
} else if (hero instanceof LeeSin) {
((LeeSin) hero).showSkill();
}
}
}
遵守后
但是我们利用多态将英雄类抽象成一个接口,这样子增加英雄就不需要修改原有的代码了
// 定义一个抽象接口,所有英雄实现自己的技能展示
interface Hero {
void showSkill();
}
class Garen implements Hero {
public void showSkill() {
System.out.println("盖伦的技能:决胜时刻");
}
}
class Ahri implements Hero {
public void showSkill() {
System.out.println("阿狸的技能:魅惑");
}
}
// 英雄展示类依赖抽象接口
class HeroSkillDisplay {
public void displaySkill(Hero hero) {
hero.showSkill(); // 调用接口方法,不关心具体英雄
}
}
// 新增的盲僧类实现接口
class LeeSin implements Hero {
public void showSkill() {
System.out.println("盲僧的技能:神龙摆尾");
}
}
public class Main {
public static void main(String[] args) {
HeroSkillDisplay display = new HeroSkillDisplay();
// 展示盖伦的技能
Hero garen = new Garen();
display.displaySkill(garen);
// 展示阿狸的技能
Hero ahri = new Ahri();
display.displaySkill(ahri);
// 新增盲僧的技能展示,不需要修改原有代码
Hero leeSin = new LeeSin();
display.displaySkill(leeSin);
}
}
这样子之后需要新加的英雄再也不需要修改已经写好的代码了
单一职责链原则
一个类应该只有一个引起它变化的原因
一个类应该仅负责一件事情。如果一个类承担了过多的职责,那么这些职责之间往往会耦合在一起,导致类的功能过于复杂,难以维护和扩展。
如果有多个职责聚集在一个类中,当其中一个职责发生变化时,可能会影响到类的其他职责。这违反了高内聚、低耦合的设计原则。
实现方式:需要将原有负责很多业务逻辑的类拆分
举个例子
遵守前
class HeroManager {
private String name;
private int health;
private int attackPower;
public HeroManager(String name, int health, int attackPower) {
this.name = name;
this.health = health;
this.attackPower = attackPower;
}
// 管理英雄基本信息
public void updateHeroInfo(String name, int health, int attackPower) {
this.name = name;
this.health = health;
this.attackPower = attackPower;
}
// 处理战斗逻辑
public void attack(HeroManager target) {
System.out.println(name + " attacks " + target.name);
target.health -= attackPower;
}
// 打印英雄信息
public void printInfo() {
System.out.println("Hero: " + name + ", Health: " + health + ", Attack Power: " + attackPower);
}
}
这个类不仅负责英雄基本信息管理还处理战斗逻辑,这样子如果我们新增一个记录英雄战斗日志的逻辑就又需要在这个类里面添加方法,使得这个类非常的臃肿导致后期维护非常不方便
遵守后
// 负责管理英雄基本信息的类
class Hero {
private String name;
private int health;
private int attackPower;
public Hero(String name, int health, int attackPower) {
this.name = name;
this.health = health;
this.attackPower = attackPower;
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
public int getAttackPower() {
return attackPower;
}
public void updateHeroInfo(String name, int health, int attackPower) {
this.name = name;
this.health = health;
this.attackPower = attackPower;
}
public void printInfo() {
System.out.println("Hero: " + name + ", Health: " + health + ", Attack Power: " + attackPower);
}
}
// 负责处理战斗逻辑的类
class CombatManager {
public void attack(Hero attacker, Hero target) {
System.out.println(attacker.getName() + " attacks " + target.getName());
target.setHealth(target.getHealth() - attacker.getAttackPower());
}
}
这样子我们拆分成两个类,各自类负责各自的内容,单个类的功能就不会复杂可读性高并且维护性高
依赖倒置原则
核心思想是高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
说人话就是方法传递参数的时候使用父类或者结构,声明对象的时候使用父类或者接口,充分使用java的多态环境
遵守前
// 雪球技能
class Snowball {
public void cast() {
System.out.println("Casting Snowball!");
}
}
// 治疗技能
class Heal {
public void cast() {
System.out.println("Casting Heal!");
}
}
// 点燃技能
class Ignite {
public void cast() {
System.out.println("Casting Ignite!");
}
}
// 英雄类直接依赖具体的技能实现
class Hero {
private Snowball snowball;
private Heal heal;
private Ignite ignite;
public Hero() {
this.snowball = new Snowball();
this.heal = new Heal();
this.ignite = new Ignite();
}
// 英雄释放雪球技能
public void castSnowball() {
snowball.cast();
}
// 英雄释放治疗技能
public void castHeal() {
heal.cast();
}
// 英雄释放点燃技能
public void castIgnite() {
ignite.cast();
}
}
public class Main {
public static void main(String[] args) {
Hero hero = new Hero();
hero.castSnowball(); // 释放雪球
hero.castHeal(); // 释放治疗
hero.castIgnite(); // 释放点燃
}
}
如果多加一个技能就需要多写一个技能类,并且在hero类里面还需要多加一个释放技能的方法
遵守后
// 定义技能的抽象接口
interface Skill {
void cast();
}
// 雪球技能实现
class Snowball implements Skill {
@Override
public void cast() {
System.out.println("Casting Snowball!");
}
}
// 治疗技能实现
class Heal implements Skill {
@Override
public void cast() {
System.out.println("Casting Heal!");
}
}
// 点燃技能实现
class Ignite implements Skill {
@Override
public void cast() {
System.out.println("Casting Ignite!");
}
}
// 英雄类依赖于技能的抽象接口
class Hero {
private Skill skill;
// 通过构造函数注入具体的技能实现
public Hero(Skill skill) {
this.skill = skill;
}
// 英雄释放技能
public void castSkill() {
skill.cast();
}
// 可以动态切换技能
public void changeSkill(Skill skill) {
this.skill = skill;
}
}
public class Main {
public static void main(String[] args) {
Skill snowball = new Snowball(); // 创建雪球技能
Hero hero = new Hero(snowball); // 英雄拥有雪球技能
hero.castSkill(); // 释放雪球
Skill heal = new Heal(); // 创建治疗技能
hero.changeSkill(heal); // 切换为治疗技能
hero.castSkill(); // 释放治疗
Skill ignite = new Ignite(); // 创建点燃技能
hero.changeSkill(ignite); // 切换为点燃技能
hero.castSkill(); // 释放点燃
}
}
迪米特法则
只与直接朋友交谈,避免过多依赖外部对象
实现方式:在编写方法时,只能调用以下几种对象的方法:
当前对象自身;
方法参数;
当前对象的成员对象;
当前对象创建的对象;
当前对象的静态变量;
说人话就是避免链式调用,不要再一个方法里面调用对象中的对象的信息
遵守前
class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
class Shop {
public Item getItem(String itemName) {
// 假设商店有多件商品,根据名称找到对应商品
if (itemName.equals("Sword")) {
return new Item("Sword", 100);
} else if (itemName.equals("Shield")) {
return new Item("Shield", 150);
}
return null;
}
}
class Hero {
private int gold;
public Hero(int gold) {
this.gold = gold;
}
public void buyItem(Shop shop, String itemName) {
Item item = shop.getItem(itemName);
if (item != null && gold >= item.getPrice()) { //发生了链式调用先调用shop获取item然后再获取item里面的信息
gold -= item.getPrice();
System.out.println("Bought " + item.getName() + " for " + item.getPrice() + " gold.");
} else {
System.out.println("Not enough gold to buy " + itemName);
}
}
}
遵守后
class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
class Shop {
// 处理购买逻辑,隐藏Item的细节
public void sellItem(Hero hero, String itemName) {
Item item = getItem(itemName);
if (item != null && hero.canAfford(item.getPrice())) {
hero.deductGold(item.getPrice());
System.out.println(hero.getName() + " bought " + item.getName() + " for " + item.getPrice() + " gold.");
} else {
System.out.println(hero.getName() + " does not have enough gold to buy " + itemName);
}
}
private Item getItem(String itemName) {
if (itemName.equals("Sword")) {
return new Item("Sword", 100);
} else if (itemName.equals("Shield")) {
return new Item("Shield", 150);
}
return null;
}
}
class Hero {
private String name;
private int gold;
public Hero(String name, int gold) {
this.name = name;
this.gold = gold;
}
public boolean canAfford(int price) {
return gold >= price;
}
public void deductGold(int amount) {
gold -= amount;
}
public String getName() {
return name;
}
public void buyItem(Shop shop, String itemName) {
// Hero只需告诉Shop他想买什么,不关心Item的细节
shop.sellItem(this, itemName);
}
}
有些人就会问,如果不遵守会发生什么后果呢
英雄联盟里面小兵也需要购买装备呢
如果没有遵守的话代码会是这样子的
新例子不遵守迪米特法则
class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
class Shop {
public Item getItem(String itemName) {
if (itemName.equals("Sword")) {
return new Item("Sword", 100);
} else if (itemName.equals("Shield")) {
return new Item("Shield", 150);
}
return null;
}
}
class Hero {
private int gold;
public Hero(int gold) {
this.gold = gold;
}
public void buyItem(Shop shop, String itemName) {
Item item = shop.getItem(itemName);
if (item != null && gold >= item.getPrice()) { //发生了链式调用先调用shop获取item然后再获取item里面的信息
gold -= item.getPrice();
System.out.println("Bought " + item.getName() + " for " + item.getPrice() + " gold.");
} else {
System.out.println("Not enough gold to buy " + itemName);
}
}
}
class Villain {
private int gold;
public Villain(int gold) {
this.gold = gold;
}
public void buyItem(Shop shop, String itemName) {
Item item = shop.getItem(itemName);
if (item != null && gold >= item.getPrice()) { //发生了链式调用先调用shop获取item然后再获取item里面的信息
gold -= item.getPrice();
System.out.println("Bought " + item.getName() + " for " + item.getPrice() + " gold.");
} else {
System.out.println("Not enough gold to buy " + itemName);
}
}
}
如果我们需要修改商品打折呢,我们就需要分别修改Hero类和Villain类里面的buyItem方法
新例子遵守迪米特法则
package openAndClosePrinciple.notUse;
class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
abstract class Character {
protected String name;
protected int gold;
public Character(String name, int gold) {
this.name = name;
this.gold = gold;
}
public String getName() {
return name;
}
public int getGold() {
return gold;
}
public boolean canAfford(int price) {
return gold >= price;
}
public void deductGold(int amount) {
gold -= amount;
}
}
class Hero extends Character {
public Hero(String name, int gold) {
super(name, gold);
}
}
class Villain extends Character{
public Villain(String name, int gold) {
super(name, gold);
}
}
class Shop {
public void sellItem(Character character, String itemName) {
Item item = getItem(itemName);
if (item != null && character.canAfford(item.getPrice())) {
character.deductGold(item.getPrice());
System.out.println(character.getName() + " bought " + item.getName() + " for " + item.getPrice() + " gold.");
} else {
System.out.println(character.getName() + " does not have enough gold to buy " + itemName);
}
}
private Item getItem(String itemName) {
if (itemName.equals("Sword")) {
return new Item("Sword", 100);
} else if (itemName.equals("Shield")) {
return new Item("Shield", 150);
}
return null;
}
}
如果我们需要修改商品打折呢,我们就只需要修改shop类里面的sellItem方法,就不会修改到多个类了,而且还遵守了依赖倒置原则,依赖的是接口Character
里氏替换原则
里氏替换原则是面向对象设计的基本原则之一,由Barbara Liskov在1987年提出。它指出:子类对象必须能够替换掉父类对象,并且保证系统行为的正确性。换句话说,程序中的父类对象被其子类对象替换后,程序行为仍然保持一致,而不会引发错误或产生意外的结果。
举个例子,英雄联盟里面每个英雄都有qwer技能,但是厄斐琉斯没有e技能就不能继承hero类
遵守前
// 英雄的基础类,定义了所有英雄都有E技能
class Champion {
public void useE() {
System.out.println("Champion uses E skill");
}
}
// 普通英雄,比如盖伦
class Garen extends Champion {
@Override
public void useE() {
System.out.println("Garen uses Judgment (E skill)");
}
}
// 特殊英雄:厄斐琉斯,没有E技能
class Aphelios extends Champion {
@Override
public void useE() {
throw new UnsupportedOperationException("Aphelios has no E skill!");
}
}
public class Main {
public static void main(String[] args) {
Champion garen = new Garen();
garen.useE(); // 输出: Garen uses Judgment (E skill)
Champion aphelios = new Aphelios();
aphelios.useE(); // 抛出异常:UnsupportedOperationException
}
}
遵守后
// 定义有E技能的接口
interface ESkill {
void useE();
}
// 英雄的基础类
class Champion {
public void useQ() {
System.out.println("Champion uses Q skill");
}
}
// 普通英雄,比如盖伦,具有E技能
class Garen extends Champion implements ESkill {
@Override
public void useE() {
System.out.println("Garen uses Judgment (E skill)");
}
}
// 特殊英雄:厄斐琉斯,没有E技能
class Aphelios extends Champion {
// 厄斐琉斯没有实现 ESkill 接口,因为他没有E技能
}
public class Main {
public static void main(String[] args) {
Champion garen = new Garen();
((ESkill) garen).useE(); // 输出: Garen uses Judgment (E skill)
Champion aphelios = new Aphelios();
aphelios.useQ(); // 输出: Champion uses Q skill
// aphelios.useE(); // 不会调用E技能,因为厄斐琉斯没有E技能
}
}
接口隔离原则
一个类不应该依赖它不需要的接口,换句话说,接口应该尽量小而精,不应将不相关的功能强加给类,而是根据不同的需求进行接口的拆分和隔离。
举个例子,比如说每个英雄的技能是一个接口,技能有击飞,眩晕,持续伤害这些效果,但是不是每个英雄的技能都有这些效果,就不能将这些击飞,眩晕,持续伤害等方法写在英雄技能的抽象接口中,而是写在不同的接口中,哪些技能有这些方法再去实现接口重写方法调用。说起来可能有点抽象,我们直接看代码。
遵守前
// 定义一个包含所有技能的大接口
interface AllSkills {
void knockUp(); // 击飞技能
void dash(); // 位移技能
void dealDamage(); // 伤害技能
void applyDot(); // 持续性伤害技能
}
// 实现李青的“神龙摆尾”技能
class DragonRage implements AllSkills {
@Override
public void knockUp() {
System.out.println("Lee Sin uses Dragon's Rage to knock the enemy away!");
}
@Override
public void dash() {
// 李青没有位移技能,但被迫实现
throw new UnsupportedOperationException("Lee Sin doesn't dash!");
}
@Override
public void dealDamage() {
// 李青没有伤害技能,但被迫实现
throw new UnsupportedOperationException("Lee Sin doesn't deal damage with this skill!");
}
@Override
public void applyDot() {
// 李青没有持续性伤害技能,但被迫实现
throw new UnsupportedOperationException("Lee Sin doesn't apply DOT with this skill!");
}
}
// 实现亚索的“踏前斩”技能
class SweepingBlades implements AllSkills {
@Override
public void knockUp() {
// 亚索没有击飞技能,但被迫实现
throw new UnsupportedOperationException("Yasuo doesn't knock up!");
}
@Override
public void dash() {
System.out.println("Yasuo uses Sweeping Blades to dash forward!");
}
@Override
public void dealDamage() {
// 亚索没有伤害技能,但被迫实现
throw new UnsupportedOperationException("Yasuo doesn't deal damage with this skill!");
}
@Override
public void applyDot() {
// 亚索没有持续性伤害技能,但被迫实现
throw new UnsupportedOperationException("Yasuo doesn't apply DOT with this skill!");
}
}
// 主程序
public class Main {
public static void main(String[] args) {
// 创建技能对象
AllSkills dragonRage = new DragonRage();
AllSkills sweepingBlades = new SweepingBlades();
// 使用技能
dragonRage.knockUp(); // 输出: Lee Sin uses Dragon's Rage to knock the enemy away!
sweepingBlades.dash(); // 输出: Yasuo uses Sweeping Blades to dash forward!
}
}
遵守后
// 定义击飞技能接口
interface KnockUpSkill {
void knockUp();
}
// 定义位移技能接口
interface DashSkill {
void dash();
}
// 实现李青的“神龙摆尾”技能
class DragonRage implements KnockUpSkill {
@Override
public void knockUp() {
System.out.println("Lee Sin uses Dragon's Rage to knock the enemy away!");
}
}
// 实现亚索的“踏前斩”技能
class SweepingBlades implements DashSkill {
@Override
public void dash() {
System.out.println("Yasuo uses Sweeping Blades to dash forward!");
}
}
// 主程序
public class Main {
public static void main(String[] args) {
// 创建技能对象
KnockUpSkill dragonRage = new DragonRage();
DashSkill sweepingBlades = new SweepingBlades();
// 使用技能
dragonRage.knockUp(); // 输出: Lee Sin uses Dragon's Rage to knock the enemy away!
sweepingBlades.dash(); // 输出: Yasuo uses Sweeping Blades to dash forward!
}
}