Location>code7788 >text

More on how to change code gracefully

Popularity:83 ℃/2024-10-07 22:58:03

Continued from the previous page./xo0M0) and then expand on it.

The above says: "Programming based on abstract interfaces is indeed a best practice: exposing easily changeable points of functionality by defining abstract interfaces, and isolating and extending different implementations, which embodies the principle of openness and closure"
public class Foo {
    private Bar bar ;
    @Inject
    public Foo(Bar bar) {
        this.bar = bar;
    }
    public String doSomething(int key) {
//Bar#getResult experiences the complexity of the code, and isolates and extends function points by injecting different Bar implementation objects.
        return (key);
    }
}

However, in real projects, which are often developed in collaboration with many people, some historical reasons lead to the implementation of certain code snippets tend to be "strange", which can't be well covered unilaterally, and at the same time is also filled with "bad code flavor" that violates the principle of opening and closing;

You, as the "chosen one", need to iterate on its functionality;

Maybe after your assessment, go for a radical architectural evolution, which is kudos;

But sometimes it's also a matter of global ROI to assess whether the benefits of a radical reorganization are large enough, and sometimes we have to compromise (trade-off). I.e., how to do a relatively good refactoring within a tight delivery cycle and not let the code continue to decay;

So this time, we continue with the two arts of modifying code:Methods addedcap (a poem)method override

Strategy 1: Methodological additions

Isolate old logic by adding new methods, i.e., cut "gaps" in old methods and inject new business logic to be called;

Take the previous case, for example, an old historical method.Need to filter out empty objects from the returned data set

public class Foo {
  private Bar bar;
  public Foo() {
    bar = new Bar();
  }
  public List<Data> doSomething(int key) {
    //Reliance on third-party services, RPC call result sets
    List<Data> result = (key);
    // Filter out empty objects
    return ().filter(Objects::nonNull).collect(());
  }
}

The logic here is simple, using theJava Lambda The expression does the filtering, but the way it's written certainly adds insult to injury: indeed the original method was already very Low and couldn't be unilateral. This time, I just added a simple piece of logic at the end. It's already familiar, and probably quite a lot of people will mess with it this way;

But as a good programmer, the immediate status quo indeed we can only compromise, but the follow-up of each line of code, the need to do quality and quantity, and strive to do without affecting the original business logic to do testable;

"Methods added": by adding new methodsgetDataIfNotNull to isolate the old logic:

public List<Data> doSomething(int key) {
  //Reliance on third-party services, RPC call result sets
  List<Data> result = (key);
  return getDataIfNotNull(result);
}

as belowgetDataIfNotNull As an added method, it is easy to test it independently, while the original methoddoSomething And it didn't continue to corrupt.

public List<Data> getDataIfNotNull(List<Data> result) {
  return ().filter(Objects::nonNull).collect(());
}

You can see that the advantages are obvious: clear isolation of old and new code; and of course, for more segregation of duties, the use of theNew class segregationIt'll be better;

Strategy 2: Method Overrides

Rename the method to be modified, create a new method with the same name and signature as the original method, and call the renamed original method in the new method;

Assuming a new demand: fordoSomething method does a message notification operation, then "method override" that is:

Combining the original methodologydoSomething renamedoSomethingAndFilterDataand then create a new method with the same name as the original method.doSomething, and finally call the renamed original method in the new method:

// Rename the original method doSomething to doSomethingAndFilterData.
public List<Data> doSomethingAndFilterData(int key) {
//Reliance on third-party services, RPC call result sets
  List<Data> result = (key);
  return getDataIfNotNull(result);
}
// Create a new method doSomething with the same name as the original method.
public List<Data> doSomething(int key) {
  // Calling old methods
  List<Data> data = this.doSomethingAndFilterData(key);
  // Call new method
  doNotifyMsg(data);
  return data;
}
// New extension methods conform to isolated extensions, do not affect old methods, and also support one-sided coverage
public void doNotifyMsg(List<Data> data){
  //
}
Another way to write method overrides: usually a new method is defined, and then the old and new business logic is invoked sequentially in the new method;

Typically used to cut stream old and new logic as the architecture evolves; e.g., based on client version, greater than3. The client-side tangential flow uses the new logic - we create a new method calling both the old and new methods.

// Old historical code, no remodeling
public List<Data> doSomething(int key) {
  //Reliance on third-party services, RPC call result sets
  List<Data> result = (key);
  List<Data> data = getDataIfNotNull(result);
  return data;
}
// create a new method that aggregates calls to old and new logic
public List<Data> doSomethingWithNotifyMsg(int key) {
  List<Data> data = this.doSomething(key);
  // Call new method
  doNotifyMsg(data);
  return data;
}
// New extension methods conform to isolated extensions, do not affect old methods, and also support one-sided coverage
public void doNotifyMsg(List<Data> data){
  //
}

The benefits of this are obvious: no changes are made to the old approach, and the flow is cut at a higher "upper" level: new functionality is guaranteed to evolve iteratively, while the old functionality remains unchanged.

boolean enableFunc=getClientVersion()>DEFAULT_CLIENT_VERSION;
if (enableFunc){
  return doSomethingWithNotifyMsg();
}else{
  return doSomething();
}

You can see that "method override" regardless of how it is implemented, it does not add logic to the current old method, but by using the new method as an entry point, so as to avoid coupling the old and new logic together;

"Method overriding can be taken a step further by using a separate class to isolate it, known as the decorator pattern. Often the original class is so complex that you don't want to iterate on it anymore, so consider decoupling it with a decorator:

class DecoratedFoo extends Foo{

private Foo foo;
  public DecoratedFoo(Foo foo){
}
 
@Override
public List<Data> doSomething(int key) {
  List<Data> data = super.doSomething(key);
  notifyMsg();
  return data;
}
private void notifyMsg(){}
}