Open-closed principle: Tips to improve scalability

What is the opening and closing principle?

The full English name of the Open Closed Principle is Open Closed Principle, abbreviated as OCP. Its English description is: software entities (modules, classes, functions, etc.) should be open for extension, but closed for modification. We translate it into Chinese: software entities (modules, classes, methods, etc.) should be "open to extensions and closed to modifications."

The real development scenario is: adding a new function should extend the code based on the existing code (add modules, classes, methods, etc.) rather than modify the existing code (modify modules, classes, methods, etc.).

Why should we follow the open-closed principle?

I don’t know if you have ever encountered modifying the code of scenario A, but affecting the function of scenario B. The reason is that the code of scenario A and the code of scenario B are coupled together.
The open-closed principle is a powerful tool for low coupling and high cohesion of code. Complying with the open-closed principle can make the code more maintainable, scalable and reusable. This is because the open-closed principle emphasizes that code should be open to extensions and closed to modifications. This means that when modifying existing code, you should not modify the existing code, but implement new functions by extending the functions of the existing code. needs. This can reduce the number of code modifications, thereby reducing the risk of introducing new bugs and avoiding the impact on other code logic. At the same time, adhering to the Open-Closed Principle makes code easier to extend and reuse because it builds on existing code rather than modifying it. In short, adhering to the open-closed principle can make code more flexible, reliable, and easier to maintain.

How to achieve "open for extension, closed for modification"?

We already know the definition of the open-closed principle, so how do we comply with the open-closed principle? Below I will share some small experiences with you from both the ideological level and the actual coding level.

Expanded consciousness, abstract consciousness, encapsulated consciousness

expanded consciousness

When writing code, we need to take more time to think forward and think more about what requirements may change in this code in the future, how to design the code structure, and leave expansion points in advance so that no changes are needed when requirements change in the future. With the overall structure of the code and minimal code changes, new code can be flexibly inserted into extension points to achieve "open for expansion and closed for modification".
For example, if we want to develop a WeChat payment function now, we need to think about whether other payment methods such as Alipay payment and bank card payment will appear in the future. If other payment methods appear, how can we design our code now to facilitate the future? extension.

abstract consciousness, encapsulation consciousness

What does abstraction awareness have to do with code extensibility?
If we want to answer this question, we must first know the relationship between abstraction and concrete implementation. Personal understanding of abstraction is to look at one thing or object from a higher-level perspective. For example: Type 92 pistol, Type 95 assault rifle , Type 88 sniper rifle, these weapons can be abstracted at a higher level into "weapons that can fire and fire bullets." Let's temporarily call "weapons that can fire and fire bullets" a "gun", and the "gun" is the Type 92 pistol. The Type 95 assault rifle, Type 88 sniper rifle and other weapons are abstractions, while the Type 92 pistol, Type 95 assault rifle, and Type 88 sniper rifle are the concrete realization of "guns".
Let's take the payment function as an example again. Payment is an abstraction, and whether it is WeChat payment or Alipay payment is the specific implementation.
Usually the abstraction is simple and stable, while the concrete implementation is complex and changeable. For complex and changeable specific implementations, we can encapsulate them to isolate the spread of complexity and uncertainty.
A fixed abstraction that supports different implementations is extensible.

Tips for adding parameters to improve scalability

Encapsulate parameters into an object

// 重构前
func A(a, b, c string) {
    
    
    // ...
}

// 重构后
type Args struct{
    
    
    a string
    b string
    c string
}

func A(args Args) {
    
    }

The code after refactoring is more scalable than before refactoring. When you need to add parameters, you only need to expand the fields of the Args structure. The signature of the function does not need to be changed, and the caller of the function basically does not need to be changed.

mutable variable

func doSomething(arg ...interface{
    
    }) {
    
    
    switch len(arg) {
    
    
    case 0:
       // ...
    case 1:
       // ...
    case 2:
       // ...
    case 3:
       // ...
    case 4:
       // ...
    default:
       // ...
    }
}

Using variable parameters, code flexibility and scalability can be improved

Function options mode (recommended)

type Connection struct{}

type stuffClient struct {
conn Connection
timeout int
retries int
}

type StuffClientOption func(*stuffClient)

func NewStuffClient(conn Connection, opts …StuffClientOption) stuffClient {
client := stuffClient{}
for _, o := range opts {
o(&client)
}
client.conn = conn
return client
}

func WithRetries(r int) StuffClientOption {
return func(o *stuffClient) {
o.retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *stuffClient) {
o.timeout = t
}
}

func main() {
connection := Connection{}
client := NewStuffClient(connection, WithRetries(3), WithTimeout(3))
}

Option pattern: When designing the code, extensibility is taken into consideration, and different attributes are set by passing different options.

Tips for improving scalability by exporting parameters

Similar to the method of improving the scalability of input parameters, the return value can also be encapsulated in an object to improve the scalability of output parameters.

// 重构前
func A() (a, b, c string) {
    
    
    return
}
a, b, c := A()

// 重构后
type Res struct{
    
    
    a string
    b string
    c string
}

func A() (res Res) {
    
    
    return
}
res := A()

When adding fields or reducing fields in the return value, there is no need to modify the function declaration. Similarly, there are very few modifications at the function call, or even no modifications are needed.

Tips for code logic scalability

Register plugin

type handler func(args ...interface{
    
    })
type EventBus struct {
    
    
    handlers map[string][]handler
}

// 注册事件,提供事件名和回调函数
func (e *EventBus) RegisterEvent(name string, eventHandle handler) (err error) {
    
    
    // ...
    if _, ok := e.handlers[name]; ok {
    
    
       e.handlers[name] = append(e.handlers[name], eventHandle)
    } else {
    
    
       e.handlers[name] = []handler{
    
    eventHandle}
    }
    return nil
}

