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
,它有两个子类Dog
和Cat
。我们可以定义一个抽象接口IAnimal
,其中包含一个方法Speak()
,用于让动物发出声音。然后,在Dog
和Cat
类中分别实现这个方法。
最后,我们可以使用多态性来调用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中,多态性的实现主要依赖于以下两个机制:
- 继承:子类继承了父类的属性和方法,可以重写父类的方法,实现不同的功能。
- 接口:类可以实现一个或多个接口,从而拥有接口中定义的方法。
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
和组合节点类Composite
。 Leaf
类表示单个对象,而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
拆分为两个接口InterfaceA
和InterfaceB
InterfaceA
包含methodA()
InterfaceB
包含methodB()
和methodC()
。
ClassA
和ClassB
就可以根据自己的需要选择实现InterfaceA
或InterfaceB
,而不是被迫实现所有方法。
优点
如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。