面向对象设计的七大原则(实战)

1. 里氏代换原则

  • 里氏替换原则(Liskov Substitution Principle ,LSP)

只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

(1)不应该在代码中出现if/else之类对派生类类型进行判断的条件。

js 复制代码
abstract class Shape {
    abstract double area();
}
//基类Shape的 派生类`Rectangle` 
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    double area() {
        return width * height;
    }
}
//基类Shape的派生类 `Circle`
class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = new Shape[2];
        shapes[0] = new Rectangle(2, 3);
        shapes[1] = new Circle(5);
        //没有对派生类类型进行判断的条件,而是直接调用了`calculateTotalArea()`方法
        double totalArea = calculateTotalArea(shapes);
        System.out.println("Total area: " + totalArea); // 输出:Total area: 47.14
    }

    public static double calculateTotalArea(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.area();
        }
        return totalArea;
    }
}

派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说 把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

里氏替换原则(LSP)是使代码符合开闭原则的一个重要保证。

子类可以扩展父类的功能,但不能改变父类原有的功能。

子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

子类中可以增加自己特有的方法。

当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。

当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。

派生类和子类 区别与联系

派生类和子类 都是由基类或父类派生出来的,它们继承了基类的属性和方法。不过,派生类可能包含了更多的特性和行为,因为它可以在继承的基础上添加新的属性和方法,或者覆盖基类中的方法。

派生类可以是从基类直接继承的类,也可以是从另一个派生类继承的类,即存在多级继承的情况。而子类通常指的是直接从父类继承的类,强调的是直接的继承关系。

2. 迪米特原则(最少知道原则)

迪米特原则(最少知道原则)(Law of Demeter ,LoD)

1)一个软件实体应当尽可能少地与其他实体发生相互作用。

2)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

迪米特原则 优点

迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系

示例:

js 复制代码
// 定义一个中介类
class Mediator {
    private ColleagueA colleagueA;
    private ColleagueB colleagueB;

    public void setColleagueA(ColleagueA colleagueA) {
        this.colleagueA = colleagueA;
    }

    public void setColleagueB(ColleagueB colleagueB) {
        this.colleagueB = colleagueB;
    }

    public void doSomething() {
        colleagueA.doSomething();
        colleagueB.doSomething();
    }
}

// 定义两个同事类
class ColleagueA {
    public void doSomething() {
        System.out.println("Colleague A is doing something");
    }
}

class ColleagueB {
    public void doSomething() {
        System.out.println("Colleague B is doing something");
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        ColleagueA colleagueA = new ColleagueA();
        ColleagueB colleagueB = new ColleagueB();
        Mediator mediator = new Mediator();
        mediator.setColleagueA(colleagueA);
        mediator.setColleagueB(colleagueB);
        mediator.doSomething(); // 输出:Colleague A is doing something
                               //       Colleague B is doing something
    }
}

迪米特原则 缺点

应用 迪米特原则 缺点有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。

3. 开闭原则

  • 开闭原则(The Open-Closed Principle ,OCP)
  • 开闭原则:软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。

开闭原则的实现方法: 应该对软件系统中的不变的部分加以抽象,在面向对象的设计中 (1)可以把这些不变的部分加以抽象成不变的接口 ,这些不变的接口可以应对未来的扩展; (2)接口的最小功能设计原则 。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现; (3)模块之间的调用通过抽象接口 进行,这样即使实现层发生变化,也无需修改调用方的代码。 接口可以被复用,但接口的实现却不一定能被复用。

模块之间的调用通过抽象接口进行 调用示例

go 复制代码
   模块之间的调用可以通过抽象接口进行,降低模块之间的耦合度,提高代码的可维护性和可扩展性。
   假设我们有两个模块,一个是`UserModule`,另一个是`OrderModule`。`UserModule`负责用户相关的操作,`OrderModule`负责订单相关的操作。
   我们可以定义一个抽象接口`IUserService`,用于描述用户服务的操作,然后在`UserModule`中实现这个接口。
   在`OrderModule`中,我们可以通过依赖注入的方式获取`IUserService`的实例,然后调用其方法来完成用户相关的操作。

首先,定义抽象接口IUserService: 不变的部分加以抽象成不变的接口 + 最小功能设计原则

csharp 复制代码
public interface IUserService
{
    User GetUserById(int id);
    void UpdateUser(User user);
}

