No amount of effort is too small to multiply by 365

preface

In the last article, we talked about one of the most basic design principles: the single responsibility principle. In this lecture, let’s look at the next design principle: the open and closed principle.

As programmers, we’ve become so accustomed to changing code once a request comes in that it’s almost a knee-jerk reaction. It’s easy to change, as long as we do what we’ve done before.

It’s a no-brainer, but it comes with long-term damage. Each person only changed a little bit each time, but after long-term accumulation, when a new demand comes, the change momentum will be huge. And in the process, everyone is innocent, because everyone is just following the convention. But as a result, everyone gets hurt and the code gets harder and harder to maintain.

Since “modification” brings so many problems, can we not modify it? The open and closed principle provides such a new direction.

Introduction to the

The principle of openness and closure is expressed as follows:

Software entities (classes, modules, functions) should be open to extension and closed to modification.

Coined by Bertrand Meyer in his book Object-oriented Software Construction, this phrase places a high order on Software design: don’t change the code.

You may be wondering, how can I implement new requirements without changing the code? The answer is by scaling. In more general terms, new requirements should be implemented in new code.

The open closed principle describes to us the result that we can do new functionality without modifying the code and just extend it. However, this result requires leaving extension points inside the software, which is where we need to design. Because each extension point is a model that needs to be designed.

explain

For example, if we are developing a hotel reservation system, we need to calculate different rates for different users. For example, if regular users get full price, a gold card gets 20% off, and a silver card gets 10% off, the code might look something like this:

class HotelService {
  public double getRoomPrice(final User user, final Room room) {
    double price = room.getPrice();
    if (user.getLevel() == Level.GOLD) {
      return price * 0.8;
    }
    
    if (user.getLevel() == Level.SILVER) {
      return price * 0.9;
    }
    
    returnprice; }}Copy the code

At this point, there is a new demand to increase the platinum card membership, giving a 25% discount. It should be written like this:

class HotelService {
  public double getRoomPrice(final User user, final Room room) {
    double price = room.getPrice();
    if (user.getLevel() == UserLevel.GOLD) {
      return price * 0.8;
    }
    
    if (user.getLevel() == UserLevel.SILVER) {
      return price * 0.9;
    }
    
    if (user.getLevel() == UserLevel.PLATINUM) {
      return price * 0.75;
    }
    
    returnprice; }}Copy the code

Obviously, this is the practice of changing the code, each time a new type is added. However, a hotel system with users of all levels will certainly differ not only in room rates but also in service offerings. As you can imagine, with each additional user level, we had to change a lot of code.

So what should we do? We should think about how to design it into a model that can be extended. In this case, we need a user-level model since the user level is being added each time and the differences between services are reflected in the user level. In the previous code, the user level was just a simple enumeration, which we can enrich:

interface UserLevel {
  double getRoomPrice(Room room);
}

class GoldUserLevel implements UserLevel {
  public double getRoomPrice(final Room room) {
    return room.getPrice() * 0.8; }}class SilverUserLevel implements UserLevel {
  public double getRoomPrice(final Room room) {
    return room.getPrice() * 0.9; }}Copy the code

Our original code would look like this:

class HotelService {
  public double getRoomPrice(final User user, final Room room) {
    returnuser.getRoomPrice(room); }}class User {
  privateUserLevel level; .public double getRoomPrice(final Room room) {
    returnlevel.getRoomPrice(room); }}Copy the code

So to add platinum users, we just need to write a new class:

class PlatinumUserLevel implements UserLevel {
  public double getRoomPrice(final Room room) {
    return room.getPrice() * 0.75; }}Copy the code

We can do this because we leave an extension point in the code: UserLevel. In this case, we have upgraded the original UserLevel that supports only enumerated values to a UserLevel with actions.

With this revamp, the getRoomPrice method of HotelService has been stabilized so that we don’t need to constantly adjust it based on user level. At this point, we have a stable building block that can be used as a stable module in later work.

Of course, in this case, the method is relatively simple. In a real project, the business approach is more complex.

conclusion

Today, we talked about the open closed principle that software entities should be open to extension and closed to modification. Simply put, do not change the code, new functions should be implemented with new code.

In fact, we all know the truth, but for many people, it is difficult to do, especially in the code to leave extension points, often requires some design ability. A lot of good software is designed to provide us with enough extensibility, and we can learn more by learning from the interfaces of these software.

Obviously, to provide extension points, you need to program toward the interface. But is it good design to have interfaces? In the next lecture, we’ll look at what other principles you need to follow to design an interface.