polymorphism

“Polymorphism is a feature supported by programming languages that makes it possible for static code to behave dynamically when run, so that you can program without worrying about types and write unified processing logic rather than relying on specific types.”

Polymorphism happens at Starbucks, and polymorphism is convenient for customers. Whether it’s cash, Alipay or wechat, customers don’t need to care about the details behind each payment, just submit the payment tool. The attendant is the key to generating polymorphic behavior by dynamically exchanging the same delivery behavior with different payment methods at the checkout machine.

Deep inside the checkout machine, suppose it implements payment polymorphism in the following way:

class PayManager {
    fun pay(type: String){
        if (type == 现金){
            payByCash()
        } else if(type == alipay) {payByAli()}else if(type == wechat) {payByWechat()}}Copy the code

If else, it’s not really polymorphic, and there’s a lot of trouble with that.

If-else Piezo: No extensibility

When new payment methods are added, the checkout machines have to be rebuilt.

Because the if-else behavior is added at compile time, that is, when the code is compiled, a PDF snapshot of payManager.pay () is generated. It cannot be dynamically changed at run time and the new behavior will have to be recompiled, so the new behavior is static and non-extensible.

The corresponding new behavior at compile time is the new behavior at run time, that is, payManager.pay () behaves differently (polymorphic) when running different upper-layer code.

The policy pattern is suitable for the current scenario. The policy pattern isolates the specific behavior from the user of the behavior. When the behavior changes, the user of the behavior does not need to change with it.

// Use the interface to define the abstract payment behavior
interface Pay {
    fun pay(a)
}

// PayManager holds abstract payment behavior
class PayManager {
    private var pay:Pay
    
    fun setPay(pay: Pay) {
        this.pay = pay
    }
    
    fun pay(a){
        pay.pay()
    }
}
Copy the code

Encapsulated by the policy pattern, upper classes using PayManager can dynamically add payment methods to PayManager at run time by injecting different instances of the Pay interface.

Don’t the upper classes of PayManager have to be compiled? How many payment methods can be added to PayManager is determined before compilation.

That’s right! Dynamics are hierarchical, and at least the PayManager layer implements “dynamic new behavior at run time” by encapsulating policy patterns. The PayManager class does not need to be changed when new payers are added, that is, “the upper code does not need to be changed when changes occur”, which can also be expressed as: “Extend functionality without modifying existing code”, which is the famous “open closed principle”.

  • “Off on modifications” means: When you need to extend functionality for a class, don’t think about modifying the existing code of the class. This is not allowed! Why not? Because the existing code is the efforts of several programmers, after iterations of multiple versions, it is not easy to get the correct code. It contains extensive and profound knowledge, and you do not know the details, modify it will bug!
  • “Open to extension” means that the code of a class should be so abstracted that it does not need to modify the existing code when extending the class.

Sometimes it is expensive to change the upper class, such as PayManager, which is a library provided by another team, and if extensibility is not considered, the small feature of adding payment methods becomes a big requirement for cross-team collaboration.

When you look at the code above, is the “policy mode” abstracted lonely because PayManager doesn’t do anything in the demo, so why bother?

Other code that should be included in PayManager, such as the order information from the home server before payment, or the callback of payment results, or the retry logic after payment failure, should not be changed by the payment method, they should be fixed in PayManager class. The policy pattern is like opening a small hole in the PayManager, and the upper layer can plug in different behaviors according to the needs.

For a detailed explanation of the strategy pattern, you can click on one sentence to summarize the design pattern that leads to the same destination: Factory pattern =? Policy mode =? Template method pattern

If-else Piezo: Unable to reuse

Suppose Starbucks presets eight if-else payment methods for its Beijing store. The new Shanghai store will need two of them, plus a Shanghai-only “OK card”.

The solution with if-else idea is to produce a batch of check-out machines for Shanghai, copy and paste 2 of them from the IF-else branch of Beijing check-out machine into the new check-out machine, and add “OK card” payment method through else-if.

The Shanghai Starbucks opened “successfully”, despite the boss’s vague dissatisfaction with the increased cost of reproducing checkout machines.

In order to improve the flow, the operation department decided to seize the day of “1024 Programmers Festival” to make a big promotion, on that day, the programmers through Alipay to buy 10 cups of coffee at a 20% discount, so that they can stay up all night.

For the technical department, this operation activity is an iteration of the existing payment method. The technical leader patted his chest and said, “Small demand, 10 minutes”. Programmer Xiao Ming “fast” to make the implementation, but after the test, he has some fear: “because before is the Beijing checkout machine in the alipay payment logic copy and paste into the Shanghai checkout machine, so the same logic appears in two places, if there are guangzhou store, Shenzhen store, Nanjing store… How to do? And not only the frequent operational activities, alipay SDK update led to the ADAPTATION of API changes scattered in different places.”

In fact, xiao Ming can not be blamed, who asked the predecessors to use if-else way to achieve payment mode polymorphisms?

Logic in if-else cannot be extracted for reuse by other classes!

This is one reason why the DRY principle, don’t repeat yourself, is advocated.

But in fact, Xiao Ming’s copy and paste work is not easy to do:

class PayManager {
    var aliPay: AliPay
    varWechatPay: wechatPayvar resultHandler: Handler // Payment result handler
    var retryRunnable: Runnable // Retry logic after payment failure
    
    fun pay(type: String){
        if (type == 现金){
            payByCash()
        } else if(type == alipay) {payByAli()}else if(type == wechat) {payByWechat()}}private payByAli(){...}
}
Copy the code

Alipay’s payment logic is not all wrapped in a private method, but also scattered in various member variables within PayManager. In this scenario, copying a method into another class will often result in a lot of errors, and then copying and pasting member variables back and forth. (Copy and paste code with low coupling and high cohesion will not report errors)

As can be imagined, with the increase of the number of branches, the existing structure to adapt to the changes will increase exponentially. Although Xiao Ming works more and more overtime, the delivery speed and quality shortage are getting worse and worse. Slowly, Xiao Ming also entered a sleepless state.

Multistate status quo in project actual combat

The above story is pure fiction, but similar scenarios often occur in daily development. Here is an example from a real project.

This is a feed stream that consists of posts that start as text only. So the json structure returned by the server is very simple and clear:

{
    feeds:[
        {
            text: "UU Tour...",
            user: {},// User information
            commentCount: 15,
            likeCount: 102},... ] }Copy the code

As the iteration continued, the types of posts increased, such as pictures, videos, and voice. The server extends JSON as follows:

{
    feeds:[
        {
            type: 1.// Post type
            text: "UU Tour...",
            user: {},// User information
            commentCount: 15,
            likeCount: 102
            images: [], // Image URL array
            video: {}, // Video field
            voice: {} // Voice field},... ] }Copy the code

The client reads the type of each post and then parses the different fields. When type is voice, the voice field is read; when type is video, the video field is read.

In other words, the server uses a “big JSON” to represent a post, and each post uses only certain fields from the big JSON. The JSON structure grows larger as the post type increases. This is called a wide field.

The disadvantage of wide fields for the server is that they are type redundant and require additional null-value processing, such as the current audio post, which would have to empty all other fields that are mutually exclusive with the Voice field. (Isn’t this bug-prone for newcomers?)

Since the server has wide fields, it is easy for the client to habitually assign the corresponding God class to the large JSON, and the PostBean class representing the post in the client code has 100+ fields. This is a huge burden for newcomers to understand, because to fully understand the class, you need to know which fields in postBeans will be useful in which scenarios.

The situation is even worse than expected, because Type only contains the basic types mentioned above: text, picture, video, and voice. There are also N extended types, which cannot be distinguished by type. As a result, post type determination is an extremely complicated if-else logic. Show the getType() method in the PostBean:

public int getType(a) {
    if(timeline ! =0) {
        return TYPE_TIMELINE_YEAR;
    }
    if (TextUtils.equals(category, CATEGORY_BEHAVIOR)) {
        if (type == TYPE_IMAGE || type == TYPE_VOICE) {
            return TYPE_POST_CHECK;
        } else if (type == TYPE_TEXT || type == TYPE_VOICE_COMPLEX || type == TYPE_STREET_NORMAL) {
            return TYPE_BEHAVIOR;
        } else {
            returnTYPE_NOT_SUP; }}if (TextUtils.equals(category, CATEGORY_POKE)) {
        return TYPE_POINT;
    }
    if(mPostAdvert ! =null) {
        if (mPostAdvert == PostAdvert.RulesAdvert.INSTANCE) {
            return TYPE_RULE;
        }
        if (mPostAdvert instanceof PostAdvert.TopicHeaderAd) {
            return TYPE_TOPIC_HEADER;
        }
        if (mPostAdvert instanceof PostAdvert.PartyAdvert) {
            return TYPE_RECOMMEND_JOIN;
        }
        if (mPostAdvert instanceof PostAdvert.StreetLaneAdvert) {
            return TYPE_RECOMMEND_CIRCLE;
        }
        if (mPostAdvert instanceof PostAdvert.VoiceLaneAdvert) {
            return TYPE_RECOMMEND_AU;
        }
        if (mPostAdvert instanceof PostAdvert.TestAdvert) {
            return TYPE_RECOMMEND_TEST;
        }
        if (mPostAdvert instanceof PostAdvert.ChannelPromoteAdvert) {
            returnTYPE_CHANNEL_RECOMMEND_HEADER; }}if(liveComment ! =null) {
        return TYPE_LIVE_COMMENT;
    }


    if(insertParty ! =null) {
        return TYPE_INSERT_PARTY;
    }

    if(topicBeans ! =null) {
        return TYPE_HOT_TOPIC;
    }
    if(insertTopics ! =null) {return TYPE_INSERT_TOPIC;
    }
    if (type == TYPE_ZHUAN_FA) {
        if (sourcePost == null) {
            return TYPE_ZHUAN_FA_DELETE;
        } else {
            int gender = UserManager.getSex().blockingGet();
            if(sourcePost.selfOnly == 1) {return TYPE_ZHUAN_FA_DELETE;
            }else if(gender == 1 && (sourcePost.publicStatus == 5 || sourcePost.publicStatus == 7)) {return TYPE_ZHUAN_FA_DELETE;
            }else if(gender ==0  && (sourcePost.publicStatus == 4 || sourcePost.publicStatus == 6) ){
                return  TYPE_ZHUAN_FA_DELETE;
            }else if (sourcePost.publicStatus == 0 ) {
                return TYPE_ZHUAN_FA_DELETE;
            } else {
                if (sourcePost.type == TYPE_IMAGE || sourcePost.type == TYPE_STREET_INVITE) {
                    return TYPE_ZHUAN_FA_TEXT_PHOTO;
                } else if (sourcePost.type == TYPE_VOICE || sourcePost.type == TYPE_DUET || sourcePost.type == TYPE_VOICE_COMPLEX) {
                    return TYPE_ZHUAN_FA_AUDIO;
                } else if (sourcePost.type == TYPE_VIDEO || sourcePost.type == TYPE_MOVIE) {
                    return TYPE_ZHUAN_FA_VIDEO;
                } else {
                    returnTYPE_ZHUAN_FA_TEXT; }}}}if (type == TYPE_TEXT) {
        return TYPE_ITEM_TEXT;
    }
    if (type == TYPE_IMAGE) {
        return TYPE_ITEM_PHOTO;
    }
    if (type == TYPE_VOICE) {
        return TYPE_ITEM_VOICE;
    }
    if (type == TYPE_VOICE_COMPLEX) {
        if(ObjectsCompat.nonNull(songDet) && ! TextUtils.isEmpty(songDet.getFirstParagraph())) {return TYPE_ITEM_VOICE_CONTROL_OLD;
        } else {
            returnTYPE_ITEM_VOICE_CON; }}if (type == TYPE_STREET_INVITE) {
        return TYPE_STREET_SHARE;
    }
    if (type == TYPE_DUET) {     
        return TYPE_CHORUS;
    }
    if (type == TYPE_MOVIE) {     
        return TYPE_VIDEO;
    }
    return TYPE_NOT_SUPPORT;
}
Copy the code

In order to get the type of the post, I had to combine the type field and other N fields for comprehensive comparison, which was so complicated that I could not sleep at night.

Each new type requires a new field for God PostBean, making him more omniscient and making the already long and unreadable getType() method even more unreadable.

As iterations go on, new types are constantly inserted into the feed stream, such as:

For the server, the recommended topic in the figure above comes from another service, so the client has to fetch the data from a new interface. In this way, the content of the entire feeds stream comes from both interfaces.

Although the server can’t stick with the idea of a wide field and insert the recommended topic as a new field into the original large JSON, the client’s inertia has caused the PostBean to add another member variable.

This is not necessary because the adapter used to present the feeds stream is defined as follows:

class FeedsAdapter() :ListAdapter<PostBean, RecyclerView.ViewHolder>() {}
Copy the code

The FeedsAdapter is a “single-type list adapter” that can accommodate only one data type, PostBean. So even if the server returns a new type, the client has to treat the new type as a member variable of a PostBean, pretending it is a PostBean.

That’s not the worst of it. This is the climax of the story.

Due to the various burdens of the server and client implementations, the presentation and interaction logic of different posts has to be done by a super-large if-else in Adapter:

class FeedsAdapter() :ListAdapter<PostBean, RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        if(viewType == 1){
            create1ViewHolder()
        } else if (viewType == 2) {...}
        ...
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        if(holder is 1ViewHolder){
            holder.bind()
        } else if (holder is 2ViewHolder) {...}
        ...
    }
    
    override fun getItemViewType(position: Int): Int {
        return data[position].getType()
    }
}
Copy the code

The FeedsAdapter class on the client side is 2000+ lines long, which makes it hard to sleep at night.

With each new type, PostBean and FeedsAdapter, the two God classes become a little more God. (This violates the open and close principle)

What’s more, the post does not only appear in this interface, there are 6 different interfaces in the whole app to display the post, because the display and interaction logic of the post is written in if-else, so it cannot be reused, and can only be copied and pasted to another 5 adapters. The amount of work for each new post type or change in the interaction of a post is multiplied by 6. He doesn’t know the business. He doesn’t know that there are 5 potholes waiting for him in the code. The way he gets the bad news is probably because of a bug mentioned in the test.

A polymorphic solution

In order to solve the three shortcomings of high complexity, poor scalability and reuse. I offer a solution:

Server side disallows wide fields

Instead of returning large JSON with all the redundant fields, the server uses the following form:

{
    feeds:[
        {
            type: 1,
            data: {
                text:""
            }
        },
        {
            type: 2,
            data: {
                imgUrls: []
            }
        }
    ]
    ...
}
Copy the code

That is, a separate JSON structure is configured for each type, each corresponding to an entity class on the client side.

class BaseBean {
    String type
}
Copy the code

This is the base class of the entity class, and it contains the field Type common to all entity classes, which is used to implement polymorphism when parsing.

// Text paste entity class
class TextBean : BaseBean {
    String text
}

// Image post entity class
class ImageBean: BaseBean {
    List<String> imageUrls
}
Copy the code

The client needs to customize a parser that inherits JsonDeserializer when parsing the multi-type JSON:

class MyDeserializer implements JsonDeserializer<List<BaseBean>> {
    @Override
    public List<BaseBean> deserialize(JsonElement element, Type type,JsonDeserializationContext context) throws JsonParseException {
        JsonArray array = element.getAsJsonArray();
        List<BaseBean> list = new ArrayList<>();
        for (JsonElement e : array) {
            int type = e.getAsJsonObject().get("type").getAsInt();
            if(type == 1) {
                list.add(new Gson().fromJson(e, TextBean.class));
            } else if(type == 2) {
                list.add(newGson().fromJson(e, ImageBean.class)); }}returnlist; }}Copy the code

This is the only if-else block that appears in the entire scenario.

The network requests the client with a List

, but each element in the List is polymorphic through inheritance.

This solution provides client memory and CPU performance by reducing the size of the JSON string returned by the server and reducing the number of fields to parse per post (up from 53 fields for each post of any type).

Multi-type list adapter

The next question is, how to design a multi-type list adapter, I in the strategy pattern application | every time for the new type RecyclerView get mad when this article made a detailed analysis.

The summary is as follows:

  1. Table entry presentation and data binding are abstracted into policies that are declared with generics to indicate which type of data is encountered and which policy is used. An Adapter holds a set of policies and a set of abstract dataList<Any>. To add a new type is to inject a new policy. (The Adapter does not need to be modified)
  2. The main function of the Adapter is to match different policies for different data types. In this way, the presentation and interaction of each entry is wrapped in a separate policy class that can be injected into any Adapter for reuse of different interfaces.

Performance optimization

Finally, there is a performance concern.

Suppose the feeds stream contains 20 types, and in a single pull, the server returns 10 posts, all of them of different types. When you scroll through 10 posts, none of the entries can be reused, because RecyclerView reuse is type based, Scrolls off the screen and roll into the table entries must be the same type of the screen to reuse (about RecyclerView reuse logic analysis can click RecyclerView caching mechanism | how to reuse table?) , so it is better to break a post into subitems:

As shown, posts are divided into user area, text area, video area, tag area and bottom sidebar area. It can be understood that a complete post is now divided into five posts, which correspond to five different beans and five different policies within the Adapter.

This way, after the avatar of the previous video post rolls off the screen, the avatar of the next audio post hits the cache and can be reused.

But there are downsides:

  1. Semantic inconsistencies: a post in the eyes of the client and a post in the eyes of the server are not the same Bean, and the client has to split the post in the eyes of the server into n sub-posts. For those of you on the server side who are reading this, I have a question to ask: if the server also returns a different JSON structure for each post subarea, what’s wrong with that?
  2. The overall operation of a post becomes more complex, such as deleting a post, and now you must traverse the Adapter data set to delete all beans with the same ID as the post ID.

conclusion

To make code ugly, long, and difficult to maintain, the following principles must be followed:

  1. Don’t design for inaction before you write code. Don’t distinguish between “logic that changes” and “logic that doesn’t change.” Iterative progress is unpredictable, and design in advance will become “over-design”.
  2. When dealing with multiple types of problems (and the number of types can vary), forget about polymorphism, and never use the polymorphic mechanisms that the programming language already has in mind, such as inheritance, interfaces, and overloading. You can only use if-else for classification discussions.
  3. Follow the JRY principle (Just Repeat Yourself), CTRL + C and CTRL + V skills in hand, always be ready to copy and paste, and have the same code scattered all over the project so that more eggs can be hidden when the bucket runs.
  4. Follow the principle of open for change, close for extension, change the base class when you have a problem, so that each change has a larger scope of influence, so that you and the test sister become friends in trouble.