然后,在UserModule中实现这个接口:

arduino 复制代码
public class UserService : IUserService
{

    //接口可以被复用,但接口的实现却不一定能被复用。
    public User GetUserById(int id)
    {
        // 实现获取用户的逻辑
    }

    public void UpdateUser(User user)
    {
        // 实现更新用户的逻辑
    }
}  

接下来,在OrderModule中通过依赖注入的方式获取IUserService的实例,并调用其方法:

csharp 复制代码
public class OrderService
{
    private readonly IUserService _userService;

    public OrderService(IUserService userService)
    {
        _userService = userService;
    }

    public void CreateOrder(Order order)
    {
        // 创建订单的逻辑

        // 获取用户信息
         //这里实现了模块之间的调用通过抽象接口进行。 接口不变,实现可扩展
        User user = _userService.GetUserById(order.UserId);
        // 根据用户信息处理订单逻辑
    }
}

注意:构建100%满足开闭原则的软件系统是相当困难的,这就是开闭原则的相对性。

抽象接口在模块间调用中的作用是什么?

定义模块间互相作用方式的机制

象接口有助于实现开闭原则

使用接口还可以促进多态性的实现

使用接口和多态性实现的示例:

假设我们有一个动物类Animal,它有两个子类DogCat。我们可以定义一个抽象接口IAnimal,其中包含一个方法Speak(),用于让动物发出声音。然后,在DogCat类中分别实现这个方法。

最后,我们可以使用多态性来调用Speak()方法,而不需要关心具体是哪个子类实现了该方法。

js 复制代码
public interface IAnimal
{
    void Speak();
}

public class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Meow!");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        IAnimal dog = new Dog();
        IAnimal cat = new Cat();

        MakeAnimalSpeak(dog); // 输出 "Woof!"
        MakeAnimalSpeak(cat); // 输出 "Meow!"
    }

    public static void MakeAnimalSpeak(IAnimal animal)
    {
      //多态性的体现 :类可以实现一个或多个接口,从而拥有接口中定义的方法。
        animal.Speak();
    }
}

通过使用多态性,我们可以在MakeAnimalSpeak()方法中统一处理不同类型的动物 ,而不需要关心具体是哪个子类实现了Speak()方法。 在Java中,多态性的实现主要依赖于以下两个机制:

  1. 继承:子类继承了父类的属性和方法,可以重写父类的方法,实现不同的功能。
  2. 接口:类可以实现一个或多个接口,从而拥有接口中定义的方法。

4. 单一职责原则

单一职责原则原则的核心含意是:只能让一个类/接口/方法有且仅有一个职责。 示例

js 复制代码
// 不遵循单一职责原则的类
class User {
    private String name;
    private int age;
    private String email;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setEmail(String email) {
        this.email = email;
    }
    // 违反单一职责: 一个类应该只负责一项职责
    public void save() {
        // 保存用户信息的逻辑
    }
    // 违反单一职责: 一个类应该只负责一项职责
    public void sendEmail() {
        // 发送邮件的逻辑
    }
}

// 遵循单一职责原则的类
class User {
    private String name;
    private int age;
    private String email;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

class UserRepository {
    public void save(User user) {
        // 保存用户信息的逻辑
    }
}

class EmailService {
    public void sendEmail(User user) {
        // 发送邮件的逻辑
    }
}

5. 组合/聚合复用原则

组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP) 尽量使用组合/聚合,不要使用类继承。 聚合表示整体和部分的关系,表示"拥有"。组合则是一种更强的"拥有",部分和整体的生命周期一样。

组合的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。

组合设计原则(Composite Reuse Principle):组合是值的聚合(Aggregation by Value)

组合设计原则(Composite Reuse Principle)是指通过将对象组合成树形结构来表示部分整体的层次结构,使得客户端可以统一对待个别对象和组合对象

聚合是引用的聚合(Aggregation by Reference)。

聚合设计原则(Aggregate Design Principle)是指将对象组合成树形结构,使得客户端可以统一对待个别对象和组合对象。 定义了一个抽象组件类Component,它有两个子类:叶子节点类Leaf和组合节点类CompositeLeaf类表示单个对象,而Composite类表示由多个对象组成的复合对象。 客户端可以通过调用operation()方法来统一处理单个对象和复合对象。

js 复制代码
// 定义一个抽象组件类
abstract class Component {
    public abstract void operation();
}

