首页 > 其他分享 >设计模式:六大原则

设计模式:六大原则

时间:2024-01-18 20:33:07浏览次数:28  
标签:六大 原则 子类 代码 系统 接口 void 设计模式 public

文章目录

1.单一职责原则(Single Responsibility Principle,SRP)

2.开闭原则(Open-Closed Principle, OCP)

3.里氏代换原则(Liskov Substitution Principle, LSP)

4.依赖倒置原则(Dependence Inversion Principle,DIP)

5.接口隔离原则(Interface  Segregation Principle, ISP)

6.迪米特法则(Law of  Demeter, LoD)

 

1.单一职责原则(Single Responsibility Principle,SRP)

1.1.定义

一个类应该只有一个引起它变化的原因。换句话说:一个类只负责一项职责,只负责一个特定的功能或任务。

理解一个类应该专注于完成一个特定的任务或功能,而不涉及多个不同的责任。如果一个类有多个责任,那么对其中一个责任的变化可能会影响其他责任,导致代码变得复杂且难以维护。通过遵循单一职责原则,可以确保每个类都专注于单一功能,使代码更加清晰、可维护,并降低系统的复杂性。这有助于提高代码的质量和可读性。

1.2.代码实现

场景:假设有一个图形类 Shape,它负责绘制各种形状,同时它还负责保存和加载这些形状的数据。然而,根据单一职责原则,我们应该将这两个职责分开,使得每个类只负责一个功能。

using System;

// 定义图形接口
public interface IShape
{
    void Draw();
}

// 定义图形数据存储接口
public interface IShapeDataStorage
{
    void SaveData();
    void LoadData();
}

// 实现矩形类,实现绘制和数据存储接口
public class Rectangle : IShape, IShapeDataStorage
{
    public void Draw()
    {
        Console.WriteLine("绘制矩形");
        // 绘制矩形的具体逻辑
    }

    public void SaveData()
    {
        Console.WriteLine("保存矩形数据");
        // 保存矩形数据的具体逻辑
    }

    public void LoadData()
    {
        Console.WriteLine("加载矩形数据");
        // 加载矩形数据的具体逻辑
    }
}

// 实现圆形类,同样实现绘制和数据存储接口
public class Circle : IShape, IShapeDataStorage
{
    public void Draw()
    {
        Console.WriteLine("绘制圆形");
        // 绘制圆形的具体逻辑
    }

    public void SaveData()
    {
        Console.WriteLine("保存圆形数据");
        // 保存圆形数据的具体逻辑
    }

    public void LoadData()
    {
        Console.WriteLine("加载圆形数据");
        // 加载圆形数据的具体逻辑
    }
}

// 客户端代码
class Program
{
    static void Main()
    {
        // 创建矩形并执行绘制和数据存储操作
        IShape rectangle = new Rectangle();
        rectangle.Draw();
        ((IShapeDataStorage)rectangle).SaveData();
        ((IShapeDataStorage)rectangle).LoadData();

        Console.WriteLine();

        // 创建圆形并执行绘制和数据存储操作
        IShape circle = new Circle();
        circle.Draw();
        ((IShapeDataStorage)circle).SaveData();
        ((IShapeDataStorage)circle).LoadData();
    }
}

例子中,通过接口将图形类的绘制和数据存储功能分开,然后让矩形类和圆形类分别实现这两个接口。这样,每个类都专注于一个职责,符合单一职责原则。

1.3.优缺点

优点:

1. 高内聚性(High Cohesion):类只负责一个功能,使得类内部的各个成员彼此关联紧密,实现了高内聚性。这使得类的设计更加清晰和易于理解。

2. 可维护性提高:当一个类只有一个变化的原因时,修改和维护变得更加简单。如果一个类负责多个职责,修改其中一个职责可能会影响到其他职责,导致代码变得复杂,而SRP避免了这种情况。

3. 可测试性提高:单一职责原则有助于提高代码的可测试性,因为每个类都有一个清晰的责任,易于编写和执行单元测试。

4. 降低耦合度:每个类专注于一个职责,类与类之间的耦合度降低。这意味着修改一个类的实现不太可能影响其他类的行为,从而提高了系统的灵活性。

缺点:

1. 类的数量增多:遵循SRP可能导致类的数量增加,每个类只负责一个功能,这可能使得代码结构变得更加分散。在一些简单的情况下,这可能显得过于繁琐。

2. 设计难度增加:在实际应用SRP时,需要准确地划定类的职责,这可能需要更深入的设计思考。有时候,在平衡各个类的职责时可能会变得复杂。

3. 过度划分:有时为了满足SRP,可能会将一些本来可以合并的功能划分到不同的类中,导致过度划分的问题。

尽管存在一些缺点,但总体来说,遵循单一职责原则有助于提高代码的质量、可维护性和可测试性,是面向对象设计中的基本原则之一。在实际应用中,需要根据具体情况权衡利弊,确保合适的类设计和结构。

1.4.使用场景

1. 类的功能简单明确:当一个类的功能非常明确,只有一个原因会导致它的变化时,使用SRP是非常合适的。每个类应该专注于一个明确定义的功能或职责。

2. 高内聚性要求:如果系统需要高内聚性,即希望一个类内的各成员之间关联紧密,SRP是一种很好的设计原则。每个类专注于一个职责,有助于实现高内聚性。

3. 提高可维护性的需求:当系统要求易于维护时,SRP可以帮助减少代码的复杂性。每个类只负责一个功能,修改和维护变得更加简单,可维护性得到提高。

4. 支持可测试性:在需要进行单元测试的系统中,使用SRP可以提高可测试性。每个类都有明确的责任,易于编写和执行单元测试。

5. 系统需要低耦合度:当系统要求各个类之间的耦合度尽可能低时,SRP是有益的。每个类专注于一个职责,减少了类与类之间的直接关联,提高了系统的灵活性。

6. 避免功能冲突:当一个类的多个职责可能发生功能冲突时,使用SRP可以帮助解决这个问题。通过将职责分离,避免了可能导致冲突的情况。

 

2.开闭原则(Open-Closed Principle, OCP)

2.1.定义

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。换句话说:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展

理解:这意味着在系统设计中,我们应该允许通过扩展来引入新的功能,而不是通过修改已有的源代码。通过这种方式,我们可以在不改变现有代码的情况下,轻松地引入新的功能或进行系统的扩展。这有助于保持系统的稳定性,降低引入错误的风险,同时提高系统的可维护性和灵活性。

2.2.代码实现

场景:假设有一个图形绘制系统,初始版本只能绘制矩形和圆形。现在需求变更,要求系统能够绘制新的图形(比如三角形)。按照开闭原则,我们需要确保系统对扩展开放,不修改已有的代码,而是通过扩展来引入新的图形类型。

using System;
using System.Collections.Generic;

// 定义图形接口
public interface IShape
{
    void Draw();
}

// 定义矩形类,实现图形接口
public class Rectangle : IShape
{
    public void Draw()
    {
        Console.WriteLine("绘制矩形");
        // 绘制矩形的具体逻辑
    }
}

// 定义圆形类,实现图形接口
public class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("绘制圆形");
        // 绘制圆形的具体逻辑
    }
}

// 新需求:定义三角形类,实现图形接口
public class Triangle : IShape
{
    public void Draw()
    {
        Console.WriteLine("绘制三角形");
        // 绘制三角形的具体逻辑
    }
}

// 图形绘制系统
public class DrawingSystem
{
    // 绘制图形的方法,通过接口调用
    public void DrawShape(IShape shape)
    {
        shape.Draw();
    }
}

// 客户端代码
class Program
{
    static void Main()
    {
        // 初始化图形绘制系统
        DrawingSystem drawingSystem = new DrawingSystem();

        // 绘制矩形
        IShape rectangle = new Rectangle();
        drawingSystem.DrawShape(rectangle);

        // 绘制圆形
        IShape circle = new Circle();
        drawingSystem.DrawShape(circle);

        // 新需求:绘制三角形,不需要修改已有代码,只需引入新的图形类
        IShape triangle = new Triangle();
        drawingSystem.DrawShape(triangle);
    }
}

例子中,通过定义图形接口(IShape)和具体图形类(RectangleCircleTriangle),确保每个图形类都实现了绘制方法。图形绘制系统(DrawingSystem)通过接口调用绘制方法,不需要知道具体的图形类型。当新需求出现时,我们只需要引入新的图形类而无需修改已有的代码,遵循了开闭原则。

2.3.优缺点

优点:

1. 可扩展性:开闭原则允许系统通过扩展来引入新的功能,而不需要修改已有的代码。这使得系统更容易适应变化,具有良好的可扩展性。

2. 可维护性提高:由于系统对扩展开放,不对修改开放,因此对已有代码的维护工作较小。新功能的引入不会影响到已有的稳定代码,有助于提高系统的可维护性。

3. 降低风险:不修改已有代码意味着不引入新的错误,从而降低了引入错误的风险。新功能的开发只需要关注新代码,不会影响到已有功能的稳定性。

4. 提高代码复用性:由于新功能的引入是通过扩展而不是修改,因此可以更容易地重用已有的代码。现有的类可以作为基础构建块,用于构建新的功能。

5. 提高系统的灵活性:开闭原则有助于提高系统的灵活性。系统可以通过引入新的类或模块来适应变化,而不会影响到已有的部分。这样可以更容易地应对未来的需求变更。

缺点:

1. 抽象设计难度:遵循开闭原则需要进行合适的抽象设计,确保系统能够通过扩展来引入新的功能。有时候,为了满足开闭原则,可能需要引入更多的抽象层次,增加了设计的难度。

2. 类的数量增加:为了支持扩展,可能需要引入更多的类和接口,导致类的数量增加。这可能会使得系统结构变得复杂,需要权衡设计的复杂度。

3. 不适用于所有情况:开闭原则并不适用于所有情况。有些系统或模块可能是相对稳定的,不需要频繁地引入新功能。在这种情况下,过度遵循开闭原则可能会增加设计的复杂度。

开闭原则在大多数情况下都是有利的,可以提高系统的灵活性和可维护性。还是需要根据实际情况权衡利弊,确保设计的合理性。

2.4.使用场景

1. 变化频繁的系统:当系统中某一部分的需求经常发生变化时,使用开闭原则可以降低修改现有代码的频率。新的功能可以通过扩展来引入,而不需要修改已有的代码。

2. 支持插件和扩展性需求:当系统需要支持插件或模块的扩展,允许第三方开发者引入新的功能时,开闭原则是非常有用的。系统可以通过定义接口或抽象类,并允许插件通过实现或继承来引入新的功能。

3. 框架和库的设计:在设计框架或库时,开闭原则是一个重要的原则。框架或库的设计者希望用户能够通过扩展来适应不同的需求,而不是强制用户修改框架或库的源代码。

4. 业务规则变化频繁:当业务规则经常变化,而且需要系统能够灵活适应这些变化时,开闭原则是非常适用的。业务规则的变化可以通过引入新的类或模块来实现,而不影响已有的业务规则。

5. 支持未来扩展:当系统需要保持长期稳定并且能够在未来引入新的功能时,开闭原则是一种有益的设计原则。系统可以通过扩展来适应未来的需求,而不需要重构已有的代码。

开闭原则适用于那些可能经常变化、需要灵活扩展、或者需要长期稳定并支持未来扩展的系统。

 

3.里氏代换原则(Liskov Substitution Principle, LSP)

3.1.定义

任何基类可以出现的地方,子类一定可以出现。所有引用基类(父类)的地方必须能透明地使用其子类的对象。

理解:如果一个类是另一个类的子类,那么在程序中能够替换父类的实例,而不会导致程序出现错误。换句话说,子类能够完全替代父类的行为,而不影响程序的正确性。当我们使用基类的实例时,如果替换为子类的实例,程序的行为仍然是可预测和正确的。子类应该遵循与基类相同的契约,不引入意外行为。这有助于确保继承关系的正确性和稳定性,使得代码更加可靠和易于维护。

3.2.代码实现

场景:假设有一个图形类的层次结构,其中包含一个基类 Shape 和两个子类 Rectangle(矩形)和 Square(正方形)。按照里氏代换原则,我们希望能够在不影响程序正确性的情况下,替换父类 Shape 的实例为任何子类的实例。

using System;

// 定义图形基类
public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("绘制图形");
    }
}

// 定义矩形类,继承自图形基类
public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("绘制矩形");
    }
}

// 定义正方形类,继承自图形基类
public class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("绘制正方形");
    }
}

// 图形绘制系统
public class DrawingSystem
{
    // 绘制图形的方法,接受图形基类的实例作为参数
    public void DrawShape(Shape shape)
    {
        shape.Draw();
    }
}

