C#(14)七大原则

发布于:2024-11-27 ⋅ 阅读:(11) ⋅ 点赞:(0)

前言

其实在面向对象设计里,程序猿们互相约定好一些原则,即七大原则。

面向对象的七大原则是一组指导软件设计的原则,旨在帮助开发人员实现松耦合、可维护和可扩展的软件系统。这些原则的设计过程和发展历史可以追溯到20世纪80年代。

  1. 单一职责原则(Single Responsibility Principle):这个原则最早由罗伯特·C·马丁(Robert C. Martin)提出,并在他的《敏捷软件开发:原则、模式和实践》一书中详细阐述。该原则指出,一个类应该只有一个引起变化的原因,即一个类应该只负责一项职责。这样可以实现类的高内聚性和低耦合性。

  2. 开放关闭闭原则(Open-Closed Principle):开放封闭原则由伯特兰·梅耶(Bertrand Meyer)提出,他在他的《面向对象软件构造》一书中详细阐述了该原则。该原则指出,一个软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当需求改变时,应该通过扩展现有实体而不是修改已有代码。

  3. 里氏替换原则(Liskov Substitution Principle):里氏替换原则由芭芭拉·利斯科夫(Barbara Liskov)提出,并在她的论文《数据抽象和层次类型》中详细阐述。该原则指出,子类对象应该能够替换所有使用基类对象的地方,而不会产生错误或违反系统的行为。这可以保证继承关系的正确性。

  4. 依赖倒置原则(Dependency Inversion Principle):依赖倒置原则由罗伯特·C·马丁提出,并在他的《敏捷软件开发:原则、模式和实践》一书中详细阐述。该原则指出,高层模块不应该依赖于低层模块,它们应该依赖于抽象。这样可以实现模块之间的松耦合。

  5. 接口隔离原则(Interface Segregation Principle):接口隔离原则由罗伯特·C·马丁提出,并在他的《敏捷软件开发:原则、模式和实践》一书中详细阐述。该原则指出,客户端不应该依赖于它不需要的接口。一个类只应该依赖于它所使用的接口,避免了不必要的依赖。

  6. 迪米特法则(Law of Demeter)(也叫最少知识原则):迪米特法则由伊恩·霍洛维茨(Ian Holland)和巴斯卡尔·勒格兰(Pascal Leroux)提出,并在他们的论文《迪米特法则对面向对象设计的影响》中详细阐述。该原则指出,一个对象应该对其他对象保持最少的了解,只与直接的朋友交流。这样可以减少对象之间的耦合。

  7. 合成复用原则(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()}");  
    }  
}

我想这个示例能让你明白这是什么个情况

总结

七大原则本身其实是大家不断探索后发现的约定,其实你可以不这样写程序,当然你在遇到长期项目的时候必然会遇到很多问题,规范的代码有主意你更好地进行长线开发。

可能博主对着七大原则的理解也有一些误区,欢迎批评指正。

还是那句话,学习路上,脚踏实地。

请期待我的下一篇博客!

我来自佑梦游戏开发,感谢你的关注和收藏!