当谈论Java编程和面向对象设计时,通常提到的七个设计原则是指五个SOLID原则加两个独立的设计原则,这是一组用于创建更具扩展性和维护性的软件设计的原则。这七个原则是:
这些原则有助于编写可维护、灵活和可扩展的代码,是面向对象设计的基本准则。合成复用原则 和 最小知识原则是两个独立的设计原则,并不属于 SOLID 原则的范畴。
当遵循单一职责原则时,一个类应该只有一个引起它变化的原因。这意味着一个类应该负责一项明确定义的任务,而不应该具有多个不相关的职责。以下是一个示例:
假设有一个图形类,既负责绘制图形,又负责计算图形的面积。
public class ShapeDrawerAndAreaCalculator {
public void drawShape() {
// 绘制图形的逻辑
}
public void calculateArea() {
// 计算图形面积的逻辑
}
}
在这个例子中,ShapeDrawerAndAreaCalculator
类承担了绘制图形和计算图形面积的多个职责,这会导致以下问题:
为了遵循单一职责原则,你应该将不同的职责分离为不同的类。在这个例子中,这两种职责可以分开成两个类,一个负责绘制图形,另一个负责计算图形的面积。
// 负责绘制图形
public class ShapeDrawer {
public void drawShape() {
// 绘制图形的逻辑
}
}
// 负责计算图形面积
public class AreaCalculator {
public void calculateArea() {
// 计算图形面积的逻辑
}
}
通过这种方式,每个类都有一个清晰的职责,提高了代码的可维护性、可测试性和灵活性。这是单一职责原则的一个简单示例。
当遵循开闭原则时,软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着你应该能够通过扩展现有代码来添加新功能,而不是修改已有代码。以下是一个示例:
假设你正在编写一个图形绘制应用,最初只支持绘制矩形。你可能会创建一个类如下:
public class Rectangle {
public void drawRectangle() {
// 绘制矩形的逻辑
}
}
这个类负责绘制矩形,但随着时间的推移,你需要添加对绘制其他形状(例如圆形)的支持。如果不遵循开闭原则,你可能会修改 Rectangle
类以支持新的形状:
public class Rectangle {
public void drawRectangle() {
// 绘制矩形的逻辑
}
public void drawCircle() {
// 绘制圆形的实现
}
}
这种做法破坏了开闭原则,如果需要新增一个三角形,按照开闭原则,我们不应该修改原有的代码,而是通过扩展来实现。我们可以创建一个抽象类或接口 Shape 来表示不同形状的图形,并让具体的图形类去实现它。
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
// 绘制圆形的逻辑
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
// 绘制矩形的逻辑
}
}
// 新增的三角形类
public class Triangle implements Shape {
@Override
public void draw() {
// 绘制三角形的逻辑
}
}
现在,我们通过创建新的 Triangle 类来实现对三角形的绘制,而不需要修改其他类。这样,通过添加新的形状类,我们实现了对图形绘制器的扩展,同时保持了原有代码的稳定性,符合开闭原则。这种设计方式可以有效地支持代码的扩展和维护,使得系统更具弹性和可扩展性。
里氏替换原则(Liskov Substitution Principle - LSP)是面向对象编程中的五个SOLID原则之一,它强调子类应该能够替代其基类而不引起错误。更具体地说,它提出以下要求:
LSP的目标是确保通过基类类型引用的对象,无论是基类还是子类,都可以正确地工作,而不引发错误或意外行为。这有助于提高代码的可重用性和可扩展性。以下是一个示例来说明LSP:
假设有一个图形类层次结构,其中有一个基类 Shape
和两个子类 Circle
和 Rectangle
。按照LSP,任何使用 Shape
类的地方都应该能够无缝地使用 Circle
或 Rectangle
对象。
class Shape {
// 公共图形属性和方法
}
class Circle extends Shape {
// 圆形特有的属性和方法
}
class Rectangle extends Shape {
// 矩形特有的属性和方法
}
根据LSP,可以这样使用这些对象:
Shape circle = new Circle();
Shape rectangle = new Rectangle();
在这种情况下,无论是基类 Shape
还是子类 Circle
和 Rectangle
都可以正确地工作,因为它们都遵循LSP的原则。
总之,里氏替换原则有助于确保对象之间的替代性和互换性,从而提高代码的可扩展性和可维护性。它强调了对继承关系的正确建模和设计。具体来说,里氏替换原则强调子类应该能够替代其基类,这涉及到多态的概念。多态允许你使用基类类型的引用来引用子类对象,然后根据实际运行时类型来动态调用方法。这使得代码更具灵活性,支持多态性。
在上述示例中,我们使用了多态性,允许使用 Shape
类的引用来引用 Circle
和 Rectangle
对象,而不需要知道具体的子类类型。这是里氏替换原则的一个示例,它强调了多态如何帮助实现对象的替代性。
因此,如果子类重写了父类的方法,但是重写后的行为与原有的契约不符合或者破坏了原有的功能,那就违反了里氏替换原则。另一方面,如果子类仅仅是扩展了父类的方法,添加了新的功能而不破坏原有行为,那么就是符合里氏替换原则的。
接口隔离原则(Interface Segregation Principle,ISP)要求客户端不应该依赖于它们不使用的接口。这意味着接口应该小而精确,而不是大而臃肿,以确保客户端只需知道与其相关的方法。让我通过一个示例来说明接口隔离原则:
假设你正在创建一个多媒体播放器应用,其中有不同类型的媒体文件(音频和视频)。你可能会设计一个媒体播放器接口如下:
public interface MediaPlayer {
void playAudio();
void playVideo();
void pause();
void stop();
}
然后你创建了一个实现了该接口的媒体播放器类。这个接口包含了音频和视频播放的方法,但问题是不是所有的媒体播放器都需要实现这两个方法。
如果你创建一个仅支持音频的媒体播放器,它仍然需要实现 playVideo()
方法,尽管它根本不会用到这个方法。这违反了接口隔离原则。
更好的方式是将接口拆分成多个小的接口,每个接口代表一个独立的功能。例如:
public interface AudioPlayer {
void playAudio();
void pause();
void stop();
}
public interface VideoPlayer {
void playVideo();
void pause();
void stop();
}
现在,你的音频播放器只需实现 AudioPlayer
接口,而视频播放器只需实现 VideoPlayer
接口。这符合接口隔离原则,因为每个类只需实现与其相关的方法,不会被迫实现它们不需要的方法。这提高了代码的灵活性和可维护性,同时遵循了接口隔离原则。
依赖倒置原则(Dependency Inversion Principle - DIP)强调高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这意味着你应该编写代码以依赖于接口或抽象类,而不是依赖于具体的实现。让我通过一个示例来说明:
假设我们有一个通知服务,负责向用户发送通知消息,最初的设计可能是直接在高层模块(例如业务逻辑)中依赖于具体的发送实现,如下所示:
// 高层模块
public class NotificationService {
private SMSNotification smsNotification;
public NotificationService() {
this.smsNotification = new SMSNotification();
}
public void sendNotification(String message, String recipient) {
smsNotification.send(message, recipient);
}
}
// 低层模块
public class SMSNotification {
public void send(String message, String recipient) {
// 发送短信的具体实现
System.out.println("Sending SMS to " + recipient + ": " + message);
}
}
这个设计存在问题,因为高层模块 NotificationService
直接依赖于低层模块 SMSNotification
,违反了DIP。为了符合DIP,我们可以通过引入一个抽象接口,让高层模块依赖于抽象而不是具体的实现,如下所示:
// 抽象
public interface Notification {
void send(String message, String recipient);
}
// 低层模块实现抽象
public class SMSNotification implements Notification {
public void send(String message, String recipient) {
// 发送短信的具体实现
System.out.println("Sending SMS to " + recipient + ": " + message);
}
}
// 高层模块依赖于抽象
public class NotificationService {
private Notification notification;
public NotificationService(Notification notification) {
this.notification = notification;
}
public void sendNotification(String message, String recipient) {
notification.send(message, recipient);
}
}
现在,NotificationService 高层模块依赖于抽象 Notification 接口,而不是直接依赖于具体的 SMSNotification。这使得系统更灵活,可以轻松地扩展和替换不同的通知方式,比如邮件:
// 另一种低层模块实现抽象
public class EmailNotification implements Notification {
public void send(String message, String recipient) {
// 发送邮件的具体实现
System.out.println("Sending Email to " + recipient + ": " + message);
}
}
合成复用原则(Composition Over Inheritance - COI)鼓励使用组合(composition)而不是继承(inheritance)来实现代码的重用。让我通过一个示例来说明:
假设你正在编写一个手机类,可能手机可以拍照,显示,处理等。你可能会考虑使用继承来实现这些形状:
public class Phone {
public void takePhoto() {
// 拍照逻辑
}
public void display() {
// 显示逻辑
}
public void gyroscope() {
// 陀螺仪
}
}
public class Smartphone extends Phone{
// 省略方法实现
}
这种方式看起来没问题,但是如果说,中间某部分功能就没有,比如某个手机没陀螺仪或者其他功能,就会很多的冗余和耦合。这违反了合成复用原则,因为继承并不总是最好的代码重用方式。更好的方式是使用组合如下:
// 摄像头类
public class Camera {
public void takePhoto() {
// 拍照逻辑
}
}
// 屏幕类
public class Screen {
public void display() {
// 显示逻辑
}
}
// 处理器类
public class Gyroscope {
public void process() {
// 处理逻辑
}
}
public class Smartphone {
private Camera camera;
private Screen screen;
private Gyroscope gyroscope;
public Smartphone() {
this.camera = new Camera();
this.screen = new Screen();
this.gyroscope = new Gyroscope();
}
public void takePhoto() {
camera.takePhoto();
}
public void display() {
screen.display();
}
public void process() {
gyroscope.process();
}
// 可以添加其他手机功能的方法
}
现在,通过组合的方式将不同的功能组合到一个类中,提高了代码的灵活性、可维护性和可扩展性。比如你没有拍照功能,直接移除即可。不是说继承不能用,但是通常不是很通用的情况下,都是优先组合。
最小知识原则(也称为迪米特法则
)的主要重点在于减少对象之间的直接依赖关系,将依赖限制在最小的范围内,以降低代码的耦合度。这通常通过以下方式来实现:
这有助于确保对象之间的关系更加清晰,减少了代码中的隐藏依赖,使代码更容易理解、维护和修改。最小知识原则通常在大型、复杂的系统中更为重要,以确保代码的可维护性和可扩展性。在小型程序中,有时可能会看到最小知识原则与其他原则重叠,因为简化问题通常会导致更少的依赖关系。
当遵循最小知识原则时,对象应该限制其通信范围,只与直接的朋友互动,而不与陌生人互动。所谓的直接的朋友
是指以下对象:
最小知识原则的主要思想是,一个对象应该知道越少越好,只与它的直接朋友互动,而不与陌生对象互动。这有助于减少对象之间的耦合,提高代码的灵活性和可维护性。
下面是一个更清晰的示例:
public class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Order {
private Customer customer;
public Order(Customer customer) {
this.customer = customer;
}
// 不直接调用,而通过订单自身的方法
public String getCustomerName() {
return customer.getName();
}
}
在这个示例中,Order
类只与其直接的朋友 Customer
类互动,而不直接与任何其他对象(如邮件服务、数据库等)互动。这符合最小知识原则,减少了对象之间的依赖关系,使系统更具灵活性。
设计模式中的七大设计原则为面向对象设计提供了指导,帮助设计出灵活、可维护、可扩展的软件系统。它们强调了代码的模块化、松耦合性、高内聚性以及对变化的适应能力。在实际设计中,这些原则通常是相互关联的,一起使用以达到更好的设计效果,接下来,开始我们设计模式之旅。