// 客户端代码
class Program
{
    static void Main()
    {
        // 初始化图形绘制系统
        DrawingSystem drawingSystem = new DrawingSystem();

        // 使用基类的实例绘制图形
        Shape baseShape = new Shape();
        drawingSystem.DrawShape(baseShape);

        // 使用子类的实例绘制图形,符合里氏代换原则
        Shape rectangle = new Rectangle();
        drawingSystem.DrawShape(rectangle);

        // 使用另一个子类的实例绘制图形,同样符合里氏代换原则
        Shape square = new Square();
        drawingSystem.DrawShape(square);
    }
}

例子中,通过继承实现了里氏代换原则。基类 Shape 中定义了一个虚拟的 Draw 方法,而子类 Rectangle Square 分别覆写了这个方法,以实现各自特定的绘制逻辑。在客户端代码中,我们可以使用基类的实例或者任意子类的实例调用 DrawShape 方法,而不需要关心具体是哪个子类的实例。这符合里氏代换原则,保证了替换子类实例不会影响程序的正确性。

3.3.优缺点

优点:

1. 继承关系更加合理:遵循LSP能够确保继承体系更加合理和符合现实世界的模型,子类应该是对父类行为的扩展而不是替代。

2. 提高代码的可维护性:当子类能够无缝替代父类时,系统更容易维护。新的子类的引入不会对已有的代码产生负面影响。

3. 降低风险:LSP有助于降低引入错误的风险。当我们替换父类实例为子类实例时,系统行为应该保持一致,不会产生意外结果。

4. 增强系统的扩展性:符合LSP的设计能够更好地支持系统的扩展。通过引入新的子类,系统可以轻松适应新的需求,而不影响已有的部分。

缺点:

1. 过度设计的风险:为了满足LSP,可能需要引入更多的抽象层次和接口,这可能导致过度设计的风险。有时候,过度追求LSP可能使得系统变得复杂。

2. 子类可能过于依赖父类的实现:如果子类过于依赖于父类的实现,可能导致子类无法独立演化。这是在设计时需要注意的问题,确保子类有足够的灵活性。

3. 逻辑的复杂性:在一些情况下,为了满足LSP,可能需要引入更复杂的逻辑来处理不同的子类。这可能使得代码难以理解和维护。

3.4.使用场景

1. 继承体系中的子类扩展:当设计继承体系时,子类应该是对父类行为的扩展,而不是替代。LSP适用于这样的场景,确保子类可以无缝替代父类。

2. 接口的使用:在使用接口定义抽象类型的情况下,LSP同样适用。确保实现某个接口的不同类能够在不影响系统正常运行的情况下相互替代。

3. 多态应用:在使用多态性的场景中,LSP非常重要。当程序设计依赖于多态性时,子类的行为应该与父类一致,以确保多态性的正确性。

4. 替代原则:当需要在程序中能够替代父类的实例为子类的实例时,LSP是非常有用的。这样可以确保替代是安全的,不会引入错误或导致系统不稳定。

5. 保持一致性:LSP有助于保持系统的一致性。当新的子类引入时,系统应该保持一致的行为,不会产生意外的结果。

LSP适用于需要使用继承、接口和多态的场景,以确保子类能够在不破坏系统行为的情况下替代父类

 

4.依赖倒置原则(Dependence Inversion Principle,DIP)

4.1.定义

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,换句话说:要面向接口编程,不要面向实现编程。

理解:在系统设计中,应该避免高层模块(比如业务逻辑)直接依赖于低层模块(比如数据存储或具体实现),而是通过引入抽象层来解耦两者。抽象层定义了高层模块和低层模块的接口,使得它们都可以依赖于抽象而不是具体的细节。这有助于实现系统的灵活性和可维护性。

4.2.代码实现

场景:假设有一个电灯系统,其中包含了高层模块(Switch开关)和低层模块(Light灯泡)。按照依赖倒置原则(DIP),我们希望高层模块(开关)不直接依赖于低层模块(灯泡),而是通过引入抽象接口来解耦。

using System;

// 定义灯泡接口
public interface ILight
{
    void TurnOn();
    void TurnOff();
}

// 定义具体的灯泡实现
public class Light : ILight
{
    public void TurnOn()
    {
        Console.WriteLine("灯泡亮了");
    }

    public void TurnOff()
    {
        Console.WriteLine("灯泡灭了");
    }
}

// 定义开关类,通过构造函数注入灯泡接口
public class Switch
{
    private readonly ILight light;

