要构建稳定且灵活的软件,我们需要牢记软件设计原则。 拥有无错误的代码至关重要。 但是,精心设计的软件体系结构同样重要。
SOLID是最著名的软件设计原则集之一。 它可以帮助您避免常见的陷阱,并从更高层次考虑应用程序的体系结构。
What are SOLID design principles?
SOLID design principles are five software design principles that enable you to write effective object-oriented code. Knowing about OOP principles like abstraction, encapsulation, inheritance, and polymorphism is important, but how would you use them in your everyday work? SOLID design principles have become so popular in recent years because they answer this question in a straightforward way.
SOLID名称是助记符的缩写,其中每个字母代表软件设计原则,如下所示:
- 单一责任原则O代表开/关原则L代表Liskov替代原理一,接口隔离原理依赖反转原理D
这五项原则在这里和那里重叠,程序员广泛使用它们。 SOLID原则带来了更灵活,更稳定的软件架构,该架构更易于维护和扩展,而且不易损坏。
Single Responsibility Principle
The Single Responsibility Principle is the first SOLID design principle, represented by the letter “S” and defined by Robert C Martin. It states that in a well-designed application, each class (microservice, code module) should have only one single responsibility. Responsibility is used in the sense of having only one reason to change.
When a class handles more than one responsibility, any changes made to the functionalities might affect others. This is bad enough if you have a smaller app but can become a nightmare when you work with complex, enterprise-level software. By making sure that each module encapsulates only one responsibility, you can save a lot of testing time and create a more maintainable architecture.
Example of the Single Responsibility Principle
让我们来看一个例子。 我将使用Java,但您也可以将SOLID设计原则应用于任何其他OOP语言。
假设我们正在为书店编写Java应用程序。 我们创建一个书该类使用户能够获取并设置每本书的标题和作者,并在清单中搜索该书。
class Book {
String title;
String author;
String getTitle() {
return title;
}
void setTitle(String title) {
this.title = title;
}
String getAuthor() {
return author;
}
void setAuthor(String author) {
this.author = author;
}
void searchBook() {...}
}
但是,以上代码违反了“单一责任原则”,因为书课堂有两个责任。 首先,它设置与书籍相关的数据(标题和作者)。 其次,它在库存中搜索书籍。 setter方法更改书对象,当我们要在清单中搜索同一本书时可能会导致问题。
要应用“单一责任原则”,我们需要将两个责任分离。 在重构代码中,书该类将仅负责获取和设置书宾语。
class Book {
String title;
String author;
String getTitle() {
return title;
}
void setTitle(String title) {
this.title = title;
}
String getAuthor() {
return author;
}
void setAuthor(String author) {
this.author = author;
}
}
然后,我们创建另一个类库存视图负责检查库存。 我们移动search书()方法在这里,并参考书构造函数中的类。
class InventoryView {
Book book;
InventoryView(Book book) {
this.book = book;
}
void searchBook() {...}
}
在下面的UML图表上,您可以看到按照单一职责原则重构代码后体系结构的变化。 我们将初始书具有两个职责的班级分为两个班级,每个班级都有自己的单一职责。
Open/Closed Principle
The Open/Closed Principle is the “O” of SOLID’s five software design principles. It was Bertrand Meyer who coined the term in his book “Object-Oriented Software Construction”. The Open/Closed Principle states that classes, modules, microservices, and other code units should be open for extension but closed for modification.
因此,您应该能够使用OOP功能(例如通过子类和接口进行继承)扩展现有代码。 但是,切勿修改类,接口和其他已存在的代码单元(特别是如果在生产环境中使用它们),因为这可能导致意外行为。 如果通过扩展代码而不是对其进行修改来添加新功能,则将故障的风险降到最低。 此外,您也不必对现有功能进行单元测试。
Example of the Open/Closed Principle
让我们继续阅读我们的书店示例。 现在,商店想在圣诞节前以折扣价分发菜谱。 我们已经遵循了单一责任原则,因此我们创建了两个单独的类:食谱折扣保留折扣的详细信息并DiscountManager将折扣应用于价格。
class CookbookDiscount {
String getCookbookDiscount() {
String discount = "30% between Dec 1 and 24.";
return discount;
}
}
class DiscountManager {
void processCookbookDiscount(CookbookDiscount discount) {...}
}
直到商店管理层通知我们他们的菜谱折扣销售非常成功以至于他们希望扩展它之前,此代码才能正常工作。 现在,他们希望在该主题生日当天以50%的折扣分发每本传记。 要添加新功能,我们创建一个新传记折扣类:
class BiographyDiscount {
String getBiographyDiscount() {
String discount = "50% on the subject's birthday.";
return discount;
}
}
要处理新的折扣类型,我们需要将新功能添加到DiscountManager课:
class DiscountManager {
void processCookbookDiscount(CookbookDiscount discount) {...}
void processBiographyDiscount(BiographyDiscount discount) {...}
}
但是,当我们更改现有功能时,我们违反了开放/封闭原则。 尽管以上代码可以正常工作,但可能会向应用程序添加新的漏洞。 我们不知道新添加的内容将如何与依赖于DiscountManager类。 在实际的应用程序中,这意味着我们需要再次测试和部署整个应用程序。
但是,我们也可以选择通过添加代表所有类型折扣的额外抽象层来重构代码。 因此,我们创建一个名为图书折扣那食谱折扣和传记折扣类将实现。
public interface BookDiscount {
String getBookDiscount();
}
class CookbookDiscount implements BookDiscount {
@Override
public String getBookDiscount() {
String discount = "30% between Dec 1 and 24.";
return discount;
}
}
class BiographyDiscount implements BookDiscount {
@Override
public String getBookDiscount() {
String discount = "50% on the subject's birthday.";
return discount;
}
}
现在,DiscountManager可以参考图书折扣接口,而不是具体的类。 当。。。的时候process图书折扣()方法被调用,我们可以同时传递食谱折扣和传记折扣作为论证,两者都是图书折扣接口。
class DiscountManager {
void processBookDiscount(BookDiscount discount) {...}
}
重构后的代码遵循“打开/关闭”原则,因为我们可以添加新代码食谱折扣类,而无需修改现有代码库。 这也意味着将来,我们可以使用其他折扣类型(例如,使用犯罪书籍折扣)。
下面的UML图显示了重构前后我们的示例代码的样子。 在左侧,您可以看到DiscountManager取决于食谱折扣和传记折扣类。 在右边,这三个类别都取决于图书折扣抽象层(DiscountManager引用它,而食谱折扣和传记折扣实施)。
Liskov Substitution Principle
The Liskov Substitution Principle is the third principle of SOLID, represented by the letter “L”. It was Barbara Liskov who introduced the principle in 1987 in her conference keynote talk “Data Abstraction”. The original phrasing of the Liskov Substitution Principle is a bit complicated, as it asserts that:
“在计算机程序中,如果S是T的子类型,则可以用类型S的对象替换类型T的对象(即,类型S的对象可以替换类型T的对象),而不会改变该类型的任何理想属性 程序(正确性,执行的任务等)。”
用外行的话说,它应该用其子类的对象替换超类的对象,而不会引起应用程序问题。 因此,子类永远不要更改其父类的特征(例如,参数列表和返回类型)。 您可以通过注意正确的继承层次结构来实现Liskov替换原理。
Example of the Liskov Substitution Principle
现在,书店要求我们向应用程序添加新的传递功能。 因此,我们创建了一个送书告知客户有关可以收集订单的地点数量的类:
class BookDelivery {
String titles;
int userID;
void getDeliveryLocations() {...}
}
但是,这家商店还出售他们只想送到高街商店的精美精装书。 因此,我们创建了一个新的精装书扩展的子类送书并覆盖getDeliveryLocations()具有自身功能的方法:
class HardcoverDelivery extends BookDelivery {
@Override
void getDeliveryLocations() {...}
}
后来,商店要求我们也为有声读物创建交付功能。 现在,我们扩展现有的送书一个班有声书交付子类。 但是,当我们要覆盖getDeliveryLocations()方法,我们意识到有声读物无法传递到实际位置。
class AudiobookDelivery extends BookDelivery {
@Override
void getDeliveryLocations() {/* can't be implemented */}
}
我们可以更改getDeliveryLocations()但是,这种方法会违反《里斯科夫替代原则》。 修改后,我们无法替换送书的超类有声书交付子类,而不会破坏应用程序。
要解决此问题,我们需要修复继承层次结构。 让我们介绍一个额外的层,该层可以更好地区分图书交付类型。 新的离线交付和在线交付类将送书超类。 我们也将getDeliveryLocations()方法离线交付和create a new getSoftwareOptions()方法在线交付类(因为它更适合在线交付)。
class BookDelivery {
String title;
int userID;
}
class OfflineDelivery extends BookDelivery {
void getDeliveryLocations() {...}
}
class OnlineDelivery extends BookDelivery {
void getSoftwareOptions() {...}
}
在重构代码中,精装书将是离线交付它将覆盖getDeliveryLocations()具有自己功能的方法。
有声书交付将是在线交付这是个好消息,因为现在不必处理getDeliveryLocations()方法。 相反,它可以覆盖getSoftwareOptions()父方法及其自己的实现(例如,通过列出和嵌入可用的音频播放器)。
class HardcoverDelivery extends OfflineDelivery {
@Override
void getDeliveryLocations() {...}
}
class AudiobookDelivery extends OnlineDelivery {
@Override
void getSoftwareOptions() {...}
}
重构之后,我们可以在不破坏应用程序的情况下使用任何子类代替其超类。
在下面的UML图上,您可以看到通过应用Liskov替换原理,我们在继承层次结构中添加了额外的一层。 尽管新架构更加复杂,但它为我们提供了更灵活的设计。
Interface Segregation Principle
The Interface Segregation Principle is the fourth SOLID design principle represented by the letter “I” in the acronym. It was Robert C Martin who first defined the principle by stating that “clients should not be forced to depend on methods they don’t use.” By clients, he means classes that implement interfaces. In other words, interfaces shouldn’t include too many functionalities.
违反接口隔离原则会损害代码的可读性,并迫使程序员编写不执行任何操作的伪方法。 在设计良好的应用程序中,应避免界面污染(也称为胖界面)。 解决方案是创建更小的接口,您可以更灵活地实现这些接口。
Example of the Interface Segregation Principle
让我们将一些用户操作添加到我们的在线书店中,以便客户可以在购买前与内容进行交互。 为此,我们创建一个名为BookAction三种方法:seeReviews(),searchSecondHand(),and listenSample()。
public interface BookAction {
void seeReviews();
void searchSecondhand();
void listenSample();
}
然后,我们创建两个类:精装UI和有声书UI实施BookAction与自己的功能接口:
class HardcoverUI implements BookAction {
@Override
public void seeReviews() {...}
@Override
public void searchSecondhand() {...}
@Override
public void listenSample() {...}
}
class AudiobookUI implements BookAction {
@Override
public void seeReviews() {...}
@Override
public void searchSecondhand() {...}
@Override
public void listenSample() {...}
}
这两个类都依赖于它们不使用的方法,因此我们打破了接口隔离原则。 精装书无法收听,因此精装UI上课不需要listenSample()方法。 同样,有声读物没有二手副本,因此有声书UI上课也不需要
但是,由于BookAction接口包含这些方法,其所有依赖类都必须实现它们。 换一种说法,BookAction是我们需要隔离的受污染的接口。 让我们用两个更具体的子接口对其进行扩展:精装动作和AudioAction。
public interface BookAction {
void seeReviews();
}
public interface HardcoverAction extends BookAction {
void searchSecondhand();
}
public interface AudioAction extends BookAction {
void listenSample();
}
现在精装UI类可以实现精装动作界面和有声书UI类可以实现AudioAction接口。
这样,两个类都可以实现seeReviews()的方法BookAction超级接口。 然而,精装UI不必实施无关紧要的listenSample()方法和音频UI不必实施searchSecondhand(),或者。
class HardcoverUI implements HardcoverAction {
@Override
public void seeReviews() {...}
@Override
public void searchSecondhand() {...}
}
class AudiobookUI implements AudioAction {
@Override
public void seeReviews() {...}
@Override
public void listenSample() {...}
}
重构后的代码遵循接口隔离原则,因为两个类都不依赖于它们不使用的方法。 下面的UML图很好地显示了隔离的接口导致了更简单的类,这些类仅实现了它们真正需要的方法:
Dependency Inversion Principle
The Dependency Inversion Principle is the fifth SOLID design principle represented by the last “D” and introduced by Robert C Martin. The goal of the Dependency Inversion Principle is to avoid tightly coupled code, as it easily breaks the application. The principle states that:
“高级模块不应依赖于低级模块。 两者都应依赖抽象。”“抽象不应依赖细节。 细节应该取决于抽象。”
换句话说,您需要解耦高级类和低级类。 高级类通常封装复杂的逻辑,而低级类通常包含数据或实用程序。 通常,大多数人都希望使高级类依赖于低级类。 但是,根据依赖关系反转原理,您需要反转依赖关系。 否则,当替换低级别的类时,高级别的类也会受到影响。
作为解决方案,您需要为低层类创建一个抽象层,以便高层类可以依赖于抽象而不是具体的实现。
罗伯特·C·马丁(Robert C Martin)也提到依赖倒置原则是开放/封闭和里斯科夫替代原则的特定组合。
Example of the Dependency Inversion Principle
现在,书店要求我们建立一项新功能,使客户能够将自己喜欢的书放在架子上。
为了实现新功能,我们创建了一个较低级别的书班级和更高级别架类。 的书该课程将允许用户查看评论并阅读他们在书架上存储的每本书的样本。 的架类将使他们在书架上添加一本书并自定义书架。
class Book {
void seeReviews() {...}
void readSample() {...}
}
class Shelf {
Book book;
void addBook(Book book) {...}
void customizeShelf() {...}
}
一切看起来都很好,但是作为高级架等级取决于低水平书,以上代码违反了Dependency Inversion Principle。 当商店要求我们也允许客户将DVD添加到他们的货架上时,这一点就变得很清楚。 为了满足需求,我们创建了一个新的DVD类:
class DVD {
void seeReviews() {...}
void watchSample() {...}
}
现在,我们应该修改架类,以便它也可以接受DVD。 但是,这显然会破坏开放/封闭原则。 解决方案是为较低层的类创建一个抽象层(书和DVD)。 我们将通过介绍产品两个类都将实现的接口。
public interface Product {
void seeReviews();
void getSample();
}
class Book implements Product {
@Override
public void seeReviews() {...}
@Override
public void getSample() {...}
}
class DVD implements Product {
@Override
public void seeReviews() {...}
@Override
public void getSample() {...}
}
现在,架可以参考产品接口而不是其实现(书和DVD)。 重构后的代码还允许我们稍后引入新的产品类型(例如,杂志),客户也可以将其放在货架上。
class Shelf {
Product product;
void addProduct(Product product) {...}
void customizeShelf() {...}
}
上面的代码还遵循Liskov替换原理,因为产品type可以用其两个子类型(书和DVD),而不会破坏程序。 同时,我们还实现了依赖倒置原则,因为在重构代码中,高级类也不依赖于低级类。
正如您在下面的UML图的左侧看到的那样,架等级取决于低水平书重构之前。 在不应用依赖倒置原则的情况下,我们应该使其依赖于底层DVD上课 但是,在重构之后,高级和低级类都依赖于抽象产品介面(架提到它,而书和DVD实施)。
How should you implement SOLID design principles?
Implementing the SOLID design principles increases the overall complexity of a code base, but it leads to more flexible design. Besides monolithic apps, you can also apply SOLID design principles to microservices where you can treat each microservice as a standalone code module (like a class in the above examples).
When you break a SOLID design principle, Java and other compiled languages might throw an Exception, but it doesn’t always happen. Software architecture problems are hard to detect, but advanced diagnostic software such as APM tools can provide you with many useful hints.