设计模式六大原则
目录
- 一、单一职责原则——SRP
- 1、作用
- 2、基本要点
- 3、举例
- 二、开放封闭原则——OCP
- 1、作用
- 2、基本要点
- 3、举例
- 三、里氏替换原则——LSP
- 1、作用
- 2、基本要点
- 3、举例
- 四、依赖倒置原则——DLP
- 1、作用
- 2、基本要点
- 3、举例
- 五、迪米特法则——LoD
- 1、作用
- 2、基本要点
- 3、举例
- 六、接口隔离原则——ISP
- 1、作用
- 2、基本要点
- 3、举例
一、单一职责原则——SRP
📌定义: 一个类应该仅有一个引起它变化的原因。
1、作用
- 提高可维护性
- 提高可扩展性
- 降低耦合度
2、基本要点
- 一个类只负责一个职责, 类的设计应该避免包含过多的功能
- 高内聚,低耦合:类的内部元素(方法、属性等)应该紧密相关,而与外部的类之间的依赖关系应该尽量降低
- 避免滥用接口: 单一职责原则并不要求每个方法都有自己的接口,而是要求类的设计在逻辑上应该是一致的,不涉及无关的功能。
3、举例
考虑一个 User
类,用于表示用户信息,例如用户名和密码。如果我们遵循单一职责原则,这个类应该只负责用户的信息表示,而不涉及与用户认证相关的逻辑。
// 不遵循单一职责原则的例子
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 不应该包含与用户认证相关的逻辑
public boolean authenticateUser(String enteredPassword) {
return this.password.equals(enteredPassword);
}
}
上述例子中,User
类不仅表示用户的信息,还包含了用户认证的逻辑。这违反了单一职责原则。更好的做法是将用户认证的逻辑移到一个独立的类中:
// 遵循单一职责原则的例子
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 只负责用户信息的表示,不包含认证逻辑
}
public class Authenticator {
// 用户认证逻辑
public boolean authenticateUser(User user, String enteredPassword) {
return user.getPassword().equals(enteredPassword);
}
}
二、开放封闭原则——OCP
📌定义: 类、模块、函数应该是可以扩展的,但是不可修改
一个常见的实现开放封闭原则的方式是通过使用抽象和接口。
1、作用
- 提高可维护性: 通过遵循开放封闭原则,系统更容易进行扩展而不需要修改已有的代码,从而降低了代码的维护成本。
- 提高可扩展性: 新功能的引入不应该影响已有代码,只需通过扩展来添加新功能,这提高了系统的可扩展性。
- 降低风险: 通过不修改已有的代码,减少了引入新功能时对系统稳定性的影响,从而降低了系统的风险。
2、基本要点
- 对扩展开放,对修改封闭: 意味着已有的代码不应该被修改,新功能的引入应该通过添加新的代码实现。
- 通过抽象实现: 通过使用抽象(接口或抽象类)来定义系统的可扩展部分,而具体的实现则通过继承或实现抽象来完成。
- 避免直接依赖具体实现: 在代码中尽量避免直接依赖具体的实现,而是依赖于抽象。
3、举例
例如,通过定义接口或抽象类,你可以在不修改现有代码的情况下创建新的实现,并通过接口的引用来使用不同的实现。
// 定义图形接口
public interface Shape {
void draw();
}
// 实现圆形
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}
// 实现矩形
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
}
// 图形绘制系统
public class DrawingSystem {
// 绘制图形的方法,不依赖于具体的图形类型
public void drawShape(Shape shape) {
shape.draw();
}
}
现在就可以实现不需要修改现有的 DrawingSystem
类,而增加新的功能。
这符合开放封闭原则,通过扩展接口和实现类来引入新的功能,而不影响已有的代码。
三、里氏替换原则——LSP
📌定义: 在任何时候,子类应该能够替代父类而不引起程序的错误。
1、作用
- 里氏替换原则是实现开放封闭原则的重要方式之一。
- 它克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
2、基本要点
- 子类必须保留父类的行为,即子类应该覆盖或实现父类的方法,而不应该删除、修改或使其行为不一致。
- 子类可以具有自己的特定行为,但不能违反父类的约定
- 子类重载父类方法的前置条件1可以比父类宽松,后置条件2需要比父类更加严格或者相等。
3、举例
// 符合里氏替换原则的例子
// 父类
public class Shape {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
// 子类-长方形
public class Rectangle extends Shape {
// 可以保留父类的行为,也可以有自己的特定行为
}
// 另一个子类-正方形
public class Square extends Shape {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 正方形的宽高一致
}
@Override
public void setHeight(int height) {
super.setWidth(height); // 正方形的宽高一致
super.setHeight(height);
}
}
// 在使用父类的地方可以使用其任何子类
public class ExampleUsage {
public void processShape(Shape shape) {
int area = shape.calculateArea();
// 处理其他逻辑
}
}
关于里氏替换原则的例子,最有名的是“正方形不是长方形”。当然,生活中也有很多类似的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。
四、依赖倒置原则——DLP
📌定义:高层模块不应该依赖低层模块,二者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象
在Java中,抽象就是抽象类/接口;细节就是实现类;高层模块就是调用端;底层模块就是具体实现类。
也就是说,模块间依赖是通过抽象发生;实现类之间没有依赖关系,所有的依赖关系通过接口/抽象类产生。
1、作用
- 降低类间的耦合性,提高系统的稳定性。
- 可以减少并行开发引起的风险。
2、基本要点
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型应是接口或者是抽象类。
- 任何类都不应该从实现类派生。
- 使用继承时尽量遵循里氏替换原则
3、举例
// 抽象
public interface DataService {
String getData();
}
// 低层模块(数据服务)实现了抽象
public class DatabaseService implements DataService {
@Override
public String getData() {
// 实际的数据获取逻辑
return "Data from database";
}
}
// 高层模块(业务逻辑)依赖于抽象
public class BusinessLogic {
private DataService dataService;
// 通过构造函数注入依赖
public BusinessLogic(DataService dataService) {
this.dataService = dataService;
}
public void doSomething() {
// 使用抽象而不是直接依赖于具体实现
String data = dataService.getData();
// 处理业务逻辑
}
}
五、迪米特法则——LoD
又叫作最少知识原则(Least Knowledge Principle,LKP)
📌定义:只与你的“直接朋友”交谈,不跟“陌生人”说话。也就是说:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。
迪米特法则中的“朋友
”:指的是直接的成员、局部变量、方法的参数以及当前对象创建的对象等。
迪米特原则的核心思想:
- 一个对象应该对其他对象有最少的了解。
- 类与类之间的关系应该尽量降低耦合。
1、作用
- 降低耦合度
- 提高模块的独立性: 迪米特原则鼓励将系统划分为独立的模块,每个模块只与少数几个其他模块发生直接的联系。
- 增强系统的可维护性: 当系统需要进行修改或扩展时,只需要关注当前对象的直接朋友,而不需要关注对象之间的详细关系。
2、基本要点
- 只与朋友交流: 一个对象应该对其他对象有最少的了解,只与朋友交流。
- 不要轻易访问非直接关联的对象的内部信息: 对于一个对象,只应该调用与之直接关联的对象的方法,而不要去调用非直接关联对象的方法。这有助于降低对象之间的耦合度。
- 如果需要一个对象调用另一个对象的某个方法,可以通过第三者转发这个调用
3、举例
考虑一个购物车系统,其中包含顾客、购物车和商品三个类。
根据迪米特原则,顾客类应该尽可能不直接获取商品类的信息,而是通过购物车类来处理商品的添加和删除等操作。
这样可以降低顾客类对商品类的依赖,提高系统的灵活性和可维护性。
// 商品类
public class Product {
private String name;
public Product(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 购物车类
public class ShoppingCart {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
public void removeProduct(Product product) {
products.remove(product);
}
}
// 顾客类
public class Customer {
private ShoppingCart shoppingCart;
public Customer(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
public void addToCart(Product product) {
shoppingCart.addProduct(product);
}
public void removeFromCart(Product product) {
shoppingCart.removeProduct(product);
}
}
六、接口隔离原则——ISP
📌定义:一个类对另一个类的依赖应该建立在最小的接口上。
该原则还有另一个定义:一个类不应该被强迫依赖于它不使用的接口。
简而言之,接口隔离原则要求一个类对其他类的依赖关系应该建立在最小的接口上,而不是依赖于它不需要的接口。这样可以避免类对不必要的接口产生依赖,减少耦合,提高系统的灵活性和可维护性。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
1、作用
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
2、基本要点
- 一个类不应该依赖它不需要的接口。
- 客户端不应该被强制依赖于它不使用的方法。
- 接口设计应该精简明了,避免臃肿的接口。
3、举例
考虑一个接口设计的例子,有一个动物接口 Animal
,其中包含了两个方法:fly()
和 swim()
。
public interface Animal {
void fly();
void swim();
}`
现在,有两个类 Bird
和 Fish
分别实现了这个接口。但是,鸟类不需要实现 swim()
方法,鱼类不需要实现 fly()
方法,这就违反了接口隔离原则。
public class Bird implements Animal {
@Override
public void fly() {
System.out.println("Bird is flying");
}
@Override
public void swim() {
// Bird does not need to swim, but forced to implement the method
System.out.println("Bird is swimming");
}
}
public class Fish implements Animal {
@Override
public void fly() {
// Fish does not need to fly, but forced to implement the method
System.out.println("Fish is flying");
}
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
为了符合接口隔离原则,可以将 Animal
接口拆分为两个接口:IFlyable
和 ISwimmable
。
public interface IFlyable {
void fly();
}
public interface ISwimmable {
void swim();
}
public class Bird implements IFlyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
public class Fish implements ISwimmable {
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
通过这样的设计,Bird
只需要实现 IFlyable
接口,而 Fish
只需要实现 ISwimmable
接口,避免了不必要的方法实现。这符合接口隔离原则,使得接口更加精简和灵活。
前置条件(Input Preconditions):当子类重载(overload)父类的方法时,子类的方法的输入参数要比父类的方法更宽松。这意味着子类的方法可以接受更多的输入参数类型,但不能缩小输入参数类型的范围。 ↩︎
后置条件(Output Postconditions):当子类覆写(override)或重载父类的方法时,子类方法的返回值类型和行为要比父类方法更严格或相等。这确保了子类方法的行为不会违反父类方法的契约,而是更强或相同。 ↩︎