    // 通过构造函数注入灯泡接口
    public Switch(ILight light)
    {
        this.light = light;
    }

    // 打开开关
    public void TurnOn()
    {
        Console.WriteLine("打开开关");
        // 调用灯泡接口方法
        light.TurnOn();
    }

    // 关闭开关
    public void TurnOff()
    {
        Console.WriteLine("关闭开关");
        // 调用灯泡接口方法
        light.TurnOff();
    }
}

// 客户端代码
class Program
{
    static void Main()
    {
        // 初始化具体的灯泡实例
        ILight light = new Light();

        // 初始化开关,并通过构造函数注入灯泡接口
        Switch mySwitch = new Switch(light);

        // 使用开关控制灯泡
        mySwitch.TurnOn();
        mySwitch.TurnOff();
    }
}

例子中,通过定义 ILight 接口和具体的 Light 类,实现了低层模块。然后,通过在高层模块 Switch 的构造函数中注入 ILight 接口,实现了依赖倒置原则。这样,开关不再直接依赖于具体的灯泡实现,而是依赖于灯泡接口,达到了解耦的效果。

4.3.优缺点

优点:

1. 松耦合:DIP通过引入抽象层,使得高层模块不直接依赖于低层模块的具体实现,从而实现了松耦合。这提高了系统的灵活性和可维护性。

2. 可扩展性:由于高层模块依赖于抽象而不是具体细节,因此系统更容易扩展。引入新的低层模块只需要实现相同的抽象接口,而不需要修改高层模块的代码。

3. 易于测试:由于高层模块依赖于抽象,可以轻松地通过模拟或替代具体实现进行单元测试。这提高了代码的可测试性。

4. 降低变更风险:当需要修改低层模块的具体实现时,由于高层模块只依赖于抽象,修改不会影响高层模块的稳定性,从而降低了变更的风险。

5. 提高可维护性:通过遵循DIP,系统的组织结构更清晰,模块之间的关系更加灵活,提高了代码的可维护性。

缺点:

1. 学习曲线:在一开始,遵循DIP可能会增加学习曲线。引入抽象层次和接口需要更多的设计思考,这可能需要一些时间来适应。

2. 过度设计风险:为了遵循DIP,可能引入过多的抽象层次,导致系统的设计变得复杂。需要在实际应用中权衡设计的合理性。

3. 性能损失:通过引入抽象层次,可能会引入一些性能损失。在一些对性能要求较高的场景中,需要谨慎使用DIP。

4.4.使用场景

1. 多层架构设计:DIP在多层架构设计中非常有用。高层模块(比如业务逻辑层)通过依赖抽象接口与低层模块(比如数据访问层)通信,而不是直接依赖于具体实现。这有助于实现多层之间的解耦。

2. 插件和扩展性需求:当系统需要支持插件或模块的扩展时,DIP是非常有用的。高层模块通过依赖抽象接口,插件或模块只需实现相同的接口即可无缝集成。

3. 框架和库的设计:在设计框架或库时,DIP是一个非常重要的原则。框架或库的设计者希望用户能够通过实现或继承抽象接口来扩展功能,而不是直接依赖于框架或库的具体实现。

4. 依赖注入(Dependency Injection):DIP与依赖注入紧密相关。通过依赖注入,可以在运行时将具体实现注入到高层模块中,而不需要在编译时硬编码依赖关系。

5. 测试驱动开发(TDD):在TDD中,DIP有助于编写可测试的代码。通过依赖于接口,可以更容易地模拟或替代具体实现进行单元测试。

DIP适用于需要降低模块间耦合性、提高系统灵活性、支持扩展和插件的情景。在这些场景中,通过引入抽象接口,使得高层模块和低层模块都依赖于抽象而不是具体实现,从而实现了依赖的倒置。

 

5.接口隔离原则(Interface  Segregation Principle, ISP)

5.1.定义

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

理解:一个类不应该被迫依赖于它不使用的接口。当一个类只需要使用接口的一部分方法时,不应该被强制实现整个接口。相反,应该根据具体需要定义多个小而专注的接口,每个接口包含一组相关的方法,使得类只需依赖于它真正需要使用的接口。这有助于避免类依赖于不必要的接口,提高了系统的灵活性和可维护性。

5.2.代码实现

场景:假设有一个报告系统,其中有一个生成报告的类 ReportGenerator,报告可以输出到文件和显示在屏幕上。按照接口隔离原则,我们希望 ReportGenerator 不应该依赖于不需要使用的方法,而是根据需要依赖于不同的接口。

using System;

// 定义显示接口
public interface IDisplayable
{
    void Display();
}

// 定义文件输出接口
public interface IFileOutput
{
    void SaveToFile();
}

// 报告生成器类,依赖于显示和文件输出接口
public class ReportGenerator : IDisplayable, IFileOutput
{
    private string reportContent;

    public ReportGenerator(string content)
    {
        this.reportContent = content;
    }

    public void Display()
    {
        Console.WriteLine("报告内容显示在屏幕上:");
        Console.WriteLine(reportContent);
    }

    public void SaveToFile()
    {
        Console.WriteLine("报告内容保存到文件中。");
        // 实际保存到文件的逻辑
    }
}

// 客户端代码
class Program
{
    static void Main()
    {
        // 初始化报告生成器
        ReportGenerator report = new ReportGenerator("这是报告的内容");

        // 使用显示接口显示报告内容
        IDisplayable displayableReport = report;
        displayableReport.Display();

        // 使用文件输出接口保存报告内容到文件
        IFileOutput fileOutputReport = report;
        fileOutputReport.SaveToFile();
    }
}

例子中,通过定义 IDisplayableIFileOutput 两个接口,分别包含显示和文件输出的方法。然后,在 ReportGenerator 类中,通过实现这两个接口,使得该类只依赖于需要使用的接口。这样,如果以后有其他输出方式,可以根据需要定义新的接口,而不会影响到已有的类。

5.3.优缺点

优点:

1. 减少类的依赖关系:ISP通过将大接口拆分成小接口,减少了类对不需要的方法的依赖。这有助于避免类被迫实现不相关的接口,减少了类之间的耦合度。

2. 提高类的灵活性:小而专注的接口使得类更容易适应变化。当需要添加新功能时,只需实现新的小接口,而不需要修改已有的代码。

3. 提高代码的可读性和可维护性:通过拆分接口,代码更加清晰,易于理解和维护。每个接口都具有明确的职责,使得代码结构更加清晰。

4. 遵循单一职责原则:拆分接口有助于确保每个接口都只有一个原因引起的变化,符合单一职责原则。这有助于维护代码的一致性。

5. 方便实现依赖注入:通过遵循ISP,每个小接口都可以独立使用,方便进行依赖注入。这对于实现松耦合和可测试的代码是有利的。

缺点:

1. 增加接口数量:ISP的实施可能导致接口的数量增加。如果过度拆分接口,可能使得系统变得复杂,增加了理解和维护的难度。

2. 可能需要更多的适配器:当类需要实现多个小接口时,可能需要使用适配器模式来适应这些接口。这可能导致引入更多的类和代码。

3. 接口命名的挑战:拆分接口时,需要考虑到如何合理地命名这些接口,确保每个接口都具有明确的含义。这可能需要一些设计和规范。

5.4.使用场景

1. 大接口的拆分:当一个接口变得庞大臃肿,其中包含了很多不相关的方法时,就可以考虑使用ISP。拆分成多个小而专注的接口,每个接口只包含与其职责相关的方法。

2. 接口的多继承:在一些语言中,一个类可以实现多个接口。当一个类需要实现的接口中包含不相关的方法时,可以通过拆分接口以适应不同的类的需要。

3. 框架和库的设计:在设计框架或库时,ISP非常有用。确保定义小而精简的接口,使得用户可以根据需要选择性地实现接口,而不被迫实现不需要的方法。

4. 依赖注入和依赖反转:在依赖注入和依赖反转的场景中,ISP有助于确保注入的依赖是小而专注的接口,而不是庞大的接口。这有助于提高代码的可维护性和测试性。

5. 系统演化和变更:当系统需要经常演化和变更时,使用ISP有助于减少对现有类的影响。新功能的添加可以通过新增接口和实现,而不需要修改已有的类。

ISP适用于需要降低类之间耦合度、提高系统灵活性和可维护性的场景。通过设计小而专注的接口,可以使得每个类只依赖于自己需要的接口,避免不必要的依赖。

 

6.迪米特法则(Law of  Demeter, LoD)

6.1.定义

一个对象应该对其他对象保持最少的了解。

理解:一个对象不应该直接与太多其他对象交互,而是应该通过少数几个紧密相关的对象来完成任务。迪米特法则强调对象之间的松耦合,减少对象之间的依赖关系,使得系统更加灵活、可维护和可扩展。简而言之,一个对象不应该直接了解过多其他对象的内部细节,而是通过与少数对象通信来实现其功能。

6.2.代码实现

场景:假设有一个电商系统,其中包含 Customer(顾客)、Order(订单)和 Product(产品)三个类。按照迪米特法则,我们希望一个类在实现某个功能时,只与其直接相关的类进行交互,而不涉及其他类的内部细节。

using System;
using System.Collections.Generic;

// 产品类
public class Product
{
    public string Name { get; set; }

    public Product(string name)
    {
        Name = name;
    }
}

// 订单类
public class Order
{
    private List<Product> products;

    public Order()
    {
        products = new List<Product>();
    }

    // 添加产品到订单
    public void AddProduct(Product product)
    {
        products.Add(product);
    }

    // 获取订单中的产品列表
    public List<Product> GetProducts()
    {
        return products;
    }
}

// 顾客类
public class Customer
{
    private string name;

    public Customer(string name)
    {
        this.name = name;
    }

    // 下订单的方法
    public void PlaceOrder(Order order)
    {
        Console.WriteLine($"{name} 下了一个订单,包含以下产品:");

        // 获取订单中的产品列表
        List<Product> products = order.GetProducts();

        // 打印订单中的产品信息
        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }
    }
}

// 客户端代码
class Program
{
    static void Main()
    {
        // 初始化产品
        Product product1 = new Product("商品1");
        Product product2 = new Product("商品2");

        // 初始化订单,并添加产品
        Order order = new Order();
        order.AddProduct(product1);
        order.AddProduct(product2);

        // 初始化顾客
        Customer customer = new Customer("张三");

        // 顾客下订单
        customer.PlaceOrder(order);
    }
}

例子中,Customer 类的 PlaceOrder 方法只与 Order 类直接交互,而没有直接涉及 Product 类的内部细节。这样符合迪米特法则,使得类之间的依赖关系更加清晰,提高了系统的灵活性。

6.3.优缺点

优点:

1. 减少耦合度:LoD有助于减少对象之间的直接依赖关系,降低了耦合度。一个对象只与其直接相关的对象通信,而不需要知道其他对象的内部结构。

2. 提高灵活性:由于对象之间的依赖关系减少,系统更具灵活性。当一个对象的内部结构发生变化时,不会影响到与之通信的其他对象,从而提高了系统的可维护性和可扩展性。

3. 提高可维护性:由于对象之间的依赖关系更为简单和清晰,代码更易于理解和维护。当需要修改某个对象时,只需关注直接相关的部分,而不需要考虑其他对象的影响。

4. 降低错误风险:减少了对象之间的直接交互,降低了出现错误的风险。每个对象只需要关注自己的责任,减少了潜在的错误来源。

缺点:

1. 可能导致过多的中介类:在遵循LoD的情况下,为了实现对象之间的间接通信,可能需要引入中介类。这可能导致系统中的类数量增加,从而增加了代码的复杂性。

2. 可能引入过多的接口:为了降低对象之间的直接依赖关系,可能需要定义多个小接口。这可能导致接口的数量增加,需要权衡好接口的设计。

3. 可能影响性能:为了遵循LoD,可能需要通过多次调用间接获取所需信息。这可能导致一些性能上的损失,需要在实际应用中进行权衡。

6.4.使用场景

1. 对象间的松耦合:当一个对象需要与其他对象进行通信,但希望减少直接依赖关系,使得对象之间更松耦合时,可以使用迪米特法则。

2. 系统的可维护性和可扩展性需求:当系统需要具备较好的可维护性和可扩展性时,迪米特法则有助于降低对象之间的依赖关系,减少系统中的耦合,使得修改和扩展更为容易。

3. 防止错误蔓延:当一个对象的内部结构发生变化时,迪米特法则有助于防止这种变化蔓延到与之直接通信的其他对象,从而降低了出现错误的风险。

4. 避免紧耦合的子系统:在大型系统中,通过使用迪米特法则,可以避免形成紧耦合的子系统,使得系统更易于理解、维护和扩展。

5. 提高系统的灵活性:当系统需要更灵活地应对变化和需求时,迪米特法则有助于确保对象之间的关系相对简单和清晰,从而提高系统的灵活性。

迪米特法则适用于需要降低对象之间直接依赖关系、提高系统灵活性和可维护性的场景。在设计中,遵循迪米特法则可以帮助构建更松耦合、更容易扩展和维护的系统。

 

标签:六大,原则,子类,代码,系统,接口,void,设计模式,public
From: https://www.cnblogs.com/hxjcore/p/17958535

相关文章

  • 我所关注的几个spring设计模式
    Spring框架中实现了许多的设计模式,而且都是非常优先的实现,这些值得我们学好好习。不过话说回来,我觉得自己只要关注几个即可:单例工厂代理适配器观察者委派在各种设计模式中,适配器和装饰器、代理模式其实都很类似,只是侧重点不同而已。spring的设计模式应用的很好,但spring......
  • 设计模式 经典问题
    目录策略模式和简单工厂模式的区别策略模式的类图为什么采用聚合简单工厂模式的类图为什么采用关联表示策略模式和简单工厂模式的区别策略模式和简单工厂模式是两种不同的设计模式,它们在用途和实现上有所不同。简单工厂模式是一种创建型模式,用于创建特定类型的对象。它通过一......
  • JavaGuide 设计模式
    JavaGuide设计模式1.软件设计原则设计原则名称简单定义开闭原则对扩展开放,对修改关闭单一职责原则一个类只负责一个功能领域中的相应职责里氏替换原则所有引用基类的地方必须能透明地使用其子类的对象依赖倒置原则依赖于抽象,不能依赖于具体实现接......
  • Spring Boot 整合工厂设计模式完成服务调用的一种实现
    工厂模式是一种创建型设计模式,其主要目的是提供一个创建对象的接口,但将具体类的实例化延迟到子类中。这样,客户端代码就不需要知道要实例化的具体类,只需要知道使用的工厂接口。项目结构如下代码实例如下所有水果都要实现这个接口publicinterfaceAllFruits{voideatFru......
  • 摆脱复杂图谱术语,7个原则搞定Schema建模
    前言在OpenSPG最新发布的0.0.2版本中,为了方便大家更好地理解和应用OpenSPG构建知识图谱,发布了知识建模最佳实践的7个指导原则。本文我们结合蚂蚁域内的多个业务场景,举例说明结合SPG规范的结构与语义解耦的知识建模及schema设计方法。OpenSPGGitHub:https://github.com/OpenSPG/o......
  • paxos协议之衍生协议:Raft协议的简述、协议模型、一致性算法、脑裂问题处理、选举流程
    raft简述raft协议中节点有三种状态leader、follower、candidate(候选人),leader复制日志的管理、客户端的新增更新请求,然后复制到follower节点,如果leader出现故障则follower就会重新选举,新增等操作若被follower所接收则会进行重定向转给leader,follower只负责客户端的读请求。有两......
  • java中 Happens-Before 原则
    前言并发问题有三个根本原因:cpu缓存导致可见性问题线程切换导致原子性问题:线程切换是发生于任何一条cpu指令级别的,而不是高级语言中的语句,例如i++是三个cpu指令编译器优化导致有序性问题CPU缓存导致可见性问题与Java内存模型(JMM)的问题实际上是两个相互关联的概念。CPU......
  • 设计模式—行为型模式之中介者模式
    设计模式—行为型模式之中介者模式中介者模式(MediatorPattern):用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,减少对象间混乱的依赖关系,从而使其耦合松散,而且可以独立地改变它们之间的交互。对象行为型模式。包含以下角色:Mediator:抽象中介者......
  • 设计模式--简单工厂模式
    目录概念使用场景概念简单工厂模式是一种创建型设计模式,它提供了一个创建对象的接口,但允许子类决定实例化哪个类。在C++中,简单工厂模式可以通过一个工厂类来实现,该工厂类负责根据输入参数创建不同的对象实例。下面是一个简单的C++示例来说明简单工厂模式的实现:#include<iostr......
  • Spring 中的设计模式详解
    1、控制反转(IoC)和依赖注入(DI)IoC是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。 控制反转怎么理解呢? 举个例子:"对象a依赖了对象b,当对象a需要使用对象b的时候必须自己去创建。但是当系统引入了IOC容器后,对象a和对象b之前就失去了直接的......