// 触发事件
func (e *EventBus) TriggerEvent(name, requestId string, param ...interface{
    
    }) (err error) {
    
    
    // ....
    handlers, ok := e.handlers[name]
    if !ok {
    
    
       log.Error(requestId, "RegisterEvent_err", err, name)
       return
    }
    for _, handler := range handlers {
    
    
       handler()
    }

    return nil
}

// 支付成功事件
var e = &EventBus{
    
    }

func EventInit() {
    
    
    e.RegisterEvent("pay_ok", func(args ...interface{
    
    }) {
    
    
       // 支付成功通知保单
    })
    e.RegisterEvent("pay_ok", func(args ...interface{
    
    }) {
    
    
       // 支付成功发送支付成功事件
    })
    e.RegisterEvent("pay_ok", func(args ...interface{
    
    }) {
    
    
       // 支付成功给用户发送短信
    })
}

func NewEventBus() *EventBus {
    
    
    return e
}

func main() {
    
    
    eb := NewEventBus()
    EventInit()
    eb.TriggerEvent("pay_ok", "requestId", 1, 2, 3)
}

If you want to add other business logic when the payment is successful, such as cash back, adding points, etc., we only need to register a handler.

strategy pattern

Take payment as an example. The following example is the payment process of Alipay payment and WeChat payment, using a typical process-oriented writing method.

// 反例
func Pay(arg) {
    
    
    // ...
    if arg.Type == "alipay" {
    
    
       // 创建订单
       CreateAlipayOrder()
       // 调用RPC进行支付
       AlipayPayRpc()
       // 更新订单
       UpdateAlipayOrder()
    } else if arg.Type == "weixin" {
    
    
       // 创建订单
       CreateWXOrder()
       // 调用RPC进行支付
       WXPayRpc()
       // 更新订单
       UpdateWXOrder()
    }
}

Questions above:

Violating the opening and closing principle: If you add a new payment platform, you will need to write a new if-else branch, which has very low scalability.

Violating the single principle: processing Alipay payment and WeChat payment.

Process-oriented: omitted. . .

Let's use policy mode, factory mode + interface-oriented to reconstruct the above code.

// 定义接口
type Payer interface {
    
    
    CreateOrder()
    PayRpc()
    UpdateOrder()
}

// 支付宝支付实现
type Alipay struct {
    
    }
func (a *Alipay)CreateOrder(){
    
    
    // ...
}
func (a *Alipay)PayRpc(){
    
    
    // ...
}
func (a *Alipay)UpdateOrder(){
    
    
    // ...
}

// 微信支付实现
type Wxpay struct {
    
    }
func (w *Wxpay)CreateOrder(){
    
    
// ...
}
func (w *Wxpay)PayRpc(){
    
    
// ...
}
func (w *Wxpay)UpdateOrder(){
    
    /
/ ...
}

// 工厂+策略模式
func NewPayer(PayType string) Payer {
    
    
    switch PayType {
    
    
    case "alipay":return &Alipay{
    
    }
    case "weixin":return &Wxpay{
    
    }
    // case "other":
            // retrun &OtherPay{}
    }
}
    
func Pay(arg) {
    
    
    payer := NewPayer(arg.type)
    
    payer.CreateOrder()

    payer.PayRpc()

    payer.UpdateOrder()
}

From process-oriented programming to interface-oriented programming, the payment logic is simpler and the scalability is also improved. If you need to add a new payment platform, the code of the Pay function basically does not need to be changed. You only need to add an implementation of payment for other platforms and add it in Add a case to the factory.

dependency injection

Taking sending notifications as an example, the Notification class below directly relies on the MessageSender class through new MessageSender(); in the code, so Notification can only send MessageSender class messages.

// 非依赖注入实现方式
public class Notification {
    
    
    private MessageSender messageSender;
    public Notification() {
    
    
        this.messageSender = new MessageSender(); // 此处有点像 hardcode
    }
    public void sendMessage(String cellphone, String message) {
    
     //... 省略校验逻辑等...
        this.messageSender.send(cellphone, message);
    } 
}

public class MessageSender {
    
    
    public void send(String cellphone, String message) {
    
    
        //....
    } 
}
// 使用 Notification
Notification notification = new Notification();
notification.sendMessage("10086", "hello world")

If dependency injection is used, the Notification class can send any message passed in through dependency injection.

// 依赖注入
public class Notification {
    
    
    private MessageSender messageSender;
    public Notification(MessageSender messageSender) {
    
    
        this.messageSender = messageSender;
    } 
    public void sendMessage(String cellphone, String message) {
    
    
        this.messageSender.send(cellphone, message);
    } 
}

public interface MessageSender {
    
    
    void send(String cellphone, String message);
}

// 短信发送类
public class SmsSender implements MessageSender {
    
    
    @Override
    public void send(String cellphone, String message) {
    
    
        //....
    } 
}

// 站内信发送类
public class InboxSender implements MessageSender {
    
    
    @Override
    public void send(String cellphone, String message) {
    
    
        //....
    } 
}

// 短信
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);
notification.sendMessage("10086", "hello world")
// 站内信
MessageSender messageSender = new InboxSender();
Notification notification = new Notification(messageSender);
notification.sendMessage("10086", "hello world")

Summarize

The opening and closing principle is a very important principle in object-oriented design. It can help us write more maintainable, extensible and reusable code. To implement the open-closed principle, we can use methods such as abstraction, polymorphism, and design patterns.

Guess you like

Origin blog.csdn.net/m0_73728511/article/details/133106674