// 定义叶子节点类
class Leaf extends Component {
    private String name;

    public Leaf(String name) {
        this.name = name;
    }

    @Override
    public void operation() {
        System.out.println("Leaf " + name + " is doing something");
    }
}

// 定义组合节点类: 组合关系一个 组合由多个组件构成!
class Composite extends Component {
    private List<Component> children = new ArrayList<>();

    public void add(Component component) {
        children.add(component);
    }

    public void remove(Component component) {
        children.remove(component);
    }

    @Override
    public void operation() {
        for (Component child : children) {
            child.operation();
        }
    }
}


// 聚合关系:
public class Main {
    public static void main(String[] args) {
        Composite root = new Composite();
        root.add(new Leaf("Engine"));
        root.add(new Leaf("Wheel"));

        Composite car = new Composite();
        car.add(root);
        car.add(new Leaf("Body"));

        car.operation(); // 输出:Leaf Engine is doing something
                         //      Leaf Wheel is doing something
                         //      Leaf Body is doing something
    }
}

组合和聚合的区别与联系

  • 组合:是一种更强的关系,表示部分和整体的关系,其中部分的生命周期依赖于整体。在组合中,整体负责部分的创建和销毁,部分不能脱离整体存在。例如,一辆车和它的发动机之间的关系就是组合,发动机是车的一部分,不能单独存在。
  • 聚合:相对于组合来说,是一种更弱的关系,也表示部分和整体的关系,但部分的生命周期不依赖于整体,部分可以在整体之间进行共享。例如,一个人和他所乘坐的汽车之间的关系就是聚合,人可以下车,汽车可以载不同的人。

面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

6. 依赖倒置原则

依赖倒置原则(Dependency Inversion Principle ,DIP)

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
  • 抽象不应该依赖于细节,细节应该依赖于抽象
  • 针对接口编程,不要针对实现编程
  • 类与类之间都通过抽象接口层来建立关系。

示例:

js 复制代码
// 定义一个抽象接口
interface Reader {
    void read();
}

// 定义一个具体实现类
class TxtReader implements Reader {
    @Override
    public void read() {
        System.out.println("Reading a txt file");
    }
}

// 定义一个高层模块
class User {
    // 高层模块不应该依赖于低层模块,二者都应该依赖于抽象  , 类与类之间都通过抽象接口层来建立关系。
    private Reader reader;

    //高层模块不应该依赖于低层模块(TxtReader), 应该依赖于抽象接口 (Reader)
    public User(Reader reader) {
        this.reader = reader;
    }
    // 执行读取动作
    public void performRead() {
        reader.read();
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        Reader txtReader = new TxtReader();
        User user = new User(txtReader);
        user.performRead(); // 输出:Reading a txt file
    }
}

7. 接口分隔原则

接口分隔原则(Interface Segregation Principle ,ISP)

不能强迫用户去依赖那些他们不使用的接口。 换句话说,使用多个专门的接口比使用单一的总接口总要好。

接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。

实现接口隔离原则方式:

  • 接口拆分:将一个大而全的接口拆分成多个小而专的接口,每个接口只包含一组相关的方法。这样,实现类可以根据自己的需要选择实现哪些接口,而不是被迫实现一些它不需要的方法。

  • 委托模式:通过创建一个新类来实现一个接口的某个子集 (一个大接口 按照需要 拆分成多个接口),客户端可以只依赖于这个新类,而不是整个接口。这种方式可以让客户端更加灵活地选择所需的功能。

  • 多继承:虽然Java不支持类的多继承,但支持接口的多继承。一个类可以实现多个接口,从而组合不同接口的行为,这样可以根据需要组合不同的接口,实现更灵活的功能。

例如,如果有一个接口InterfaceC包含了方法methodA(), methodB(), 和 methodC()

ClassA只需要methodA()

ClassB只需要methodB()methodC()

InterfaceC拆分为两个接口InterfaceAInterfaceB

InterfaceA包含methodA()

InterfaceB包含methodB()methodC()

ClassAClassB就可以根据自己的需要选择实现InterfaceAInterfaceB,而不是被迫实现所有方法。

优点

如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。

相关推荐
WaaTong6 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_743048446 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries8 分钟前
Java字节码增强库ByteBuddy
java·后端
小灰灰__28 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭31 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果1 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql