面向对象 solid
The SOLID Principles are five principles of Object-Oriented class design. They are a set of rules and best practices to follow while designing a class structure.
SOLID原则是面向对象的类设计的五项原则。 它们是设计类结构时要遵循的一组规则和最佳实践。
These five principles help us understand the need for certain design patterns and software architecture in general. So I believe that it is a topic that every developer should learn.
这五项原则有助于我们了解总体上对某些设计模式和软件体系结构的需求。 因此,我相信这是每个开发人员都应该学习的主题。
This article will teach you everything you need to know to apply SOLID principles to your projects.
本文将教您将SOLID原理应用于您的项目所需的一切。
We will start by taking a look into the history of this term. Then we are going to get into the nitty-gritty details – the why's and how's of each principle – by creating a class design and improving it step by step.
我们将从研究这个词的历史开始。 然后,我们将通过创建类设计并逐步进行改进,深入了解每个原理的本质和实质。
So grab a cup of coffee or tea and let's jump right in!
因此,抢一杯咖啡或茶,让我们跳进去吧!
The SOLID principles were first introduced by the famous Computer Scientist Robert J. Martin (a.k.a Uncle Bob) in his paper in 2000. But the SOLID acronym was introduced later by Michael Feathers.
SOLID原理是由著名的计算机科学家Robert J. Martin(又名Bob叔叔)在2000年首次提出的。但是SOLID的首字母缩写后来由Michael Feathers引入。
Uncle Bob is also the author of bestselling books Clean Code and Clean Architecture, and is one of the participants of the "Agile Alliance".
鲍勃叔叔还是畅销书《 清洁代码和清洁建筑》的作者 ,并且是“敏捷联盟”的参与者之一。
Therefore, it is not a surprise that all these concepts of clean coding, object-oriented architecture, and design patterns are somehow connected and complementary to each other.
因此,干净编码,面向对象的体系结构和设计模式的所有这些概念以某种方式相互连接和互补并不奇怪。
They all serve the same purpose:
它们都具有相同的目的:
"To create understandable, readable, and testable code that many developers can collaboratively work on."
“要创建许多开发人员可以共同工作的易于理解,可读和可测试的代码。”
Let's look at each principle one by one. Following the SOLID acronym, they are:
让我们一一看一下每个原理。 在SOLID首字母缩写之后,它们是:
The Single Responsibility Principle
英格尔责任原则在S
The Open-Closed Principle
O封闭式原理
The Liskov Substitution Principle
将L iskov替代原则
The Interface Segregation Principle
在我隔离覆盖整个院落原理
The Dependency Inversion Principle
该d ependency倒置原则
The Single Responsibility Principle states that a class should do one thing and therefore it should have only a single reason to change.
单一责任原则规定, 一个班级应该做一件事,因此它应该只有一个改变的理由 。
To state this principle more technically: Only one potential change (database logic, logging logic, and so on.) in the software’s specification should be able to affect the specification of the class.
要从技术上更详细地陈述这一原理:软件规范中只有一个潜在的更改(数据库逻辑,记录逻辑等)应该能够影响该类的规范。
This means that if a class is a data container, like a Book class or a Student class, and it has some fields regarding that entity, it should change only when we change the data model.
这意味着,如果一个类是一个数据容器,例如Book类或Student类,并且具有有关该实体的某些字段,则仅当我们更改数据模型时才应更改。
Following the Single Responsibility Principle is important. First of all, because many different teams can work on the same project and edit the same class for different reasons, this could lead to incompatible modules.
遵循单一责任原则很重要。 首先,由于许多不同的团队可以出于不同的原因从事同一个项目并编辑同一个类,因此可能导致模块不兼容。
Second, it makes version control easier. For example, say we have a persistence class that handles database operations, and we see a change in that file in the GitHub commits. By following the SRP, we will know that it is related to storage or database-related stuff.
其次,它使版本控制更加容易。 例如,假设我们有一个处理数据库操作的持久性类,并且在GitHub提交中看到该文件中的更改。 通过遵循SRP,我们将知道它与存储或与数据库相关的内容有关。
Merge conflicts are another example. They appear when different teams change the same file. But if the SRP is followed, fewer conflicts will appear – files will have a single reason to change, and conflicts that do exist will be easier to resolve.
合并冲突是另一个示例。 它们出现在不同的团队更改相同文件时。 但是,如果遵循SRP,则只会出现较少的冲突-文件将具有单一更改的原因,并且确实存在的冲突将更易于解决。
In this section we will look at some common mistakes that violate the Single Responsibility Principle. Then we will talk about some ways to fix them.
在本节中,我们将研究一些违反“单一责任原则”的常见错误。 然后,我们将讨论一些修复它们的方法。
We will look at the code for a simple bookstore invoice program as an example. Let's start by defining a book class to use in our invoice.
我们将以一个简单的书店发票程序的代码为例。 让我们从定义要在发票中使用的书籍类开始。
class Book {
String name;
String authorName;
int year;
int price;
String isbn;
public Book(String name, String authorName, int year, int price, String isbn) {
this.name = name;
this.authorName = authorName;
this.year = year;
this.price = price;
this.isbn = isbn;
}
}
This is a simple book class with some fields. Nothing fancy. I am not making fields private so that we don't need to deal with getters and setters and can focus on the logic instead.
这是一个简单的书类,其中包含一些字段。 没有什么花哨。 我不是将字段设置为私有,这样我们就不必处理getter和setter,而可以专注于逻辑。
Now let's create the invoice class which will contain the logic for creating the invoice and calculating the total price. For now, assume that our bookstore only sells books and nothing else.
现在,让我们创建发票类,其中将包含创建发票和计算总价的逻辑。 现在,假设我们的书店只卖书,没有其他东西。
public class Invoice {
private Book book;
private int quantity;
private double discountRate;
private double taxRate;
private double total;
public Invoice(Book book, int quantity, double discountRate, double taxRate) {
this.book = book;
this.quantity = quantity;
this.discountRate = discountRate;
this.taxRate = taxRate;
this.total = this.calculateTotal();
}
public double calculateTotal() {
double price = ((book.price - book.price * discountRate) * this.quantity);
double priceWithTaxes = price * (1 + taxRate);
return priceWithTaxes;
}
public void printInvoice() {
System.out.println(quantity + "x " + book.name + " " + book.price + "$");
System.out.println("Discount Rate: " + discountRate);
System.out.println("Tax Rate: " + taxRate);
System.out.println("Total: " + total);
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Here is our invoice class. It also contains some fields about invoicing and 3 methods:
这是我们的发票类。 它还包含一些有关发票的字段和3种方法:
calculateTotal method, which calculates the total price,
computeTotal方法,用于计算总价,
printInvoice method, that should print the invoice to console, and
printInvoice方法,应将发票打印到控制台,并且
saveToFile method, responsible for writing the invoice to a file.
saveToFile方法,负责将发票写入文件。
You should give yourself a second to think about what is wrong with this class design before reading the next paragraph.
在阅读下一段之前,您应该花点时间考虑一下此类设计的问题。
Ok so what's going on here? Our class violates the Single Responsibility Principle in multiple ways.
好的,这是怎么回事? 我们的课堂以多种方式违反了单一责任原则。
The first violation is the printInvoice method, which contains our printing logic. The SRP states that our class should only have a single reason to change, and that reason should be a change in the invoice calculation for our class.
第一个违规是printInvoice方法,其中包含我们的打印逻辑。 SRP指出,我们班级仅应有一个更改的原因,并且该原因应该是我们班级的发票计算中的更改。
But in this architecture, if we wanted to change the printing format, we would need to change the class. This is why we should not have printing logic mixed with business logic in the same class.
但是在这种体系结构中,如果我们想更改打印格式,则需要更改类。 这就是为什么我们不应该在同一类中将打印逻辑与业务逻辑混合在一起。
There is another method that violates the SRP in our class: the saveToFile method. It is also an extremely common mistake to mix persistence logic with business logic.
在我们的类中还有另一种违反SRP的方法: saveToFile方法。 将持久性逻辑与业务逻辑混合也是一个非常常见的错误。
Don't just think in terms of writing to a file – it could be saving to a database, making an API call, or other stuff related to persistence.
不要仅仅考虑写入文件,它可以保存到数据库,进行API调用或其他与持久性有关的内容。
So how can we fix this print function, you may ask.
您可能会问,那么我们如何解决此打印功能。
We can create new classes for our printing and persistence logic so we will no longer need to modify the invoice class for those purposes.
我们可以为打印和持久性逻辑创建新的类,因此我们将不再需要为这些目的修改发票类。
We create 2 classes, InvoicePrinter and InvoicePersistence, and move the methods.
我们创建两个类InvoicePrinter和InvoicePersistence,并移动方法。
public class InvoicePrinter {
private Invoice invoice;
public InvoicePrinter(Invoice invoice) {
this.invoice = invoice;
}
public void print() {
System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
System.out.println("Discount Rate: " + invoice.discountRate);
System.out.println("Tax Rate: " + invoice.taxRate);
System.out.println("Total: " + invoice.total + " $");
}
}
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Now our class structure obeys the Single Responsibility Principle and every class is responsible for one aspect of our application. Great!
现在,我们的班级结构遵循“单一责任原则”,每个班级都负责应用程序的一个方面。 大!
The Open-Closed Principle requires that classes should be open for extension and closed to modification.
开放-封闭原则要求类应为扩展而开放,并禁止修改。
Modification means changing the code of an existing class, and extension means adding new functionality.
修改意味着更改现有类的代码,而扩展意味着添加新功能。
So what this principle wants to say is: We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs. So we should avoid touching the tested and reliable (mostly) production code if possible.
因此,该原则要说的是:我们应该能够添加新功能,而无需触及该类的现有代码。 这是因为每当我们修改现有代码时,我们都冒着创建潜在错误的风险。 因此,如果可能,我们应该避免接触经过测试且可靠的(主要是)生产代码。
But how are we going to add new functionality without touching the class, you may ask. It is usually done with the help of interfaces and abstract classes.
您可能会问,但是我们如何在不接触课程的情况下添加新功能。 通常,它是在接口和抽象类的帮助下完成的。
Now that we have covered the basics of the principle, let's apply it to our Invoice application.
既然我们已经介绍了该原理的基础知识,那么就将其应用于“发票”应用程序中。
Let's say our boss came to us and said that they want invoices to be saved to a database so that we can search them easily. We think okay, this is easy peasy boss, just give me a second!
假设我们的老板来找我们,说他们希望将发票保存到数据库中,以便我们轻松搜索。 我们认为好吧,这很轻松,老板,请给我第二个!
We create the database, connect to it, and we add a save method to our InvoicePersistence class:
我们创建数据库,连接到数据库,然后向InvoicePersistence类添加一个save方法:
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
public void saveToDatabase() {
// Saves the invoice to database
}
}
Unfortunately we, as the lazy developer for the book store, did not design the classes to be easily extendable in the future. So in order to add this feature, we have modified the InvoicePersistence class.
不幸的是,作为书店的懒惰开发人员,我们没有将这些类设计为将来很容易扩展。 因此,为了添加此功能,我们修改了InvoicePersistence类。
If our class design obeyed the Open-Closed principle we would not need to change this class.
如果我们的班级设计遵循开闭原则,则无需更改此班级。
So, as the lazy but clever developer for the book store, we see the design problem and decide to refactor the code to obey the principle.
因此,作为书店的懒惰但聪明的开发人员,我们看到了设计问题,并决定重构代码以遵守该原则。
interface InvoicePersistence {
public void save(Invoice invoice);
}
We change the type of InvoicePersistence to Interface and add a save method. Each persistence class will implement this save method.
我们将InvoicePersistence的类型更改为Interface并添加一个save方法。 每个持久性类都将实现此save方法。
public class DatabasePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to DB
}
}
public class FilePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to file
}
}
So our class structure now looks like this:
所以我们的类结构现在看起来像这样:
Now our persistence logic is easily extendable. If our boss asks us to add another database and have 2 different types of databases like MySQL and MongoDB, we can easily do that.
现在,我们的持久性逻辑很容易扩展。 如果老板要求我们添加另一个数据库并拥有2种不同类型的数据库(例如MySQL和MongoDB),我们可以轻松地做到这一点。
You may think that we could just create multiple classes without an interface and add a save method to all of them.
您可能认为我们可以只创建多个没有接口的类,并向所有它们添加一个save方法。
But let's say that we extend our app and have multiple persistence classes like InvoicePersistence, BookPersistence and we create a PersistenceManager class that manages all persistence classes:
但是,假设我们扩展了我们的应用程序,并拥有多个持久性类(如InvoicePersistence , BookPersistence) ,并且创建了一个PersistenceManager类来管理所有持久性类:
public class PersistenceManager {
InvoicePersistence invoicePersistence;
BookPersistence bookPersistence;
public PersistenceManager(InvoicePersistence invoicePersistence,
BookPersistence bookPersistence) {
this.invoicePersistence = invoicePersistence;
this.bookPersistence = bookPersistence;
}
}
We can now pass any class that implements the InvoicePersistence interface to this class with the help of polymorphism. This is the flexibility that interfaces provide.
现在,我们可以借助多态性将实现InvoicePersistence接口的任何类传递给此类。 这是接口提供的灵活性。
The Liskov Substitution Principle states that subclasses should be substitutable for their base classes.
Liskov替代原则指出,子类应该可以替代其基类。
This means that, given that class B is a subclass of class A, we should be able to pass an object of class B to any method that expects an object of class A and the method should not give any weird output in that case.
这意味着,鉴于类B是类A的子类,我们应该能够将类B的对象传递给任何需要类A的对象的方法,并且在这种情况下该方法不应给出任何奇怪的输出。
This is the expected behavior, because when we use inheritance we assume that the child class inherits everything that the superclass has. The child class extends the behavior but never narrows it down.
这是预期的行为,因为当我们使用继承时,我们假定子类继承了超类所拥有的所有内容。 子类扩展行为,但从不缩小行为范围。
Therefore, when a class does not obey this principle, it leads to some nasty bugs that are hard to detect.
因此,当一个类不遵守该原则时,它会导致一些难以发现的讨厌的错误。
Liskov's principle is easy to understand but hard to detect in code. So let's look at an example.
Liskov的原理很容易理解,但是很难在代码中检测到。 因此,让我们看一个例子。
class Rectangle {
protected int width, height;
public Rectangle() {
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
We have a simple Rectangle class, and a getArea function which returns the area of the rectangle.
我们有一个简单的Rectangle类,还有一个getArea函数,该函数返回矩形的面积。
Now we decide to create another class for Squares. As you might know, a square is just a special type of rectangle where the width is equal to the height.
现在,我们决定为Squares创建另一个类。 您可能知道,正方形只是宽度等于高度的特殊矩形。
class Square extends Rectangle {
public Square() {}
public Square(int size) {
width = height = size;
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
Our Square class extends the Rectangle class. We set height and width to the same value in the constructor, but we do not want any client (someone who uses our class in their code) to change height or weight in a way that can violate the square property.
我们的Square类扩展了Rectangle类。 我们在构造函数中将height和width设置为相同的值,但是我们不希望任何客户端(在其代码中使用我们的类的人)以可能违反square属性的方式更改高度或重量。
Therefore we override the setters to set both properties whenever one of them is changed. But by doing that we have just violated the Liskov substitution principle.
因此,无论何时更改其中一个,我们都将覆盖设置器以设置这两个属性。 但是这样做违反了Liskov替代原则。
Let's create a main class to perform tests on the getArea function.
让我们创建一个主类来对getArea函数执行测试。
class Test {
static void getAreaTest(Rectangle r) {
int width = r.getWidth();
r.setHeight(10);
System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
}
public static void main(String[] args) {
Rectangle rc = new Rectangle(2, 3);
getAreaTest(rc);
Rectangle sq = new Square();
sq.setWidth(5);
getAreaTest(sq);
}
}
Your team's tester just came up with the testing function getAreaTest and tells you that your getArea function fails to pass the test for square objects.
您团队的测试人员刚想出了测试函数getAreaTest,并告诉您getArea函数无法通过方形对象的测试。
In the first test, we create a rectangle where the width is 2 and the height is 3 and call getAreaTest. The output is 20 as expected, but things go wrong when we pass in the square. This is because the call to setHeight function in the test is setting the width as well and results in an unexpected output.
在第一个测试中,我们创建一个宽度为2高度为3的矩形,并调用getAreaTest。 输出为预期的20,但是当我们经过正方形时,情况会出错。 这是因为在测试中对setHeight函数的调用也正在设置宽度,并导致意外的输出。
Segregation means keeping things separated, and the Interface Segregation Principle is about separating the interfaces.
隔离意味着将事物分开,并且接口隔离原理是关于分离接口。
The principle states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.
该原则指出,许多特定于客户端的接口比一个通用接口要好。 不应强迫客户实施不需要的功能。
This is a simple principle to understand and apply, so let's see an example.
这是一个易于理解和应用的简单原理,因此让我们来看一个示例。
public interface ParkingLot {
void parkCar(); // Decrease empty spot count by 1
void unparkCar(); // Increase empty spots by 1
void getCapacity(); // Returns car capacity
double calculateFee(Car car); // Returns the price based on number of hours
void doPayment(Car car);
}
class Car {
}
We modeled a very simplified parking lot. It is the type of parking lot where you pay an hourly fee. Now consider that we want to implement a parking lot that is free.
我们建模了一个非常简化的停车场。 这是您需要支付小时费用的停车场类型。 现在考虑我们要实现一个免费的停车场。
public class FreeParking implements ParkingLot {
@Override
public void parkCar() {
}
@Override
public void unparkCar() {
}
@Override
public void getCapacity() {
}
@Override
public double calculateFee(Car car) {
return 0;
}
@Override
public void doPayment(Car car) {
throw new Exception("Parking lot is free");
}
}
Our parking lot interface was composed of 2 things: Parking related logic (park car, unpark car, get capacity) and payment related logic.
我们的停车场界面由两部分组成:与停车场相关的逻辑(停车场,非停车场,获取停车位)和与付款相关的逻辑。
But it is too specific. Because of that, our FreeParking class was forced to implement payment-related methods that are irrelevant. Let's separate or segregate the interfaces.
但这太具体了。 因此,我们的FreeParking类被迫实施与付款无关的方法。 让我们分离或隔离接口。
We've now separated the parking lot. With this new model, we can even go further and split the PaidParkingLot to support different types of payment.
现在我们分离了停车场。 有了这种新模型,我们甚至可以进一步扩展PaidParkingLot,以支持不同类型的付款。
Now our model is much more flexible, extendable, and the clients do not need to implement any irrelevant logic because we provide only parking-related functionality in the parking lot interface.
现在,我们的模型更加灵活,可扩展,并且客户不需要实现任何无关的逻辑,因为我们在停车场界面中仅提供与停车场相关的功能。
The Dependency Inversion principle states that our classes should depend upon interfaces or abstract classes instead of concrete classes and functions.
依赖倒置原则指出,我们的类应依赖于接口或抽象类,而不是具体的类和函数。
In his article (2000), Uncle Bob summarizes this principle as follows:
鲍勃叔叔在他的文章 (2000年)中总结了以下原理:
"If the OCP states the goal of OO architecture, the DIP states the primary mechanism".
“如果OCP声明了OO体系结构的目标,则DIP声明了主要机制”。
These two principles are indeed related and we have applied this pattern before while we were discussing the Open-Closed Principle.
这两个原则确实相关,在讨论开放式封闭原则之前,我们已经应用了这种模式。
We want our classes to be open to extension, so we have reorganized our dependencies to depend on interfaces instead of concrete classes. Our PersistenceManager class depends on InvoicePersistence instead of the classes that implement that interface.
我们希望我们的类对扩展开放,因此我们重新组织了依赖项,使其依赖于接口而不是具体的类。 我们的PersistenceManager类依赖于InvoicePersistence,而不是实现该接口的类。
In this article, we started with the history of SOLID principles, and then we tried to acquire a clear understanding of the why's and how's of each principle. We even refactored a simple Invoice application to obey SOLID principles.
在本文中,我们从SOLID原理的历史入手,然后我们试图对每种原理的原因和方式有一个清晰的了解。 我们甚至重构了一个简单的Invoice应用程序,以遵守SOLID原则。
I want to thank you for taking the time to read the whole article and I hope that the above concepts are clear.
我要感谢您抽出宝贵的时间阅读整篇文章,并希望以上概念清晰易懂。
I suggest keeping these principles in mind while designing, writing, and refactoring your code so that your code will be much more clean, extendable, and testable.
我建议在设计,编写和重构代码时牢记这些原则,以使您的代码更加整洁,可扩展和可测试。
If you are interested in reading more articles like this, you can subscribe to my blog's mailing list to get notified when I publish a new article.
如果您有兴趣阅读更多类似的文章,可以订阅我的博客的邮件列表,以在我发表新文章时得到通知。
翻译自: https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/
面向对象 solid