Written in the book of the former
The title of this article refers to protocol-oriented programming (hereinafter referred to as POP) because I read an article about POP in Swift a few days ago. This article will use this as a starting point to talk about related concepts such as interfaces, mixins, composite patterns, and multiple inheritance, as well as illustrate my ideas with examples from various languages.
I’m sure every reader will be familiar with those cliches, and I won’t be bored enough to go over them again. I will try to make a summary of them from a higher perspective, but due to my limited experience and level, there will be some omissions. Welcome to exchange and discuss.
One last wordy word:
There is no silver bullet
The Swift POP
Swift puts a lot of emphasis on the concept of POP, which if you’re an old programmer who uses Objective-C (or some language like Java), you might think is a “new” programming concept. Some articles even scream, “Abandon object orientation for protocol orientation.” This is fundamentally wrong.
An interface
First of all, the idea of protocol-oriented has been around for many years, and the concept of “programming for interfaces, not implementations” has been mentioned in many classic books.
Let’s say we have a class, light bulb, and a method of type light bulb that calls the light bulb’s “on” and “off” methods. Writing in interface-oriented terms, you define the parameter type as an interface, such as Openable, in which you define open and close methods.
The advantage of this is that if you have another class in the future, such as a TV, it can be used as an argument to the method as long as it implements the Openable interface. This satisfies the idea of “open to expansion, closed to revision”.
The natural thought was, why can’t I define a parent of a light bulb and a TV instead of just the interface? The simple answer is that light bulbs and televisions probably already have parents, and even if they don’t, they can’t be defined so hastily.
Disadvantages of interfaces
So at this stage, you can think of an interface as a kind of classification that can group multiple unrelated classes into the same category. But the interface also has a major drawback, because it is a constraint, not an implementation. That is, a class that implements an interface needs to implement its own methods in that interface.
Sometimes you’ll find that it’s actually nice to have a default implementation, just like inheritance. Using the light bulb as an example, we would like the Openable interface to provide a default implementation of open and close methods that call the sound function, assuming that all appliances make a sound every time they are turned on and off. For example, if my appliance needs to count the number of times it is turned on or off, I would want the Openable protocol to define a count variable and count it every time it is turned on or off.
Obviously using interfaces is not going to do the job, because interfaces have very poor support for code reuse, so the use of interface-oriented scenarios in client development (such as Objective-C) is not very common except for some very large projects (such as JDBC).
The improvement of Swift,
Swift places such emphasis on POP in the first place because protocol-oriented programming does have its advantages. Imagine the following inheritance:
B and C inherit from A, B1 and B2 inherit from B, C1 and C2 inherit from C
If you find that B1 and C2 have certain features in common, the full use of inheritance is to find the nearest ancestor of B1 and C2, which is A, and add A piece of code to A. So you have to override B2 and C1 to disable this method. As A result, the code of A becomes bigger and bigger and becomes A God Class, which is very difficult to maintain.
If you use an interface, you’re back to the problem above, and you have to write the method implementation twice in B1 and C2. The emphasis on POP in Swift is due to the extension that Swift provides to the protocol by providing a default implementation of the methods specified in the protocol. Now let B1 and C2 implement this protocol without affecting the class inheritance structure or requiring duplicate code.
Seems Swift’s POP has no problem? The answer is clearly no.
Multiple inheritance
If you look at Protocol Extension from a higher perspective, it’s not magic, just an implementation of multiple inheritance. Theoretical multiple inheritance is problematic, the most common of which is Diamond Problem. It describes this situation:
B and C inherit from A, D from B and C
See the following image (from Wikipedia):
If classes A, B, and C all define the test method, what happens if an instance object of D calls the test method?
It can be argued that almost all major languages support the idea of multiple inheritance, but not all support explicitly defining multiple inheritance as C++ does. Nevertheless, they all provide various solutions to avoid the Diamond Problem, the core of which is the conflict between legal names and variable names of different parent classes.
I have chosen five common languages and come up with four typical solutions:
- Explicit support for multiple inheritance, representing languages Python and C++
- Use Interface to represent the language Java
- Traits are used to represent languages Swift and Java8
- Use mixins, which represent the language Ruby
Explicit support for multiple inheritance
The simplest way is to support multiple inheritance directly, typically C++ and Python.
C++
In C++, you can specify that a class inherits from more than one parent class, which in fact holds instances of more than one parent class (except for virtual inheritance). When a function name conflict occurs, the programmer needs to manually specify which parent class method to call, otherwise it will not compile:
#include using namespace std; class A { public: void test() { cout << "A\n"; }}; class B: public A { public: void test() { cout << "B\n"; }}; class C: public A { public: void test() { cout << "C\n"; }}; class D: public B, public C {}; int main(int argc, char *argv[]) { D *d = new D(); // d->test(); // Compilation failed, you must specify which parent class method to call. d->B::test(); d->C::test(); }Copy the code
As you can see, C++ gives programmers the power of manual management at the cost of implementation complexity.
Python
Python solves the problem of function name collisions by reducing a complex inheritance tree to an inheritance chain. To this end, it adopts C3 Linearization algorithm, the results of which are closely related to the inheritance order, as shown in the following figure:
Suppose the order of inheritance is as follows:
- class K1 extends A, B, C
- class K2 extends D, B, E
- class K3 extends D, A
- class Z extends K1, K2, K3
To find the inheritance chain of Z is actually the process of flattening the sequence [[K1, A, B, C], [K2, D, B, E], [K3, D, A]].
We first iterate over the first element K1, which can be extracted if it appears only at the beginning of each array. In this case, K1 obviously only appears at the beginning of the first array, so it can be extracted. Similarly, K2 and K2 can be extracted. So the question becomes:
[A, B, C] [D, B, E], [D, A]
It then iterates through to A, which cannot be fetched because it appears at the end of the third array. Similarly, B and C don’t satisfy this requirement. Finally, it is found that D meets the requirements and can be extracted. And so on… Complete documentation is available at WikiPedia.
The final inheritance chain is: [K1, K2, K3, D, A, B, C, E], so that multiple inheritance is transformed into single inheritance, naturally there is no method name conflict problem.
As you can see, Python doesn’t give the programmer a choice. It calculates the inheritance relationship automatically. We can also use __mro__ to see the inheritance relationship:
class A(object):
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
class E(C, B):
pass
print(D.__mro__)
print(E.__mro__)
# (, , , , )
# (, , , , )Copy the code
Interface
Java’s Interface takes a different approach, and while it is also multiple inheritance, it is merely “specification inheritance,” meaning you inherit what you can do, but not how you do it. The drawbacks of this approach have already been mentioned, but here is just an explanation of how it handles conflicts.
In Java, even if a class implements multiple protocols with the same method specified in these protocols, the class can only be implemented once. Therefore, multiple protocols share the same implementation, which is not a good solution in my opinion.
In Java 8, methods in protocols can add default implementations. When there is a method conflict in multiple protocols, subclasses must override the method (or get an error) and call the default implementation of a protocol as needed (much like C++):
interface HowEat{ public abstract String howeat(); default public void test() { System.out.println("tttt"); } } interface HowToEat { public abstract String howeat(); default public void test() { System.out.println("yyyy"); } } class Untitled implements HowEat, HowToEat { public void test() { HowEat.super.test(); // Select the HowEat implementation and print TTTT system.out.println (" SSSS "); } public static void main(String[] args) { Untitled t = new Untitled(); System.out.println(t.howeat()); t.test(); }}Copy the code
Trait
Although the default Implementation that provides protocol methods is called differently in different languages, it is commonly referred to as a Trait, which can be easily interpreted as Trait = Interface + Implementation.
The Trait is a relatively elegant multi-inheritance solution. It not only provides the concept of multi-inheritance, but also does not change the original inheritance structure. A class can only have one parent class. In different languages, the details of traits are different. For example, in Swift, when we rewrite methods, we can only call methods that are not defined in Protocol, otherwise we will generate segment errors:
protocol Addable { // func add(); } extension Addable {func add() {print ("Addable add"); } } class CustomCollection {} extension CustomCollection: Addable { func add() { (self as Addable).add() print("CustomCollection add"); } } var c = CustomCollection() c.addAll()Copy the code
After consulting relevant information, it is found that this is related to the static distribution and dynamic distribution of Swift method.
Mixin
Another solution similar to a Trait is called Mixin, which is adopted by Ruby and can be understood as Mixin = Trait + LOCAL_variable. In Ruby, the hierarchy of multiple inheritance is much flatter and can be understood as: “Once a module is brought in by a mixin, its host module immediately has all the properties and methods of the mixin module.” Like the runtime in OC, this is more of a metaprogramming idea:
module Mixin Ss = "mixin" define_method(:print) { puts Ss } end class A include Mixin puts Ss end a = A.new() a.print # Output a mixinCopy the code
conclusion
Using traits or mixins is more elegant than allowing multiple inheritance completely (C++/Python) and almost not (Java). Although they are sometimes not very convenient to specify a method in a “parent class”, the idea of using single inheritance to simulate multiple inheritance has a unique twist: “do not change the inheritance tree”, which will be discussed later.
Inheritance and composition
At the beginning of this article, I once said that Swift’s POP is not a great thing. Besides the idea of interface has been put forward long ago, its essence is inheritance, which cannot get rid of the natural defects of inheritance relationship. As for replacing OOP with POP, it’s nonsense. Multiple inheritance is OOP, and how can a slightly more elegant implementation be called a replacement?
Disadvantages of inheritance
Some people say that inheritance is not bottom-up abstraction, but top-down refinement. I don’t think I understood this, but one of the main purposes of using inheritance is to reuse code. In OOP, with inheritance, we enjoy the benefits of encapsulation and polymorphism, but improper use of inheritance often boomerang.
encapsulation
Once you inherit from a parent class, you immediately have all the methods and properties of the parent class, and using inheritance breaks good encapsulation if those methods and properties are not what you intended to expose. For example, you might inherit an array when defining a Stack:
Class Stack extends ArrayList {public void push(Object value) {... } public Object pop() {... }}Copy the code
While you’ve succeeded in adding push and pop methods to the array, you’ve exposed other methods of the array that Stack doesn’t need.
Another way to think about it is when do you expose the interface of a superclass? The answer is “when you are a refinement of the superclass”, which is the is-A concept we are emphasizing. Inheritance should only be used if you are truly a parent and can replace the parent wherever it appears (the Richter’s substitution principle). In this example, the stack is clearly not a refinement of the array, because arrays are random-access and stacks are linear access.
In this case, the correct approach is to use composition, defining a class Stack that holds array objects to access its own data, while exposing only the necessary push and POP methods.
Another behavior that can break encapsulation is to have business-related classes inherit from utility classes. For example, if we have a class that needs to hold multiple Customer objects internally, we should choose the composite mode and hold an array instead of directly inheriting from the array. For similar reasons, business modules should mask implementation details externally.
The same concept applies to the Stack example, which is a business implementation with special rules that should not expose the implementation interface of an array as opposed to an array implementation.
polymorphism
Polymorphism is a powerful weapon in OOP, and because of the IS-A relationship, subclasses can be used directly as superclasses. In this way, the subclass has a strong coupling relationship with the parent class. Any modification of the parent class will affect the subclass, and such modification will affect the interface exposed by the subclass, causing all instances of the subclass to need to be modified. The corresponding composite pattern, when the “parent” changes, only affects the implementation of the child class, but does not affect the interface of the child class, so all instances of the child class need not be modified.
In addition, polymorphism can cause serious bugs:
public class CountingList extends ArrayList { private int counter = 0; @Override public void add(T elem) { super.add(elem); counter++; } @Override public void addAll(Collection other) { super.addAll(other); counter += other.size(); }}Copy the code
This subclass overrides the implementation of the add method, incrementing the count count by one. The problem, however, is that the subclass’s addAll method is already counted, and it calls the addAll method of the parent class, which in turn calls add. Note that the subclass’s add method is called because of polymorphism, which means that the final result count is twice as large as expected.
Even worse, if the parent class is provided by the SDK, the subclass is completely unaware of the implementation details of the parent class and cannot possibly be aware of the cause of the error. To avoid these mistakes, in addition to gaining experience, ask yourself every time you use inheritance if a subclass is a refinement of a parent class and has an IS-A relationship, rather than just to reuse code.
You should also check whether subclasses and superclasses have a business-implementation relationship, and if so, you should consider using composition. In this case, for example, subclasses add counting logic to the parent class, favoring the business implementation over the (implementation-favoring) refinement of the parent class, so inheritance is not appropriate.
combination
Despite the usual talk of preference for composition, the composite pattern is not without its drawbacks. First, the composite pattern breaks the connection between the original parent and child classes. Multiple “subclasses” that use composite patterns no longer have anything in common and can’t enjoy the benefits of interface oriented programming or polymorphism.
Using a composite pattern is more like a proxy, and if you find that a held class has a large number of methods that need to be proided by the outer class, then you should consider using inheritance.
Look at the POP
For a language that uses the Trait or Mixin pattern, although it is still inherited in nature, it does not have the aforementioned polymorphism problem because it adheres to the single-inheritance model and there is no IS-A relationship.
Interested readers can choose Swift or Java to try it out.
From this point of view, Swift’s POP simulates multiple inheritance relationships and realizes cross-parent reuse of code without is-A relationships. But it still uses the idea of inheritance, so it’s not a silver bullet. In use should still be carefully considered, distinguish the difference with combination mode, make a reasonable choice.
The resources
- Ruby: How do I access module local variables?
- Swift protocol extension method dispatch
- Composition vs. Inheritance: How to Choose?
- Protocol-Oriented Programming in Swift
- Multiple inheritance
- python c3 linearization