前言
其实在面向对象设计里,程序猿们互相约定好一些原则,即七大原则。
面向对象的七大原则是一组指导软件设计的原则,旨在帮助开发人员实现松耦合、可维护和可扩展的软件系统。这些原则的设计过程和发展历史可以追溯到20世纪80年代。
单一职责原则(Single Responsibility Principle):这个原则最早由罗伯特·C·马丁(Robert C. Martin)提出,并在他的《敏捷软件开发:原则、模式和实践》一书中详细阐述。该原则指出,一个类应该只有一个引起变化的原因,即一个类应该只负责一项职责。这样可以实现类的高内聚性和低耦合性。
开放关闭闭原则(Open-Closed Principle):开放封闭原则由伯特兰·梅耶(Bertrand Meyer)提出,他在他的《面向对象软件构造》一书中详细阐述了该原则。该原则指出,一个软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当需求改变时,应该通过扩展现有实体而不是修改已有代码。
里氏替换原则(Liskov Substitution Principle):里氏替换原则由芭芭拉·利斯科夫(Barbara Liskov)提出,并在她的论文《数据抽象和层次类型》中详细阐述。该原则指出,子类对象应该能够替换所有使用基类对象的地方,而不会产生错误或违反系统的行为。这可以保证继承关系的正确性。
依赖倒置原则(Dependency Inversion Principle):依赖倒置原则由罗伯特·C·马丁提出,并在他的《敏捷软件开发:原则、模式和实践》一书中详细阐述。该原则指出,高层模块不应该依赖于低层模块,它们应该依赖于抽象。这样可以实现模块之间的松耦合。
接口隔离原则(Interface Segregation Principle):接口隔离原则由罗伯特·C·马丁提出,并在他的《敏捷软件开发:原则、模式和实践》一书中详细阐述。该原则指出,客户端不应该依赖于它不需要的接口。一个类只应该依赖于它所使用的接口,避免了不必要的依赖。
迪米特法则(Law of Demeter)(也叫最少知识原则):迪米特法则由伊恩·霍洛维茨(Ian Holland)和巴斯卡尔·勒格兰(Pascal Leroux)提出,并在他们的论文《迪米特法则对面向对象设计的影响》中详细阐述。该原则指出,一个对象应该对其他对象保持最少的了解,只与直接的朋友交流。这样可以减少对象之间的耦合。
合成复用原则(Composite Reuse Principle):合成复用原则由伊恩·霍洛维茨和巴斯卡尔·勒格兰提出,并在他们的论文《迪米特法则对面向对象设计的影响》中详细阐述。该原则指出,尽量使用对象组合,而不是继承来实现代码的复用。这样可以使系统更加灵活和可扩展。
随着面向对象编程的兴起和软件开发的需求不断演变,它们得到了广泛的应用和发展。这些原则的目标是提高软件系统的可维护性、可扩展性和可重用性,使软件的开发过程更加灵活和高效,而今天我们也将详细讲讲七大原则,希望能在日后的编程对你有所帮助。
简述
单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个变化的原因,即每个类应该只负责一项功能。
开放-关闭原则(Open/Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,可以通过扩展现有代码而不是修改它来实现新功能。
里氏替换原则(Liskov Substitution Principle,LSP):子类对象应该能够替换父类对象而不影响程序的正确性,确保子类的行为符合父类的预期。
接口隔离原则(Interface Segregation Principle,ISP):不应强迫客户端依赖于它们不使用的接口。应该将大的接口拆分成多个小接口,以便于实现和使用。
依赖反转原则(Dependency Inversion Principle,DIP):高层模块不应依赖于低层模块,二者都应依赖于抽象(接口或抽象类),而不应依赖于具体实现。
合成复用原则(Composite Reuse Principle,CRP):尽量使用对象的组合而不是继承来实现代码复用。组合关系比继承关系更灵活。
最少知识原则(Least Knowledge Principle,LKP):一个对象应该对其他对象有最少的了解。减少对象之间的耦合,通过公共接口进行交互。
可能你单看文字很多都还看不懂,有些东西甚至需要你学习了之后的东西再回来看,如果你是顺序查看博主的博文的话,建议只看里氏替换原则。
单一职责原则
基本概念
强调一个类应该只有一个单一的责任,即一个类应该仅仅负责一个功能或任务。
重点
- 单一性:每个类保持单一功能,简化类的接口。
- 职责划分:明确界定类的职责,避免交叉影响。
- 易维护性:变更一个责任时只需修改相关类,降低了风险。
作用
- 提高可读性和可理解性:清晰的职责使得代码更易于阅读和理解。
- 提升可维护性:减少了因改动引入的bug,因为每个类变更都与其单一的功能相关。
- 增强可重用性:聚焦于单一功能的类可以更容易地被重用于其他项目中。
示例
这个示例是其实也是大家在日后unity的设计中也经常使用的模式,当然,gamemanager用来具体干什么,就要视情况而定了。
using System;
using System.Collections.Generic;
// 游戏管理类,负责管理游戏逻辑
class GameManager
{
private Player player;
private List<Enemy> enemies;
public GameManager()
{
player = new Player();
enemies = new List<Enemy>();
InitializeEnemies();
}
// 初始化敌人
private void InitializeEnemies()
{
enemies.Add(new Enemy("Enemy 1"));
enemies.Add(new Enemy("Enemy 2"));
enemies.Add(new Enemy("Enemy 3"));
}
// 游戏主循环
public void Run()
{
while (true)
{
player.Update();
foreach (Enemy enemy in enemies)
{
enemy.Update();
}
if (player.IsCollidingWithEnemy(enemies))
{
Console.WriteLine("Player collided with an enemy");
break;
}
}
}
}
// 玩家类,负责玩家相关逻辑
class Player
{
public void Update()
{
Console.WriteLine("Player is updating");
}
public bool IsCollidingWithEnemy(List<Enemy> enemies)
{
// 检测玩家是否与敌人发生碰撞
foreach (Enemy enemy in enemies)
{
if (enemy.Position == this.Position)
{
return true;
}
}
return false;
}
// 玩家的其他属性和方法...
}
// 敌人类,负责敌人相关逻辑
class Enemy
{
public string Name { get; private set; }
public int Position { get; private set; }
public Enemy(string name)
{
Name = name;
Position = 0;
}
public void Update()
{
Console.WriteLine($"{Name} is updating");
Position++;
}
// 敌人的其他属性和方法...
}
class Program
{
static void Main(string[] args)
{
GameManager game = new GameManager();
game.Run();
}
}
开放-关闭原则
基本概念
开放-关闭原则是对象设计中的一种原则,其核心思想是“软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
重点
- 开放性:允许在不改变现有代码的情况下添加新功能。
- 封闭性:现有的代码在功能上不会被修改,应该能安全地被使用。
- 通过继承和接口等机制支持灵活的扩展。
作用
- 增强软件的可维护性和可扩展性。
- 减少代码修改带来的风险,降低意外引入bug的可能性。
- 促进模块化设计,使得系统可以方便地进行部件替换或升级。
示例
在Main
方法中,我们创建了一个包含一个战士和一个法师角色的游戏场景对象,并调用了RunGame()
方法。由于Character
类是开放的,我们可以随时添加新的角色类而不需要修改GameScene
类的代码,同时GameScene
类的代码是关闭的,不需要对新的角色类进行修改,这就是所谓的开放关闭原则。
using System;
// 游戏角色基类
abstract class Character
{
public abstract void Attack();
//可以在这里添加方法
}
// 战士角色
class Warrior : Character
{
public override void Attack()
{
Console.WriteLine("战士发起了一次普通攻击!");
}
}
// 法师角色,你还可以写一个牧师角色(相当于开放的)
class Mage : Character
{
public override void Attack()
{
Console.WriteLine("法师施放了一次火球术!");
}
}
// 游戏场景类(相当于关闭了,不用管里面的)
class GameScene
{
private Character[] characters;
public GameScene(Character[] characters)
{
this.characters = characters;
}
public void RunGame()
{
foreach (Character character in characters)
{
character.Attack();
}
}
}
class Program
{
static void Main(string[] args)
{
Character[] characters = { new Warrior(), new Mage() };
GameScene gameScene = new GameScene(characters);
gameScene.RunGame();
}
}
里氏替换原则
基本概念
任何父类出现的地方,子类都可以替代
重点
语法表现——父类容器装子类对象,因为子类对象包含了父类的所有内容
作用
- 方便对象存储和管理
示例
简单写一个,我就不多解释了,就是简单的父类装子类。
using System;
public class Shape
{
public virtual double CalculateArea()
{
return 0;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return Width * Height;
}
}
public class Square : Shape
{
public double SideLength { get; set; }
public override double CalculateArea()
{
return SideLength * SideLength;
}
}
public class Program
{
public static void Main(string[] args)
{
Shape shape1 = new Rectangle { Width = 5, Height = 10 };
Shape shape2 = new Square { SideLength = 5 };
Console.WriteLine("Rectangle Area: " + shape1.CalculateArea());
Console.WriteLine("Square Area: " + shape2.CalculateArea());
}
}
接口隔离原则
基本概念
核心思想是“客户端不应该被迫依赖于它不使用的接口。” 简而言之,就是每个接口应该只包含客户端所需的方法,避免将多个不相关的方法聚合在一个接口中。
重点
- 细化接口:将大接口分拆为多个小接口,使得实现这些接口的类更为专注。
- 降低耦合度:使得类与接口的依赖关系更为精确,减小了系统之间的耦合,增强了灵活性。
- 提高可维护性:降低了不必要的方法对实现类的影响,修改接口时影响范围更小。
作用
- 增强系统的模块化,易于理解和维护。
- 提高代码的复用性,使不同的类能够更灵活地选择需要实现的接口。
- 降低修改某个接口时,导致其他代码破坏的风险。
示例
直接看例子,这个规范光靠说的话也很简单,就是不能把太多功能耦合到一个东西身上,打个比方,有些gamejam的顶级程序猿能把武器,道具这些全写到人物里面去,你可以从自己角度简单评价一下这个代码维护起来有多么逆天。
using System;
// 定义不同接口:播放音频和播放视频分开,当然实际开发不一定这样分,但是你要知道为什么要这样规定
interface IPlayer
{
void Play();
}
interface IRecord
{
void Record();
}
// 实现接口
class VideoPlayer : IPlayer
{
public void Play()
{
Console.WriteLine("开始播放视频");
}
}
class AudioPlayer : IPlayer, IRecord
{
public void Play()
{
Console.WriteLine("开始播放音频");
}
public void Record()
{
Console.WriteLine("开始录音");
}
}
class Game
{
private IPlayer player;
public Game(IPlayer player)
{
this.player = player;
}
public void Start()
{
player.Play();
}
}
class Program
{
static void Main(string[] args)
{
IPlayer videoPlayer = new VideoPlayer();
IPlayer audioPlayer = new AudioPlayer();
Game videoGame = new Game(videoPlayer);
videoGame.Start(); // 输出:开始播放视频
Game audioGame = new Game(audioPlayer);
audioGame.Start(); // 输出:开始播放音频
}
}
依赖反转原则
基本概念
这个的主要思想是“高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖于细节,细节应该依赖于抽象。”说人话,依赖于抽象(接口或抽象类)而不是具体实现,这样可以减少模块之间的耦合。
重点
- 高层模块:指的是完成业务逻辑等高级功能的部分。
- 低层模块:指的是具体的实现细节,例如数据访问或硬件控制。
- 抽象:通常是一个接口或抽象类,用于定义高层模块和低层模块之间的交互。
作用
- 提高系统的灵活性和可扩展性,方便替换实现。
- 减少模块之间的耦合,使得更改低层实现时不影响高层逻辑。
- 提高代码的可测试性,通过依赖注入等方式简化单元测试。
示例
示例中,我们定义了一个接口 IWeapon
,以及两个实现类 Sword
和 Bow
。然后,在 Player
类中通过构造函数注入 IWeapon
接口的实例,以实现依赖反转原则。这样,我们可以根据需要选择不同的武器,而不需要修改 Player
类的代码。
在 Game
类的 Main
方法中,我们创建了一个 Player
实例,分别使用剑和弓箭进行攻击。这样,我们可以灵活地为角色选择不同的武器,而不需要修改 Game
类的代码。
using System;
// 定义接口
public interface IWeapon
{
void Attack();
}
// 定义实现类
public class Sword : IWeapon
{
public void Attack()
{
Console.WriteLine("使用剑攻击");
}
}
public class Bow : IWeapon
{
public void Attack()
{
Console.WriteLine("使用弓箭攻击");
}
}
// 定义高层模块
public class Player
{
private IWeapon weapon;
// 通过构造函数注入依赖
public Player(IWeapon weapon)
{
this.weapon = weapon;
}
//避免了你在这里面写一堆道具和武器的方法
public void Attack()
{
weapon.Attack();
}
}
// 示例程序
public class Game
{
public static void Main(string[] args)
{
IWeapon sword = new Sword();
Player player1 = new Player(sword);
player1.Attack();
IWeapon bow = new Bow();
Player player2 = new Player(bow);
player2.Attack();
}
}
合用复用原则
基本概念
大概意思是“你不会需要它”。这个原则强调在软件开发中,开发者不应该添加过多的功能或代码,只应当实现当前需求所需的功能,避免为了未来可能需要的功能而过度设计。
重点
- 避免过度设计:只开发当前需求所需的功能,避免考虑和实现未来可能不必要的功能。
- 简化代码:减少无谓的复杂性,让代码保持简洁和清晰。
- 提高维护性:随着代码变得复杂,维护的成本会增加,原则帮助保持代码的可维护性。
作用
- 降低项目的复杂度:代码更加简单,易于理解和维护。
- 提高开发效率:避免不必要的功能开发,从而节省时间和资源。
- 降低出错概率:减少不必要的逻辑和功能可以降低 bug 的数量,提高系统稳定性。
示例
没有示例,想要告诉你的更多是你要记住,不要画蛇添足
最少知识原则
基本概念
最少知识原则是面向对象设计中的一项原则,强调一个对象应当对其他对象有尽可能少的了解。说人话,一个对象应该只与直接的朋友(合作对象)进行交互,而不应该去了解其他对象之间的复杂关系。
重点
- 直接交互:对象只应与直接相关的对象进行通信,避免“连锁调用”。
- 封装性:通过减少对象间的知识,增强封装性,使对象能独立变化。
- 降低耦合:减少模块之间的依赖,有助于系统的维护和扩展。
作用
- 提高可维护性:系统的修改和维护更容易,因为对象之间的依赖关系被减少。
- 增强可读性:代码更容易理解,减少了对象之间复杂的交互模式。
- 促进独立性:使得各个模块之间能独立发展,减少了相互影响的风险。
示例
错误示范:假设我们有一个系统,其中 Order
类依赖于 Customer
和 Address
类
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
public class Customer
{
public Address Address { get; set; }
}
public class Order
{
public Customer Customer { get; set; }
public void PrintShippingAddress()
{
// 连锁调用,不符合最少知识原则
Console.WriteLine($"Shipping to: {Customer.Address.Street}, {Customer.Address.City}");
}
}
改进后:
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string GetFullAddress()
{
return $"{Street}, {City}";
}
}
public class Customer
{
public Address Address { get; set; }
public string GetShippingAddress()
{
return Address.GetFullAddress();
}
}
public class Order
{
public Customer Customer { get; set; }
public void PrintShippingAddress()
{
// 只与 Customer 交互,符合最少知识原则
Console.WriteLine($"Shipping to: {Customer.GetShippingAddress()}");
}
}
我想这个示例能让你明白这是什么个情况
总结
七大原则本身其实是大家不断探索后发现的约定,其实你可以不这样写程序,当然你在遇到长期项目的时候必然会遇到很多问题,规范的代码有主意你更好地进行长线开发。
可能博主对着七大原则的理解也有一些误区,欢迎批评指正。
还是那句话,学习路上,脚踏实地。
请期待我的下一篇博客!
我来自佑梦游戏开发,感谢你的关注和收藏!