Article 16 of Effective Java Chinese Edition 2 states:
Inheritance is a powerful tool for code reuse, but it’s not always the best tool to do the job.
Composition is better than inheritance.
What’s wrong with inheritance?
Inheritance breaks the encapsulation of classes and subclasses depend on the implementation details of specific functions in their parent classes.
When is inheritance safe
- Inheritance is used inside the package; there is no cross-package inheritance.
- Designed specifically for extension and well documented.
A case in point
Implement a HashSet that keeps track of how many elements have been added since it was created.
Using inheritance implementation
public class InstrumentedSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedSet(a) {}public InstrumentedSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount(a) {
returnaddCount; }}Copy the code
Class uses the addCount field to count the number of times an element is added, and sets the addCount field to override the parent class’s add() and addAll() implementations.
In the following program, we expect getAddCount() to return 3, but it actually returns 6.
InstrumentedSet<String> s = new InstrumentedSet<String>();
s.addAll(Arrays.asList("Snap"."Crackle"."Pop"));
Copy the code
Here’s the problem: In a HashSet, addAll() is implemented based on the add() method. It is dangerous for a subclass to extend the functionality of its parent class without knowing the implementation details, and the implementation of the parent class may change in the future, since it is not designed for extension.
Use composition implementation
Instead of extending an existing class, you add a private field to the new class that references an instance of the existing class. This design is called a combination.
Start by creating a clean SetWrapper composite class.
public class SetWrapper<E> implements Set<E> {
private final Set<E> s;
public SetWrapper(Set<E> s) { this.s = s; }
public void clear(a) { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty(a) { return s.isEmpty(); }
public int size(a) { return s.size(); }
public Iterator<E> iterator(a) { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o){ return s.remove(o); }
public boolean containsAll(Collection
c) { return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
public boolean removeAll(Collection
c) { return s.removeAll(c); }
public boolean retainAll(Collection
c) { return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o) { return s.equals(o); }
@Override public int hashCode(a) { return s.hashCode(); }
@Override public String toString(a) { returns.toString(); }}Copy the code
SetWrapper implements the decorator pattern, programming interface oriented by referencing fields of type Set
, and is more flexible than directly inheriting the HashSet class. You can pass in any Set concrete class in the constructor that calls the class. Extend the class to fulfill the requirements.
public class InstrumentedSet<E> extends SetWrapper<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount(a) {
returnaddCount; }}Copy the code
The lines
Note: the following code is pseudo code, the implementation of the combination method is packaged into the Android library and has been open source, namedModapterModular Adapter Yeah, there was an AD here.
Let’s look at the problem first. Here are two screenshots of an app I developed:
The details page and the comment list page reuse the implementation of the comment item.
GameComentsAdapter for the comment list page.
public class GameCommentsAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private static final int ITEM_TYPE_COMMENT = 1;
private List<Object> mDataSet;
@Override
public int getItemViewType(int position) {
Object item = getItem(position);
if (item instanceof Comment) {
return ITEM_TYPE_COMMENT;
}
return super.getItemViewType(position);
}
protected Object getItem(int position) {
return mDataSet.get(position);
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == ITEM_TYPE_COMMENT) {
View itemView = inflater.inflate(R.layout.item_comment, parent, false);
return new CommentViewHolder(itemView);
}
return null;
}
@Override
public int getItemCount(a) {
returnmDataSet.size(); }}Copy the code
If-else
Modify the GameComentsAdapter class to add support for game details.
public class GameCommentsAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private static final int ITEM_TYPE_COMMENT = 1;
private static final int ITEM_TYPE_GAME_DETAIL = 2;
private List<Object> mDataSet;
@Override
public int getItemViewType(int position) {
Object item = getItem(position);
if (item instanceof Comment) {
return ITEM_TYPE_COMMENT;
}
if (item instanceof GameDetail) {
return ITEM_TYPE_GAME_DETAIL;
}
return super.getItemViewType(position);
}
protected Object getItem(int position) {
return mDataSet.get(position);
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == ITEM_TYPE_COMMENT) {
View itemView = inflater.inflate(R.layout.item_comment, parent, false);
return new CommentViewHolder(itemView);
}
if (viewType == ITEM_TYPE_GAME_DETAIL) {
View itemView = inflater.inflate(R.layout.item_game_detail, parent, false);
return new GameDetailViewHolder(itemView);
}
return null;
}
@Override
public int getItemCount(a) {
returnmDataSet.size(); }}Copy the code
Create a GameCommentsAdapter object for RecyclerView in the game details page. However, this method will make the GameCommentsAdapter become bloated and does not meet the OCP open and close principle.
Inheritance implementation
To extend an Adapter, at least implement methods such as getItemViewType() and onCreateViewHolder(). The GameDetailAdapter of the details page inherits this class.
class GameDetailAdapter extends GameCommentsAdapter {
private static final int ITEM_TYPE_GAME_DETAIL = 2;
@Override
public int getItemViewType(int position) {
Object item = getItem(position);
if (item instanceof GameDetail) {
return ITEM_TYPE_GAME_DETAIL;
}
return super.getItemViewType(position);
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_GAME_DETAIL) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.item_game_detail, parent, false);
return new GameDetailViewHolder(itemView);
}
return super.onCreateViewHolder(parent, viewType); }}Copy the code
Suddenly there was a new need
The product wants to add recommendation items on the details page and reuse the list items on the home page, as shown below:
The realization effect is shown in the figure below:
Java is single-inherited. The GameDetailAdapter already inherits the GameComentsAdapter class and cannot inherit HomeAdapter.
Should we continue to add if judgments to the GameComentsAdapter class?
combination
Some of the code has been omitted for ease of reading.
Define a modular Adapter Adapter class. In order to be managed by the Adapter class, data items and view items need to be compatible: the former inherits the AbstractItem class, and the latter inherits the ItemViewHolder class.
class Comment extends AbstractItem {}
class GameDetail extends AbstractItem {}
class Game extends AbstractItem {}
class CommentViewHolder extends ItemViewHolder<Comment> {}
class GameDetailViewHolder extends ItemViewHolder<GameDetail> {}
class GameViewHolder extends ItemViewHolder<Game> {}
Copy the code
The AbstractItem class defines a Type attribute that represents the type of the data item. This attribute is compared to the registered data item configuration. If the value of the Type attribute is the same, a ViewHolder corresponding to the data item is created.
If you cannot inherit the AbstractItem class because of Java single inheritance, you can choose to implement the Item interface and implement the following methods.
public interface Item {
void setType(int type);
int getType(a);
}
Copy the code
At this point, the data items and view items are ready to be combined to implement the requirements.
On the comment list page, create an Adapter instance and add the comment item functionality.
List<Item> dataSet = new ArrayList<>();
dataSet.add(new Comment());
dataSet.add(new GameDetail());
Adapter adapter = new Adapter();
adapter.getManager()
.register(ITEM_TYPE_COMMENT, CommentViewHolder.class)
.register(ITEM_TYPE_GAME_DETAIL, GameDetailViewHolder.class)
.setList(dataSet);
Copy the code
On the game details page, create an Adapter instance and add the game item functionality.
List<Object> dataSet = new ArrayList<>();
dataSet.add(new Comment());
dataSet.add(new GameDetail());
dataSet.add(new Game());
Adapter adapter = new Adapter();
adapter.getManager()
.register(ITEM_TYPE_COMMENT, CommentViewHolder.class)
.register(ITEM_TYPE_GAME_DETAIL, GameDetailViewHolder.class)
.register(ITEM_TYPE_GAME, GameViewHolder.class)
.setList(dataSet);
Copy the code
When a page no longer supports comment items, we simply delete the following code and do not change it elsewhere, complying with OCP design principles.
dataSet.add(new Comment());
adapter.getManager().unregister(ITEM_TYPE_COMMENT);
Copy the code
Realize the principle of
The ItemManager interface is introduced to unify item data, register and unregister view item configuration information.
public interface ItemManager {
ItemManager setList(List<? extends Item> list);
<T extends ViewHolder> ItemManager register(int type, Class<T> holderClass);
<T extends ViewHolder> ItemManager register(int type, @LayoutRes int layoutId, Class<T> holderClass);
ItemManager register(ItemConfig config);
ItemManager unregister(int type);
<T extends Item> T getItem(int position);
}
Copy the code
The interface implementation class is AdapterDelegate, mainly implemented getItemViewType, onCreateViewHolder, onBindViewHolder three if-else disaster area methods.
public final class AdapterDelegate implements ItemManager {
public int getItemViewType(int position) {
Item item = getItem(position);
ItemConfig adapter = null;
if(item ! =null) {
adapter = registry.get(item.getType());
}
if (adapter == null) {
// TODO
return 0;
}
return adapter.getType();
}
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ItemConfig adapter = registry.get(viewType);
if (adapter == null) {
return null;
}
int layoutId = adapter.getLayoutId();
layoutId = layoutId == 0 ? adapter.getType() : layoutId;
if (layoutId > 0) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(layoutId, parent, false);
return createViewHolder(itemView, adapter.getHolderClass());
}
return null;
}
@SuppressWarnings("unchecked")
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Item item = getItem(position);
if (holder instanceofItemViewHolder) { ItemViewHolder viewHolder = (ItemViewHolder) holder; viewHolder.setItem(item); viewHolder.onViewBound(item); }}}Copy the code
Implement a unique Adapter using an AdapterDelegate, delegating the main code to the former.
public class Adapter extends RecyclerView.Adapter<ViewHolder> {
private AdapterDelegate delegate = new AdapterDelegate();
@Override
public int getItemViewType(int position) {
return delegate.getItemViewType(position);
}
@NonNull
@Override
public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return delegate.onCreateViewHolder(parent, viewType);
}
@Override
public final void onBindViewHolder(@NonNull ViewHolder holder, int position) {
delegate.onBindViewHolder(holder, position);
}
public ItemManager getManager(a) {
returndelegate; }}Copy the code
extension
Outside of the Java ecosystem, there are several practices for composition that are better than inheritance.
Kotlin
The Kotlin language has a Delegation mechanism that makes it easy for developers to use composition.
interface Base {
fun print(a)
}
class BaseImpl(val x: Int) : Base {
override fun print(a) { print(x) }
}
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print()
}
Copy the code
Kotlin version InstrumentedHashSet
class InstrumentedHashSet<E>(val set: MutableSet<E>)
: MutableSet<E> by set {
private var addCount : Int = 0
override fun add(element: E): Boolean {
addCount++
return set.add(element)
}
override fun addAll(elements: Collection<E>): Boolean {
addCount += elements.size
return set.addAll(elements)
}
}
Copy the code
Go
The Go language has no inheritance mechanism and uses native support composition to reuse code. The following are the Reader and Writer interface definitions.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Copy the code
Composition allows you to define new types that can read and write.
type ReadWriter interface {
Reader
Writer
}
Copy the code
The examples above are interface composition and can also be implementation composition. (The following example comes from the book Go in Action)
type user struct {
name string
email string
}
// notify implements a method that can be called via
// a value of type user.
func (u *user) notify(a) {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
// admin represents an admin user with privileges.
type admin struct {
user // Embedded Type
level string
}
// main is the entry point for the application.
func main(a) {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "[email protected]",
},
level: "super",}// We can access the inner type's method directly.
ad.user.notify()
// The inner type's method is promoted.
ad.notify()
}
Copy the code
Recommended books
- Effective Java Chinese version 2
- refactoring
- Refactoring and patterns
- Software design refactoring
The resources
- Effective Java Chinese version 2
- AdapterDelegates
- OneAdapter