前言
设计不是一个固定的技术,每个人写出的代码可能都不一样,他都有自己的设计方案和设计理念,设计模式只是提供了一套别人总结好的解决方案,我们写出来的代码可能并不是完全符合设计模式本身,但是只要它的思想和理念和某个设计模式相同,那么你就是对的,所以说我们只要写出优雅的、拓展性强的、可阅读的、测试性强的代码即可。
软件设计原则
当涉及到软件设计和开发原则时,有一些常见的原则和准则可以帮助我们编写高质量、可维护和可扩展的代码。以下是其中一些重要的原则和准则:
- 单一职责原则:一个类应该只有一个引起变化的原因,负责一项职责(Single Responsibility Principle,SRP)。
- 开放封闭原则:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭(Open-Closed Principle,OCP)。
- 里氏替换原则:子类型必须能够替换其基类型而不影响程序的正确性(Liskov Substitution Principle,LSP)。
- 接口隔离原则:客户端不应该强迫依赖于它们不需要的接口(Interface Segregation Principle,ISP)。
- 依赖倒置原则:高层模块不应该依赖于低层模块,二者都应该依赖于抽象(Dependency Inversion Principle,DIP)。
保持代码简单易懂,避免过度设计和复杂化,尽量保持代码的简洁性(Keep It Simple, Stupid)。
避免重复代码,将重复的逻辑抽象成可复用的组件或函数,提高代码的可维护性和可读性(Don't Repeat Yourself)。
一个对象应该尽量减少与其他对象之间的直接交互,应该只与其直接朋友进行通信。这样可以降低对象之间的耦合度,提高代码的可维护性和灵活性(Law of Demeter,LoD)。
这些原则和准则的目标是提高软件系统的质量、可维护性和可扩展性。它们强调了良好的设计实践和规范,使得代码更具可读性、可测试性和可维护性。
一、单一原则
1、简介
单一职责原则(Single Responsibility Principle,简称SRP),它要求一个类或模块应该只负责一个特定的功能。
- 这有助于降低类之间的耦合度,提高代码的可读性和可维护性。
- 我们可以把模块看作比类更加抽象的概念,类也可以看作模块。或者把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
2、例子
案例:
当应用单一责任原则时,我们可以看到代码的模块化和职责分离,每个类或模块专注于一个单一的职责。以下是一个示例,展示了一个简单的订单处理系统,其中有两个类:Order
和 EmailSender
。
// 使用单一责任原则的代码示例
public class Order {
private int orderId;
private String customerName;
private double totalAmount;
public Order(int orderId, String customerName, double totalAmount) {
this.orderId = orderId;
this.customerName = customerName;
this.totalAmount = totalAmount;
}
public void processOrder() {
// 处理订单的逻辑
// ...
sendConfirmationEmail();
}
private void sendConfirmationEmail() {
EmailSender emailSender = new EmailSender();
String message = "Dear " + customerName + ", your order with ID " + orderId + " has been processed.";
emailSender.sendEmail(customerName, "Order Confirmation", message);
}
}
public class EmailSender {
public void sendEmail(String recipient, String subject, String message) {
// 发送电子邮件的逻辑
// ...
}
}
在上述代码中,Order
类负责表示订单,并包含订单的处理逻辑。它的职责是处理订单和发送确认电子邮件。EmailSender
类专门负责发送电子邮件。
这样设计的好处是,Order
类只关注订单的处理逻辑,不涉及与电子邮件发送相关的代码。这遵循了单一责任原则,使得代码更加模块化、可维护和可测试。
现在,让我们看一下如果不遵循单一责任原则会发生什么:
// 没有使用单一责任原则的代码示例
public class Order {
private int orderId;
private String customerName;
private double totalAmount;
public Order(int orderId, String customerName, double totalAmount) {
this.orderId = orderId;
this.customerName = customerName;
this.totalAmount = totalAmount;
}
public void processOrder() {
// 处理订单的逻辑
// ...
sendConfirmationEmail();
}
private void sendConfirmationEmail() {
// 发送电子邮件的逻辑
// ...
}
}
在上述代码中,Order
类不仅负责订单的处理逻辑,还包含了发送电子邮件的逻辑。这违反了单一责任原则,导致一个类承担了多个职责。
这种设计的问题在于,如果以后需要更改或替换发送电子邮件的方式,需要修改 Order
类中的代码,这可能导致不必要的变更风险,并增加了代码的复杂性。
通过对比这两个示例,我们可以清晰地看到应用单一责任原则的代码更加清晰、可维护和可扩展,每个类或模块只关注一个单一的职责,避免了不必要的耦合。
好处解析:
当使用单一责任原则的代码需要进行维护时,其好处主要体现在以下几个方面:
修改订单处理逻辑:假设我们需要修改订单处理逻辑,例如添加一些额外的验证或处理步骤。在使用单一责任原则的代码中,我们只需关注
Order
类中与订单处理逻辑相关的代码,而不必担心影响到与电子邮件发送相关的代码。这样的职责分离使得修改订单处理逻辑变得更加简单和直观。替换邮件发送方式:如果我们需要更改或替换邮件发送方式,例如从使用SMTP协议改为使用API调用发送邮件,或者使用不同的邮件服务提供商。在使用单一责任原则的代码中,我们只需关注
EmailSender
类中的邮件发送逻辑,而不必修改Order
类。这样的职责分离使得替换邮件发送方式变得更加容易和安全。测试的简化:由于单一责任原则使得代码更加模块化和职责清晰,因此测试变得更加简单。在使用单一责任原则的代码中,我们可以轻松地针对不同的职责编写独立的单元测试,而不必处理与其他职责相关的复杂逻辑。这样的测试可分离性使得测试更加可靠和可维护。
降低风险:当需要对代码进行修改时,使用单一责任原则的代码降低了引入错误的风险。因为每个类或模块只关注一个职责,修改其中一个部分不会影响到其他部分,减少了不必要的依赖和耦合。这使得维护过程更加可控和安全。
总之,使用单一责任原则的代码在维护时具有更高的可维护性、可测试性和可扩展性。代码的职责分离使得维护过程更加简化和安全,降低了风险,并促进了代码的模块化和解耦。这使得开发者能够更加轻松地修改、扩展和测试代码,以满足系统的变化需求。
3、总结
一个类只负责完成一个职责或者功能。但是也要结合具体的业务。
- 也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。
- 换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
二、开闭原则
1、简介
开闭原则(pen Closed Principle,简写为 OCP)。它要求应该对扩展开放、对修改关闭
。
- 说人话就是,当我们需要添加一个新的功能时,应该在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
2、例子
案例:
如果没有遵循开闭原则,代码可能会如下所示:
class Order {
private double totalAmount;
public Order(double totalAmount) {
this.totalAmount = totalAmount;
}
// 计算折扣后的金额
public double getDiscountedAmount(String discountType) {
double discountedAmount = totalAmount;
if (discountType.equals("FESTIVAL")) {
discountedAmount = totalAmount * 0.9; // 节日折扣,9折
} else if (discountType.equals("SEASONAL")) {
discountedAmount = totalAmount * 0.8; // 季节折扣,8折
}
return discountedAmount;
}
}
在这个例子中,订单类 Order
中的 getDiscountedAmount
方法根据不同的折扣类型应用相应的折扣。当需要添加新的折扣类型时,我们不得不修改 getDiscountedAmount
方法的代码,增加新的判断逻辑。
这种设计违反了开闭原则,具有以下不利影响:
代码的脆弱性:每次添加新的折扣类型时,我们需要修改现有的代码。这增加了代码的脆弱性,因为任何错误的修改都可能导致现有功能的破坏。
可维护性下降:在没有遵循开闭原则的情况下,代码中的条件逻辑会不断增加,使得代码变得复杂和难以维护。当折扣类型增多时,代码会变得冗长且难以阅读,降低了可维护性。
扩展困难:由于没有使用抽象和多态来定义折扣策略,我们无法轻松地扩展和添加新的折扣类型。每次需要添加新的折扣类型时,都需要修改现有的代码,这增加了开发的复杂性和风险。
测试困难:代码中的条件逻辑使得测试变得困难,因为需要编写多个测试用例来覆盖不同的分支。同时,每次修改现有的条件逻辑时,还需要更新相关的测试代码,增加了测试的工作量。
综上所述,如果不遵循开闭原则,代码将变得脆弱、难以维护和扩展。通过引入抽象和多态的设计方式,可以改善代码的可扩展性、可维护性和测试性,使代码更具弹性和适应性。
遵循开闭原则的案例:
在上述代码中,我们可以使用开闭原则对其进行改进。我们可以引入一个抽象的折扣策略接口,每种折扣类型都实现该接口,然后在订单类中使用策略模式来计算折扣金额。
以下是使用开闭原则改进后的代码示例:
// 折扣策略接口
public interface DiscountStrategy {
double applyDiscount(double totalAmount);
}
// 节日折扣策略
public class FestivalDiscountStrategy implements DiscountStrategy {
@Override
public double applyDiscount(double totalAmount) {
return totalAmount * 0.9; // 节日折扣,9折
}
}
// 季节折扣策略
public class SeasonalDiscountStrategy implements DiscountStrategy {
@Override
public double applyDiscount(double totalAmount) {
return totalAmount * 0.8; // 季节折扣,8折
}
}
// 订单类
public class Order {
private double totalAmount;
private DiscountStrategy discountStrategy;
public Order(double totalAmount) {
this.totalAmount = totalAmount;
}
// 设置折扣策略
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
// 计算折扣后的金额
public double getDiscountedAmount() {
if (discountStrategy != null) {
return discountStrategy.applyDiscount(totalAmount);
}
return totalAmount;
}
}
在改进后的代码中,我们定义了一个折扣策略接口 DiscountStrategy
,并创建了两个具体的折扣策略类 FestivalDiscountStrategy
和 SeasonalDiscountStrategy
。订单类 Order
中引入了折扣策略,并使用策略模式在 getDiscountedAmount
方法中计算折扣金额。
通过这种方式,当需要添加新的折扣类型时,我们只需要创建一个新的实现了 DiscountStrategy
接口的具体折扣策略类,而不需要修改订单类的代码。我们可以通过设置不同的折扣策略来应用不同的折扣类型。
这样的设计遵循了开闭原则,使得代码更加可扩展和可维护。每个折扣类型都是独立的策略类,可以方便地扩展和修改,而不会对订单类产生影响。这提高了代码的灵活性,并符合开闭原则的设计理念。
3、总结
开闭原则是面向对象设计中的重要原则之一:
- 它要求软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 简单来说,就是在设计中要尽量避免修改已有的代码,而是通过扩展来实现新功能或适应变化的需求。
三、里氏替换原则
1、简介
里氏替换原则(Liskov Substitution Principle,简写为 LSP):它强调子类型必须能够替换其基类型而不影响程序的正确性。
- 说人话就是,当我们使用继承关系创建派生类(子类)时,这个派生类应该能够无缝替换其基类(父类)的实例,而程序的行为不会出现意外。
- 这意味着派生类在行为上应该保持与基类的一致性,并且不应该破坏基类的约定,包括输入输出的约束、异常的约束,以及前置条件和后置条件 。
- 如:尽量不要重写父类的方法。
2、例子
案例:
好的,让我们以动物类的例子来说明里氏替换原则的好处和坏处。
假设我们有一个基类 Animal
表示动物,它有一个方法 makeSound()
用于发出声音。然后我们派生出两个子类 Dog
和 Cat
分别表示狗和猫,它们都继承自 Animal
。
符合里氏替换原则的例子:
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat meows");
}
}
不符合里氏替换原则的例子:
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
// 错误:猫不应该重写 makeSound 方法
@Override
public void makeSound() {
throw new UnsupportedOperationException("Cat cannot make sound like a dog");
}
// 猫的叫声应该在另一个方法中定义,而不是覆盖基类的方法
public void meow() {
System.out.println("Cat meows");
}
}
》调用方法时:
public class Main {
public static void makeAnimalSound(Animal animal) {
animal.makeSound();
}
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
makeAnimalSound(animal1); // Output: "Dog barks"
makeAnimalSound(animal2); // Output: "Cat meows"
}
}
当调用 makeAnimalSound
方法时,如果遵循里氏替换原则,输出结果将符合预期,即分别输出 "Dog barks" 和 "Cat meows"。
符合里氏替换原则的情况下:
符合里氏替换原则的情况下:
在 makeAnimalSound
方法中,我们将 Animal
类型的参数传递进去,并调用其 makeSound()
方法。由于 Dog
和 Cat
类都是 Animal
类的子类,且它们都重写了 makeSound()
方法,所以在运行时,根据对象的实际类型,会调用相应子类的 makeSound()
方法。
这样做的好处是,我们可以使用统一的方法 makeAnimalSound
来处理不同类型的动物对象,而不需要针对每个具体的子类编写特定的代码。这增加了代码的灵活性和可扩展性,使得我们可以轻松地添加新的动物子类,而不需要修改现有的方法。
不符合里氏替换原则的情况下:
不符合里氏替换原则的情况下:
在不遵循里氏替换原则的情况下,当调用 makeAnimalSound
方法时,输出结果可能不符合预期。
在不符合里氏替换原则的示例中,Cat
类重写了 makeSound()
方法,并抛出了一个不支持的操作异常。如果我们将一个 Cat
对象传递给 makeAnimalSound
方法,它将抛出异常而不是输出 "Cat meows"。
这个问题的根本原因是 Cat
类违反了基类 Animal
的行为约定,即 makeSound()
方法应该输出相应动物的声音。这导致代码在处理 Cat
对象时出现了不一致和不可预测的行为。
因此,遵循里氏替换原则可以确保代码在处理父类对象时,不依赖于具体子类的实现细节,而是按照基类的行为约定进行操作。这增加了代码的可靠性、可维护性和可扩展性。相反,违反里氏替换原则可能导致代码的不稳定和不可靠,增加了代码的复杂性和维护成本。
3、总结
当遵循里氏替换原则时,子类对象可以完全替换父类对象,并且程序的行为保持一致。这种设计方式具有以下优势:
可复用性和可扩展性:代码中的父类可以作为通用的抽象接口或基类使用,而子类可以根据需要进行扩展和定制。这样,我们可以使用基类的对象来处理一系列子类对象,而不需要对每个子类编写特定的代码,提高了代码的复用性和可扩展性。
统一的行为约定:遵循里氏替换原则,子类必须遵循父类的行为约定,确保在任何使用父类对象的地方,都能正确地处理子类对象。这样可以增加代码的可靠性和稳定性,减少意外行为的发生。
降低代码的耦合性:通过将代码依赖于抽象的父类而不是具体的子类,实现了代码的松耦合。这使得系统更容易理解、修改和扩展,减少了代码之间的依赖关系,提高了代码的可维护性和灵活性。
符合多态性原则:里氏替换原则是多态性的基础之一。通过将子类对象视为父类对象,实现了多态的特性,可以在运行时根据对象的实际类型来调用相应的方法,增加了代码的灵活性和可扩展性。
总之,里氏替换原则是面向对象设计中的一个重要原则,它强调子类对象必须能够替换其父类对象并且保持系统行为的一致性。遵循该原则可以提高代码的可复用性、可扩展性和可维护性,降低代码的耦合性,并保持多态性的特性。
四、接口隔离原则
1、简介
接口隔离原则(Interface Segregation Principle,简称ISP),它强调客户端不应该强迫依赖于它们不需要的接口,其中的“客户端”,可以理解为接口的调用者或者使用者。
- 说人话就是,当我们设计接口时,应该将其拆分成更小、更具体的接口,以满足客户端的实际需求,而不是设计一个庞大而臃肿的接口。
- 这样做的好处是,可以提高代码的内聚性,使得类或模块只需实现与其业务相关的接口,而不需要依赖于不相关的接口。同时也降低了类之间的耦合性,提高了代码的灵活性、可维护性和可测试性。
2、例子
假设我们正在开发一个机器人程序,机器人具有多种功能,如行走、飞行和工作。我们可以为这些功能创建一个统一的接口:
public interface Robot {
void walk();
void fly();
void work();
}
然而,这个接口并不符合接口隔离原则,因为它将多个功能聚合在了一个接口中。
对于那些只需要实现部分功能的客户端来说,这个接口会导致不必要的依赖(如机器人分为:扫地机器人、飞行机器人等)。为了遵循接口隔离原则,我们应该将这个接口拆分成多个更小、更专注的接口:
public interface Walkable {
void walk();
}
public interface Flyable {
void fly();
}
public interface Workable {
void work();
}
现在,我们可以根据需要为不同类型的机器人实现不同的接口。例如,对于一个只能行走和工作的机器人,我们只需要实现 Walkable 和 Workable 接口:
public class WalkingWorkerRobot implements Walkable, Workable {
@Override
public void walk() {
// 实现行走功能
}
@Override
public void work() {
// 实现工作功能
}
}
通过遵循接口隔离原则,我们将功能拆分成更小的接口,避免了不必要的依赖关系。现在,客户端只需要依赖于它们真正需要的接口,使得代码更加清晰、可维护和可扩展。如果我们需要新的功能,只需要针对相应的接口进行扩展即可,而不会影响其他接口的实现。这样,我们可以根据需求和场景来选择实现相应的接口,而不需要强制实现不需要的方法。
在实际项目中,应用接口隔离原则可以带来以下好处:
- 提高代码的内聚性:每个接口只关注特定的功能,使得代码更加专注和可理解。
- 降低代码的耦合性:客户端只依赖于所需的接口,不受其他不相关接口的影响。
- 提升代码的灵活性:根据需求选择合适的接口实现,使得系统更加灵活和可扩展。
3、总结
接口隔离原则(Interface Segregation Principle,简写为 ISP)强调将大型、笼统的接口拆分成小而精确的接口,以符合客户端的实际需求,避免不必要的依赖和复杂性。
以下是接口隔离原则的总结:
- 接口应该精确地定义只与特定功能相关的方法,而不是将所有功能都聚合在一个接口中。
- 将大型接口拆分成多个小接口,每个接口关注特定的功能领域。
- 客户端应该只依赖于它们真正需要的接口,而不是依赖于不相关的方法。
- 避免将不需要的方法强加给实现类,防止出现空实现或抛出不支持的操作异常。
- 接口隔离原则提高了代码的内聚性,使得每个接口和实现类都专注于特定的任务。
- 通过减少接口之间的依赖,接口隔离原则降低了代码的耦合性,提高了系统的灵活性和可维护性。
- 合理应用接口隔离原则可以简化系统的设计和维护工作,提升团队的开发效率和协作效果。
总之,接口隔离原则通过细化接口的设计,使得代码更加模块化、可扩展和易于理解。它促进了单一职责原则的实现,提高了代码质量和可维护性。在实际项目中,我们应该根据需求和场景合理应用接口隔离原则,避免过度依赖和冗余代码,构建更灵活、可扩展的系统。
ISP 和 SRP 的区别:
单一职责原则(SRP)要求一个类或模块只负责一项职责或功能。它强调类的内聚性,即一个类应该只有一个引起它变化的原因。SRP的目标是将功能划分清晰,避免一个类承担过多的责任,从而提高代码的可维护性、可测试性和可理解性。
接口隔离原则(ISP)则侧重于接口的设计。它提倡将大型、笼统的接口拆分成小而精确的接口,以符合客户端的实际需求。ISP的目标是避免客户端依赖不需要的方法,减少不相关功能的耦合。通过接口的细化和分离,ISP提高了代码的内聚性,促进了代码模块化、可扩展性和可理解性。
简而言之,**SRP关注类或模块的职责和功能的划分,强调类的单一职责。而ISP关注接口的设计,强调接口的精确性和客户端的需求。**它们共同为代码的可维护性、可测试性和可理解性提供了指导原则,但侧重点和应用场景略有不同。在实际设计中,我们可以同时考虑和应用这两个原则来构建高质量的软件系统。
五、依赖倒置原则
1、简介
依赖倒置原则(Dependency Inversion Principle,简称DIP):它强调高层模块不应该依赖于低层模块的具体实现方式,而是应该依赖于抽象。
- 说人话就是,当我们设计代码时,应该通过抽象来定义模块之间的关系,而不是直接依赖于具体的实现细节。
- 这样做的好处是,提高了代码的灵活性和可维护性。高层模块不需要知道低层模块的具体实现,只需依赖于抽象接口。这样,当低层模块的实现发生变化时,高层模块不受影响。
- 另外,依赖倒置原则也鼓励通过依赖注入等方式来实现模块之间的解耦,提高了代码的可测试性和可扩展性。
2、例子
举例一:UserController 和 UserService / UserServiceImpl
假设我们正在开发一个用户管理系统,其中包括用户控制器(UserController)和用户服务(UserService)以及其具体实现(UserServiceImpl)。
按照依赖倒置原则的设计,UserController 不应该直接依赖于 UserServiceImpl,而是应该依赖于抽象的 UserService 接口。
public interface UserService {
void addUser(User user);
void deleteUser(int userId);
User getUser(int userId);
List<User> getAllUsers();
}
public class UserServiceImpl implements UserService {
// 具体的实现代码
// ...
}
public class UserController {
private UserService userService;
// 通过构造函数进行依赖注入
public UserController(UserService userService) {
this.userService = userService;
}
// 控制器方法调用用户服务方法
public void addUser(User user) {
userService.addUser(user);
}
public void deleteUser(int userId) {
userService.deleteUser(userId);
}
public User getUser(int userId) {
return userService.getUser(userId);
}
public List<User> getAllUsers() {
return userService.getAllUsers();
}
}
通过依赖倒置原则的设计,UserController 依赖于抽象的 UserService 接口,而不是具体的 UserServiceImpl 实现类。这样做的好处是,当我们需要更换或扩展用户服务的具体实现时,只需创建新的实现类并实现 UserService 接口即可,而不需要修改 UserController 的代码。这样实现了高层模块(UserController)不依赖于低层模块(UserServiceImpl)的具体实现,提高了代码的灵活性和可维护性。
Tomcat
Tomcat 是一个常用的 Java Web 服务器,它也遵循了依赖倒置原则。Tomcat 作为高层模块,不直接依赖于具体的 Servlet 实现类,而是通过 Servlet 接口与具体的 Servlet 容器进行交互。
这样的设计允许我们在不改变 Tomcat 的代码的情况下,可以使用不同的 Servlet 容器来运行我们的 Web 应用程序。例如,我们可以在 Tomcat 上部署一个基于 Apache Tomcat 的 Web 应用,也可以在 Jetty 上部署一个基于 Eclipse Jetty 的 Web 应用,因为 Tomcat 和 Jetty 都实现了 Servlet 接口。
Spring 的 IOC 容器
当我们使用 Spring 的 IOC 容器来实现依赖注入时,依赖倒置原则发挥了重要作用。让我们结合之前的例子来说明。
假设我们有一个接口 A
和实现类 AImpl
,我们想要将 AImpl
注入到容器中并使用接口 A
来引用它。首先,我们需要在 Spring 的配置文件中将 AImpl
的实例注入到容器中:
<bean id="a" class="com.example.AImpl" />
现在,我们可以在需要使用 A
接口的地方通过依赖注入来引用 AImpl
的实例。例如,在 MyClass
类中添加一个依赖注入的属性 a
:
public class MyClass {
private A a;
public void setA(A a) {
this.a = a;
}
public void doSomething() {
a.doSomething();
}
}
通过依赖注入,我们可以让 Spring 的 IOC 容器自动将 AImpl
的实例注入到 MyClass
类中。这样,我们就可以通过调用 a.doSomething()
来使用 AImpl
的功能。
现在,假设我们还需要另外一个实现类来实现接口 A
。我们创建一个名为 AnotherAImpl
的类来实现接口 A
:
public class AnotherAImpl implements A {
@Override
public void doSomething() {
System.out.println("Doing something in AnotherAImpl");
}
}
然后,我们在 Spring 的配置文件中将 AnotherAImpl
的实例注入到容器中:
<bean id="anotherA" class="com.example.AnotherAImpl" />
现在,你可以在需要使用 A
接口的任何地方通过依赖注入来引用 AnotherAImpl
的实例。在 MyClass
类中添加一个新的依赖注入的属性 anotherA
:
public class MyClass {
private A a;
private A anotherA;
public void setA(A a) {
this.a = a;
}
public void setAnotherA(A anotherA) {
this.anotherA = anotherA;
}
public void doSomething() {
a.doSomething();
anotherA.doSomething();
}
}
现在,你可以同时使用 AImpl
和 AnotherAImpl
的实例来执行不同的操作。
通过依赖倒置原则和 Spring 的 IOC 容器,你可以轻松地切换不同的实现类,只需修改配置文件即可。这种灵活性使你能够根据需求随时替换和扩展实现,而无需修改依赖它们的类的代码。
总结起来,依赖倒置原则与 Spring 的 IOC 容器相辅相成。通过将接口作为抽象的约定,将实现类的选择和创建交给容器管理,我们实现了高层模块和低
层模块之间的解耦和灵活性。无论是使用 AImpl
还是 AnotherAImpl
,MyClass
类只需依赖于接口 A
,从而提高了代码的可维护性和可扩展性。
3、总结
依赖倒置原则(DIP)是面向对象设计中的一条重要原则。它的核心思想是高层模块不应该依赖于低层模块,而应该依赖于抽象接口或抽象类。这样可以降低模块之间的耦合性,提高代码的灵活性和可维护性。通过依赖注入等方式实现依赖倒置原则可以使代码更易于扩展和修改,同时也能提升代码的可测试性和可复用性。
六、KISS原则
1、简介
KISS原则(Keep It Simple, Stupid),它强调保持代码简单易懂的重要性。在编写代码时,应避免过度设计和复杂化,而是以简洁的方式解决问题
。KISS原则鼓励我们使用简单直接的方法来实现功能,避免过多的复杂性和不必要的抽象。
- 说人话就是,写代码的时候要保持简单,不要过度设计和增加复杂性,不要花里胡哨。要选择简洁直接的方法来解决问题,避免不必要的复杂性和抽象。这样做的好处是,代码更易于理解、调试和维护,降低出错的概率,并且提高开发效率。
2、例子
当我们设计一个用户管理系统时,假设有以下需求:
- 用户可以注册账号。
- 用户可以登录账号。
- 用户可以查看自己的个人信息。
- 用户可以修改个人信息。
以下是一个违反KISS原则的示例代码:
public class UserManagementSystem {
private UserRepository userRepository;
private EmailService emailService;
public UserManagementSystem() {
userRepository = new UserRepository();
emailService = new EmailService();
}
public void registerUser(String username, String password) {
// 一些注册逻辑
userRepository.saveUser(username, password);
emailService.sendEmail(username, "Welcome to our system!");
}
public void loginUser(String username, String password) {
// 一些登录逻辑
// ...
}
public void displayUserInfo(String username) {
// 一些显示用户信息的逻辑
// ...
}
public void updateUserProfile(String username, String newEmail) {
// 一些更新用户信息的逻辑
userRepository.updateEmail(username, newEmail);
emailService.sendEmail(username, "Your profile has been updated.");
}
}
在上述代码中,UserManagementSystem
类承担了太多的责任,既包含了用户管理逻辑,又包含了与用户相关的邮件服务逻辑。这导致类的职责过重,代码复杂度高,并且增加了对UserRepository
和EmailService
的直接依赖。
下面是符合KISS原则的重构后的示例代码:
public class UserManagementSystem {
private UserRepository userRepository;
private EmailService emailService;
public UserManagementSystem(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public void registerUser(String username, String password) {
userRepository.saveUser(username, password);
emailService.sendWelcomeEmail(username);
}
// 其他方法的实现省略...
}
在重构后的代码中,我们将与用户相关的逻辑拆分成了两个独立的类:UserRepository
负责用户数据的持久化,EmailService
负责发送邮件。UserManagementSystem
类只关注用户管理的核心逻辑,并通过构造函数依赖注入的方式获取UserRepository
和EmailService
实例。
通过拆分职责,每个类的责任更加清晰,代码也更加简洁和可维护。同时,减少了类之间的直接依赖关系,提高了代码的灵活性和可测试性。
3、总结
KISS原则(Keep It Simple, Stupid)是一种设计原则,强调保持代码简单易懂的重要性。以下是对KISS原则的总结:
- KISS原则建议在编写代码时避免过度设计和复杂化,以简洁的方式解决问题。
- 简单的代码更易于理解、调试和维护,降低了引入错误和bug的风险。
- 通过避免不必要的复杂性,可以提高代码的可读性,使其更容易被团队成员理解和使用。
- 简单的代码更容易进行扩展和重构,使系统更具灵活性和可维护性。
- 遵循KISS原则可以减少代码的冗余和复杂度,提高开发效率,并降低项目的成本和风险。
总而言之,KISS原则是一种鼓励简洁和直观代码的设计原则,它强调避免过度复杂化(代码多不是复杂),以简单的方式解决问题。通过遵循KISS原则,我们可以提高代码的可读性、可维护性和可扩展性,从而为项目的成功和可持续发展奠定坚实基础。
七、DRY原则
1、简介
DRY原则(Don't Repeat Yourself),它强调避免重复代码的产生
。
2、例子
在 Java 编程中,我们可以通过以下方法遵循 DRY 原则:
(1)使用方法(functions):当你发现自己在多处重复相同的代码时,可以将其抽取为一个方法,并在需要的地方调用该方法。
public class DryExample {
public static void main(String[] args) {
printHello("张三");
printHello("李四");
}
public static void printHello(String name) {
System.out.println("你好," + name + "!");
}
}
在这个例子中,我们使用 printHello
方法避免了重复的 System.out.println
语句。
(2)使用继承和接口:当多个类具有相似的行为时,可以使用继承和接口来抽象共享的功能,从而减少重复代码。
public abstract class Animal {
public abstract void makeSound();
public void eat() {
System.out.println("动物在吃东西");
}
}
public class Dog extends Animal {
public void makeSound() {
System.out.println("汪汪");
}
}
public class Cat extends Animal {
public void makeSound() {
System.out.println("喵喵");
}
}
在这个例子中,我们使用抽象类 Animal
和继承来避免在 Dog
和 Cat
类中重复 eat
方法的代码。
(3)重用代码库和框架:使用成熟的代码库和框架可以避免从零开始编写一些通用功能。例如,使用 Java 标准库、Apache Commons 或 Google Guava 等库。
遵循 DRY 原则可以帮助我们编写更高质量的代码,并更容易进行维护和扩展。同时,要注意不要过度优化,以免影响代码的可读性和理解性。
3、总结
DRY原则(Don't Repeat Yourself)是一种软件设计原则,强调避免重复代码的重要性。它鼓励开发人员在编写代码时避免重复的逻辑、功能或信息。
- 首先,它提高了代码的可维护性和可读性。通过将重复的代码抽取到单独的方法、函数或模块中,我们可以避免在多个地方修改相同的代码,降低了出错的风险,并使代码更易于理解和修改。
- 其次,DRY原则促进了代码的重用和模块化。通过将通用的逻辑抽象为可复用的组件,我们可以在不同的地方调用它们,避免了重复编写相同的代码。这样可以减少代码量,提高开发效率,并增加系统的灵活性和可扩展性。
- 同时,DRY原则还有助于降低代码的耦合性。通过将重复的代码抽象为单一的实现,我们可以减少代码之间的依赖关系,使系统的各个部分更加独立和解耦。这样可以提高代码的可测试性,降低修改一个功能对其他部分造成的影响。
- 需要注意的是,DRY原则并不意味着完全消除重复代码。重复的代码只有在处理相同逻辑和功能时才被认为是违反DRY原则的。在评估重复代码时,我们需要考虑业务差异和上下文变化。如果代码的相似之处只是因为业务上的差异或上下文的变化,那么这种重复可能是合理的。
八、迪米特原则
1、简介
迪米特原则(Law of Demeter,简称LoD),也被称为最少知识原则(Principle of Least Knowledge),它强调对象之间的松耦合和信息隐藏。
- 说人话就是,当我们设计软件时,对象之间的交互应该尽量简单,避免直接访问其他对象的内部细节,而是通过调用公共方法来间接进行通信。
迪米特原则的核心思想是将对象设计为尽可能少地依赖其他对象,只与自己的直接朋友对象进行交互。这样做的好处是:
减少耦合:对象之间的直接依赖越少,耦合度越低。当一个对象只与少数几个朋友对象进行交互时,修改一个对象的内部结构或实现不会对其他对象产生太大的影响。
提高灵活性:由于对象之间的关系简单明确,系统更容易进行扩展和修改。当需要修改系统时,我们只需关注与当前对象直接相关的部分,而不需要了解其他对象的内部细节。
提升可维护性:迪米特原则使得系统中的对象独立性更强,易于单独测试和调试。当一个对象的实现发生变化时,不会对其他对象产生连锁影响,减少了代码的藕合度。
遵循迪米特原则可以帮助我们设计出更加松耦合、可维护和可扩展的软件系统。同时,要注意避免过度设计,遵循适度原则,不要违背其他设计原则或增加不必要的复杂性。
2、例子
假设我们有一个订单处理系统,其中包括订单(Order)、用户(User)和库存(Inventory)三个核心概念:
public class Order {
private User user;
private Inventory inventory;
public Order(User user, Inventory inventory) {
this.user = user;
this.inventory = inventory;
}
public void processOrder() {
String userName = user.getName();
int availableQuantity = inventory.getAvailableQuantity();
// 处理订单逻辑
// ...
}
}
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Inventory {
private int availableQuantity;
public Inventory(int availableQuantity) {
this.availableQuantity = availableQuantity;
}
public int getAvailableQuantity() {
return availableQuantity;
}
}
在这个例子中,Order 类直接依赖于 User 和 Inventory 类,通过调用它们的方法来获取用户信息和库存信息。这种直接依赖关系导致了较高的耦合性,当 User 或 Inventory 类发生变化时,需要修改 Order 类的代码。
现在,让我们看看遵循迪米特法则的情况下的代码:
public class Order {
private OrderService orderService;
public Order(OrderService orderService) {
this.orderService = orderService;
}
public void processOrder() {
String userName = orderService.getUserName();
int availableQuantity = orderService.getAvailableQuantity();
// 处理订单逻辑
// ...
}
}
public interface OrderService {
String getUserName();
int getAvailableQuantity();
}
public class UserService implements OrderService {
private User user;
public UserService(User user) {
this.user = user;
}
public String getUserName() {
return user.getName();
}
public int getAvailableQuantity() {
// 调用库存服务获取库存信息
// ...
return availableQuantity;
}
}
public class InventoryService implements OrderService {
private Inventory inventory;
public InventoryService(Inventory inventory) {
this.inventory = inventory;
}
public String getUserName() {
// 调用用户服务获取用户信息
// ...
return userName;
}
public int getAvailableQuantity() {
return inventory.getAvailableQuantity();
}
}
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Inventory {
private int availableQuantity;
public Inventory(int availableQuantity) {
this.availableQuantity = availableQuantity;
}
public int getAvailableQuantity() {
return availableQuantity;
}
}
在这个例子中,我们引入了一个中间层接口 OrderService,并有两个实现类 UserService 和 InventoryService。Order 类通过依赖 OrderService 接口来获取用户信息和库存信息,而不直接依赖于具体的 User 和 Inventory 类。这种间接的依赖关系降低了耦合性,当 User 或 Inventory 类发生变化时,只需修改对应的实现类,而不需要修改 Order 类的代码。
通过遵循迪米特法则,我们实现了类之间的解耦,提高了代码的灵活性和可维护性。中间层的引入使得系统的模块职责更加清晰,提升了代码的可读性和可测试性。
3、总结
迪米特法则在设计和编写代码时,强调对象之间的松耦合,通过减少对象之间的直接依赖关系来提高代码的质量。它带来的优势包括降低耦合性、提高代码的灵活性、可维护性、模块化和封装性,同时也有助于代码的可读性和可测试性:
迪米特法则的核心思想是减少对象之间的直接依赖关系。一个对象应该尽可能少地了解其他对象的细节和内部结构。
迪米特法则鼓励使用中间层或接口来实现对象之间的通信,而不是直接依赖具体的对象。这样做可以降低耦合性,提高代码的灵活性和可维护性。
迪米特法则能够促进代码的模块化和封装性。对象只需关注与其密切相关的对象,不需要了解其他对象的具体实现细节。这样可以提高代码的可读性和可理解性。
遵循迪米特法则可以降低代码的脆弱性。当一个对象发生变化时,只有直接依赖它的对象需要进行相应的修改,而其他无关的对象不会受到影响。
迪米特法则有助于提高代码的可测试性。由于对象之间的依赖关系更加简化和清晰,测试对象的行为变得更加容易,可以更好地进行单元测试和模块测试。
GoF(Gang of Four)是指四位著名的计算机科学家:Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides。他们合作编写了一本具有里程碑意义的著作《Design Patterns: Elements of Reusable Object-Oriented Software》(设计模式:可复用的面向对象软件元素)。这本书对软件开发领域产生了深远影响。
GoF提出了23种设计模式,分为三大类:
创建型模式(Creational Patterns)关注对象的创建过程,包括:
- 单例模式(Singleton)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 建造者模式(Builder)
- 原型模式(Prototype)
结构型模式(Structural Patterns)关注类和对象之间的组合,包括:
- 适配器模式(Adapter)
- 桥接模式(Bridge)
- 组合模式(Composite)
- 装饰模式(Decorator)
- 外观模式(Facade)
- 享元模式(Flyweight)
- 代理模式(Proxy)
行为型模式(Behavioral Patterns)关注对象之间的通信,包括:
- 职责链模式(Chain of Responsibility)
- 命令模式(Command)
- 解释器模式(Interpreter)
- 迭代器模式(Iterator)
- 中介者模式(Mediator)
- 备忘录模式(Memento)
- 观察者模式(Observer)
- 状态模式(State)
- 策略模式(Strategy)
- 模板方法模式(Template Method)
- 访问者模式(Visitor)
这些设计模式提供了在软件开发中常见问题的解决方案,并促进了可