Effective Java Tuesday! Favor Composition Over Inheritance

Effective Java Review (90 Part Series)

1 Effective Java Tuesday! Let’s Consider Static Factory Methods
2 Effective Java Tuesday! The Builder Pattern!
86 more parts…
3 Effective Java Tuesday! Singletons!
4 Effective Java Tuesday! Utility Classes!
5 Effective Java Tuesday! Prefer Dependency Injection!
6 Effective Java Tuesday! Avoid Creating Unnecessary Objects!
7 Effective Java Tuesday! Don’t Leak Object References!
8 Effective Java Tuesday! Avoid Finalizers and Cleaners!
9 Effective Java Tuesday! Prefer try-with-resources
10 Effective Java Tuesday! Obey the `equals` contract
11 Effective Java Tuesday! Obey the `hashCode` contract
12 Effective Java Tuesday! Override `toString`
13 Effective Java Tuesday! Override `clone` judiciously
14 Effective Java Tuesday! Consider Implementing `Comparable`
15 Effective Java Tuesday! Minimize the Accessibility of Classes and Member
16 Effective Java Tuesday! In Public Classes, Use Accessors, Not Public Fields
17 Effective Java Tuesday! Minimize Mutability
18 Effective Java Tuesday! Favor Composition Over Inheritance
19 Effective Java Tuesday! Design and Document Classes for Inheritance or Else Prohibit It.
20 Effective Java Tuesday! Prefer Interfaces to Abstract Classes
21 Effective Java! Design Interfaces for Posterity
22 Effective Java! Use Interfaces Only to Define Types
23 Effective Java! Prefer Class Hierarchies to Tagged Classes
24 Effective Java! Favor Static Members Classes over Non-Static
25 Effective Java! Limit Source Files to a Single Top-Level Class
26 Effective Java! Don’t Use Raw Types
27 Effective Java! Eliminate Unchecked Warnings
28 Effective Java! Prefer Lists to Array
29 Effective Java! Favor Generic Types
30 Effective Java! Favor Generic Methods
31 Effective Java! Use Bounded Wildcards to Increase API Flexibility
32 Effective Java! Combine Generics and Varargs Judiciously
33 Effective Java! Consider Typesafe Heterogenous Containers
34 Effective Java! Use Enums Instead of int Constants
35 Effective Java! Use Instance Fields Instead of Ordinals
36 Effective Java! Use EnumSet Instead of Bit Fields
37 Effective Java! Use EnumMap instead of Ordinal Indexing
38 Effective Java! Emulate Extensible Enums With Interfaces.
39 Effective Java! Prefer Annotations to Naming Patterns
40 Effective Java! Consistently Use the Override Annotation
41 Effective Java! Use Marker Interfaces to Define Types
42 Effective Java! Prefer Lambdas to Anonymous Classes
43 Effective Java! Prefer Method References to Lambdas
44 Effective Java! Favor the Use of Standard Functional Interfaces
45 Effective Java! Use Stream Judiciously
46 Effective Java! Prefer Side-Effect-Free Functions in Streams
47 Effective Java! Prefer Collection To Stream as a Return Type
48 Effective Java! Use Caution When Making Streams Parallel
49 Effective Java! Check Parameters for Validity
50 Effective Java! Make Defensive Copies When Necessary
51 Effective Java! Design Method Signatures Carefully
52 Effective Java! Use Overloading Judiciously
53 Effective Java! Use Varargs Judiciously
54 Effective Java! Return Empty Collections or Arrays, Not Nulls
55 Effective Java! Return Optionals Judiciously
56 Effective Java: Write Doc Comments For All Exposed APIs
57 Effective Java: Minimize The Scope of Local Variables
58 Effective Java: Prefer for-each loops to traditional for loops
59 Effective Java: Know and Use the Libraries
60 Effective Java: Avoid Float and Double If Exact Answers Are Required
61 Effective Java: Prefer Primitive Types to Boxed Types
62 Effective Java: Avoid Strings When Other Types Are More Appropriate
63 Effective Java: Beware the Performance of String Concatenation
64 Effective Java: Refer to Objects By Their Interfaces
65 Effective Java: Prefer Interfaces To Reflection
66 Effective Java: Use Native Methods Judiciously
67 Effective Java: Optimize Judiciously
68 Effective Java: Adhere to Generally Accepted Naming Conventions
69 Effective Java: Use Exceptions for Only Exceptional Circumstances
70 Effective Java: Use Checked Exceptions for Recoverable Conditions
71 Effective Java: Avoid Unnecessary Use of Checked Exceptions
72 Effective Java: Favor The Use of Standard Exceptions
73 Effective Java: Throw Exceptions Appropriate To The Abstraction
74 Effective Java: Document All Exceptions Thrown By Each Method
75 Effective Java: Include Failure-Capture Information in Detail Messages
76 Effective Java: Strive for Failure Atomicity
77 Effective Java: Don’t Ignore Exceptions
78 Effective Java: Synchronize Access to Shared Mutable Data
79 Effective Java: Avoid Excessive Synchronization
80 Effective Java: Prefer Executors, Tasks, and Streams to Threads
81 Effective Java: Prefer Concurrency Utilities Over wait and notify
82 Effective Java: Document Thread Safety
83 Effective Java: Use Lazy Initialization Judiciously
84 Effective Java: Don’t Depend on the Thread Scheduler
85 Effective Java: Prefer Alternatives To Java Serialization
86 Effective Java: Implement Serializable With Great Caution
87 Effective Java: Consider Using a Custom Serialized Form
88 Effective Java: Write readObject Methods Defensively
89 Effective Java: For Instance Control, Prefer Enum types to readResolve
90 Effective Java: Consider Serialization Proxies Instead of Serialized Instances

Today we get to take on one of the core items of object-oriented programming, inheritance. Particularly the dangers of inheritance and what is a better way in many cases. Inheritance is a core part of what makes object-oriented great and powerful when used correctly. What we will go over in this blog post is a particular pattern that gives us some of the capabilities of inheritance, while keeping us safe and maintaining encapsulation.

First off, there are a couple of different types of inheritance; the first is implementation inheritance which is one class extends the functionality of another class. This is the type of inheritance we will be talking about in this blog post. There is also interface inheritance where a class implements an interface or one interface extends another interface. This second type of inheritance is not covered in this blog post.

The core of what the title of this blog post comes down to is that inheritance breaks encapsulation. The problem comes in that changes to the super class can cause issues in the sub-classes without the sub-classes realizing it. This can lead to breakage, unexpected data leakage, and other issues that would best be avoided. This means the sub-classes need to evolve in lock step with their parent classes or risk encountering issues.

Let’s look at an example, and although contrived, the example in the book is solid at showing the issues. The idea behind this example class is that it creates a method for a user to have a HashSet that can retrieve how many times an item was added to it. Let’s take a look.

<span>@NoArgsConstructor</span>
<span>public</span> <span>class</span> <span>InstrumentedHashSet</span><span><</span><span>E</span><span>></span> <span>extends</span> <span>HashSet</span><span><</span><span>E</span><span>></span> <span>{</span>
<span>@Getter</span>
<span>private</span> <span>int</span> <span>addCount</span> <span>=</span> <span>0</span><span>;</span>
<span>public</span> <span>InstrumentedHashSet</span><span>(</span><span>int</span> <span>initCap</span><span>,</span> <span>float</span> <span>loadFactor</span><span>)</span> <span>{</span>
<span>super</span><span>(</span><span>initCap</span><span>,</span> <span>loadFactor</span><span>);</span>
<span>}</span>
<span>@Override</span>
<span>public</span> <span>boolean</span> <span>add</span><span>(</span><span>E</span> <span>e</span><span>)</span> <span>{</span>
<span>addCount</span><span>++;</span>
<span>return</span> <span>super</span><span>.</span><span>add</span><span>(</span><span>e</span><span>);</span>
<span>}</span>
<span>@Override</span>
<span>public</span> <span>boolean</span> <span>addAll</span><span>(</span><span>Collection</span><span><?</span> <span>extends</span> <span>E</span><span>></span> <span>c</span><span>)</span> <span>{</span>
<span>addCount</span> <span>+=</span> <span>c</span><span>.</span><span>size</span><span>();</span>
<span>return</span> <span>super</span><span>.</span><span>addAll</span><span>(</span><span>c</span><span>);</span>
<span>}</span>
<span>}</span>
<span>@NoArgsConstructor</span>
<span>public</span> <span>class</span> <span>InstrumentedHashSet</span><span><</span><span>E</span><span>></span> <span>extends</span> <span>HashSet</span><span><</span><span>E</span><span>></span> <span>{</span>
  <span>@Getter</span>
  <span>private</span> <span>int</span> <span>addCount</span> <span>=</span> <span>0</span><span>;</span>

  <span>public</span> <span>InstrumentedHashSet</span><span>(</span><span>int</span> <span>initCap</span><span>,</span> <span>float</span> <span>loadFactor</span><span>)</span> <span>{</span>
    <span>super</span><span>(</span><span>initCap</span><span>,</span> <span>loadFactor</span><span>);</span>
  <span>}</span>

  <span>@Override</span>
  <span>public</span> <span>boolean</span> <span>add</span><span>(</span><span>E</span> <span>e</span><span>)</span> <span>{</span>
    <span>addCount</span><span>++;</span>
    <span>return</span> <span>super</span><span>.</span><span>add</span><span>(</span><span>e</span><span>);</span>
  <span>}</span>

  <span>@Override</span>
  <span>public</span> <span>boolean</span> <span>addAll</span><span>(</span><span>Collection</span><span><?</span> <span>extends</span> <span>E</span><span>></span> <span>c</span><span>)</span> <span>{</span>
    <span>addCount</span> <span>+=</span> <span>c</span><span>.</span><span>size</span><span>();</span>
    <span>return</span> <span>super</span><span>.</span><span>addAll</span><span>(</span><span>c</span><span>);</span>
  <span>}</span>
<span>}</span>
@NoArgsConstructor public class InstrumentedHashSet<E> extends HashSet<E> { @Getter private int addCount = 0; public InstrumentedHashSet(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); } }

Enter fullscreen mode Exit fullscreen mode

This implementation, although it looks reasonable, has an issue that you won’t discover until you realize how the implementation of HashSet works. Under the hood addAll simply calls add to insert elements so when you perform the call myCoolInstrumentedHashSet.addAll(List.of("a","b","c")) you end up with an addCount of 6 not 3 like you would expect. Three get added in the addAll call and 3 get added in the add call. There are ways to “fix” this issue, like removing the code in the addAll call, but that is still making it dependent on the implementation of the class. While it works today, will it work tomorrow?

In addition to the coupling discussed above, there are other issues with using inheritance when it comes to fragility. As time goes on various things can happen to the super class. It can inherit new abilities with new methods, potentially conflicting with the names of methods in your subclass, it can expose internal state that you were depending on controlling the invariants of, and various other issues.

There must be a better way!? Well there is. Enter composition. What in the world is composition? Well simply put, instead of extending a class’s functionality a class simply has an instance of that class as an internal member and it delegates behaviors to it. This changes the inheritance is-a relationship to a has-a relationship. Let’s take a look at what this could look like with our example from above:

<span>public</span> <span>class</span> <span>InstrumentedSet</span><span><</span><span>E</span><span>></span> <span>extends</span> <span>ForwardingSet</span><span><</span><span>E</span><span>></span> <span>{</span>
<span>@Getter</span>
<span>private</span> <span>int</span> <span>addCount</span><span>;</span>
<span>public</span> <span>InstrumentedSet</span><span>(</span><span>Set</span><span><</span><span>E</span><span>></span> <span>wrappedSet</span><span>)</span> <span>{</span>
<span>super</span><span>(</span><span>wrappedSet</span><span>);</span>
<span>}</span>
<span>@Override</span>
<span>public</span> <span>boolean</span> <span>add</span><span>(</span><span>E</span> <span>newElement</span><span>)</span> <span>{</span>
<span>addCount</span><span>++;</span>
<span>super</span><span>.</span><span>add</span><span>(</span><span>newElement</span><span>);</span>
<span>}</span>
<span>@Override</span>
<span>public</span> <span>boolean</span> <span>addAll</span><span>(</span><span>Collection</span><span><?</span> <span>extends</span> <span>E</span><span>></span> <span>newElements</span><span>)</span> <span>{</span>
<span>addCount</span> <span>+=</span> <span>newElements</span><span>.</span><span>size</span><span>();</span>
<span>return</span> <span>super</span><span>.</span><span>addAll</span><span>(</span><span>newElements</span><span>);</span>
<span>}</span>
<span>}</span>
<span>@RequiredArgsConstructor</span>
<span>public</span> <span>class</span> <span>ForwardingSet</span><span><</span><span>E</span><span>></span> <span>implements</span> <span>Set</span><span><</span><span>E</span><span>></span> <span>{</span>
<span>private</span> <span>final</span> <span>Set</span><span><</span><span>E</span><span>></span> <span>set</span><span>;</span>
<span>public</span> <span>void</span> <span>clear</span><span>()</span> <span>{</span>
<span>set</span><span>.</span><span>clear</span><span>();</span>
<span>}</span>
<span>public</span> <span>boolean</span> <span>contains</span><span>(</span><span>Object</span> <span>o</span><span>)</span> <span>{</span>
<span>return</span> <span>set</span><span>.</span><span>contains</span><span>(</span><span>o</span><span>);</span>
<span>}</span>
<span>public</span> <span>boolean</span> <span>isEmpty</span><span>()</span> <span>{</span>
<span>return</span> <span>set</span><span>.</span><span>isEmpty</span><span>();</span>
<span>}</span>
<span>public</span> <span>int</span> <span>size</span><span>()</span> <span>{</span>
<span>return</span> <span>set</span><span>.</span><span>size</span><span>();</span>
<span>}</span>
<span>...</span> <span>repeated</span> <span>for</span> <span>every</span> <span>method</span> <span>in</span> <span>the</span> <span>Set</span> <span>interface</span><span>.</span>
<span>}</span>
<span>public</span> <span>class</span> <span>InstrumentedSet</span><span><</span><span>E</span><span>></span> <span>extends</span> <span>ForwardingSet</span><span><</span><span>E</span><span>></span> <span>{</span>
  <span>@Getter</span>
  <span>private</span> <span>int</span> <span>addCount</span><span>;</span>

  <span>public</span> <span>InstrumentedSet</span><span>(</span><span>Set</span><span><</span><span>E</span><span>></span> <span>wrappedSet</span><span>)</span> <span>{</span>
    <span>super</span><span>(</span><span>wrappedSet</span><span>);</span>
  <span>}</span>

  <span>@Override</span>
  <span>public</span> <span>boolean</span> <span>add</span><span>(</span><span>E</span> <span>newElement</span><span>)</span> <span>{</span>
    <span>addCount</span><span>++;</span>
    <span>super</span><span>.</span><span>add</span><span>(</span><span>newElement</span><span>);</span>
  <span>}</span>

  <span>@Override</span>
  <span>public</span> <span>boolean</span> <span>addAll</span><span>(</span><span>Collection</span><span><?</span> <span>extends</span> <span>E</span><span>></span> <span>newElements</span><span>)</span> <span>{</span>
    <span>addCount</span> <span>+=</span> <span>newElements</span><span>.</span><span>size</span><span>();</span>
    <span>return</span> <span>super</span><span>.</span><span>addAll</span><span>(</span><span>newElements</span><span>);</span>
  <span>}</span>
<span>}</span>

<span>@RequiredArgsConstructor</span>
<span>public</span> <span>class</span> <span>ForwardingSet</span><span><</span><span>E</span><span>></span> <span>implements</span> <span>Set</span><span><</span><span>E</span><span>></span> <span>{</span>
  <span>private</span> <span>final</span> <span>Set</span><span><</span><span>E</span><span>></span> <span>set</span><span>;</span>

  <span>public</span> <span>void</span> <span>clear</span><span>()</span> <span>{</span>
    <span>set</span><span>.</span><span>clear</span><span>();</span>
  <span>}</span>

  <span>public</span> <span>boolean</span> <span>contains</span><span>(</span><span>Object</span> <span>o</span><span>)</span> <span>{</span>
    <span>return</span> <span>set</span><span>.</span><span>contains</span><span>(</span><span>o</span><span>);</span>
  <span>}</span>

  <span>public</span> <span>boolean</span> <span>isEmpty</span><span>()</span> <span>{</span>
    <span>return</span> <span>set</span><span>.</span><span>isEmpty</span><span>();</span>
  <span>}</span>

  <span>public</span> <span>int</span> <span>size</span><span>()</span> <span>{</span>
    <span>return</span> <span>set</span><span>.</span><span>size</span><span>();</span>
  <span>}</span>

  <span>...</span> <span>repeated</span> <span>for</span> <span>every</span> <span>method</span> <span>in</span> <span>the</span> <span>Set</span> <span>interface</span><span>.</span>
<span>}</span>
public class InstrumentedSet<E> extends ForwardingSet<E> { @Getter private int addCount; public InstrumentedSet(Set<E> wrappedSet) { super(wrappedSet); } @Override public boolean add(E newElement) { addCount++; super.add(newElement); } @Override public boolean addAll(Collection<? extends E> newElements) { addCount += newElements.size(); return super.addAll(newElements); } } @RequiredArgsConstructor public class ForwardingSet<E> implements Set<E> { private final Set<E> set; public void clear() { set.clear(); } public boolean contains(Object o) { return set.contains(o); } public boolean isEmpty() { return set.isEmpty(); } public int size() { return set.size(); } ... repeated for every method in the Set interface. }

Enter fullscreen mode Exit fullscreen mode

OK what did we just see. We took our one class using inheritance and turned it into two classes without using inheritance but using composition. What did this gain us? It helps us have more robust code. We have isolated ourselves from changes in the individual concrete classes, there is no chance of something changing out from under us because we control the whole interface, etc. We also got some more flexibility. We now take in a Set of any type (not just HashSet) and can operate on any of them. We can even add the instrumentation after the Set has been initialized by some other piece of code.

The way we are using the wrapper class above is called the Decorator pattern. We are taking an already existing object and “decorating” it with additional behavior while still allowing it to be used as the original object.

So nothing is without it’s downsides, what are our downsides here? Well the main one should be pretty obvious. We took a pretty small class using inheritance and ended up with two classes and a lot more mind numbing code where we just duplicating an interface as we forward on calls. While this is a good chunk of code it’s not hard code to write. This is such a solid pattern that languages such as Kotlin build syntactic sugar around making this pattern easier to code up without so much code. You can also reuse these forwarding classes after you have written them. I have also always wondered if you could use Java proxies to generate these forwarding classes at runtime. Make that an exercise for the reader.

Inheritance truly does have a place. When you can truly say that WidgetB is-a WidgetA then an inheritance relationship can be appropriate. If WidgetB instead just needs to have the behavior of WidgetA then composition is likely what you are after. Honestly, if you can get away with composition it’s likely the safer bet. There is a lot of robustness and power that comes when you use this pattern and I hope you can recognize when this pattern could be useful to you as your continue along your development efforts.

Effective Java Review (90 Part Series)

1 Effective Java Tuesday! Let’s Consider Static Factory Methods
2 Effective Java Tuesday! The Builder Pattern!
86 more parts…
3 Effective Java Tuesday! Singletons!
4 Effective Java Tuesday! Utility Classes!
5 Effective Java Tuesday! Prefer Dependency Injection!
6 Effective Java Tuesday! Avoid Creating Unnecessary Objects!
7 Effective Java Tuesday! Don’t Leak Object References!
8 Effective Java Tuesday! Avoid Finalizers and Cleaners!
9 Effective Java Tuesday! Prefer try-with-resources
10 Effective Java Tuesday! Obey the `equals` contract
11 Effective Java Tuesday! Obey the `hashCode` contract
12 Effective Java Tuesday! Override `toString`
13 Effective Java Tuesday! Override `clone` judiciously
14 Effective Java Tuesday! Consider Implementing `Comparable`
15 Effective Java Tuesday! Minimize the Accessibility of Classes and Member
16 Effective Java Tuesday! In Public Classes, Use Accessors, Not Public Fields
17 Effective Java Tuesday! Minimize Mutability
18 Effective Java Tuesday! Favor Composition Over Inheritance
19 Effective Java Tuesday! Design and Document Classes for Inheritance or Else Prohibit It.
20 Effective Java Tuesday! Prefer Interfaces to Abstract Classes
21 Effective Java! Design Interfaces for Posterity
22 Effective Java! Use Interfaces Only to Define Types
23 Effective Java! Prefer Class Hierarchies to Tagged Classes
24 Effective Java! Favor Static Members Classes over Non-Static
25 Effective Java! Limit Source Files to a Single Top-Level Class
26 Effective Java! Don’t Use Raw Types
27 Effective Java! Eliminate Unchecked Warnings
28 Effective Java! Prefer Lists to Array
29 Effective Java! Favor Generic Types
30 Effective Java! Favor Generic Methods
31 Effective Java! Use Bounded Wildcards to Increase API Flexibility
32 Effective Java! Combine Generics and Varargs Judiciously
33 Effective Java! Consider Typesafe Heterogenous Containers
34 Effective Java! Use Enums Instead of int Constants
35 Effective Java! Use Instance Fields Instead of Ordinals
36 Effective Java! Use EnumSet Instead of Bit Fields
37 Effective Java! Use EnumMap instead of Ordinal Indexing
38 Effective Java! Emulate Extensible Enums With Interfaces.
39 Effective Java! Prefer Annotations to Naming Patterns
40 Effective Java! Consistently Use the Override Annotation
41 Effective Java! Use Marker Interfaces to Define Types
42 Effective Java! Prefer Lambdas to Anonymous Classes
43 Effective Java! Prefer Method References to Lambdas
44 Effective Java! Favor the Use of Standard Functional Interfaces
45 Effective Java! Use Stream Judiciously
46 Effective Java! Prefer Side-Effect-Free Functions in Streams
47 Effective Java! Prefer Collection To Stream as a Return Type
48 Effective Java! Use Caution When Making Streams Parallel
49 Effective Java! Check Parameters for Validity
50 Effective Java! Make Defensive Copies When Necessary
51 Effective Java! Design Method Signatures Carefully
52 Effective Java! Use Overloading Judiciously
53 Effective Java! Use Varargs Judiciously
54 Effective Java! Return Empty Collections or Arrays, Not Nulls
55 Effective Java! Return Optionals Judiciously
56 Effective Java: Write Doc Comments For All Exposed APIs
57 Effective Java: Minimize The Scope of Local Variables
58 Effective Java: Prefer for-each loops to traditional for loops
59 Effective Java: Know and Use the Libraries
60 Effective Java: Avoid Float and Double If Exact Answers Are Required
61 Effective Java: Prefer Primitive Types to Boxed Types
62 Effective Java: Avoid Strings When Other Types Are More Appropriate
63 Effective Java: Beware the Performance of String Concatenation
64 Effective Java: Refer to Objects By Their Interfaces
65 Effective Java: Prefer Interfaces To Reflection
66 Effective Java: Use Native Methods Judiciously
67 Effective Java: Optimize Judiciously
68 Effective Java: Adhere to Generally Accepted Naming Conventions
69 Effective Java: Use Exceptions for Only Exceptional Circumstances
70 Effective Java: Use Checked Exceptions for Recoverable Conditions
71 Effective Java: Avoid Unnecessary Use of Checked Exceptions
72 Effective Java: Favor The Use of Standard Exceptions
73 Effective Java: Throw Exceptions Appropriate To The Abstraction
74 Effective Java: Document All Exceptions Thrown By Each Method
75 Effective Java: Include Failure-Capture Information in Detail Messages
76 Effective Java: Strive for Failure Atomicity
77 Effective Java: Don’t Ignore Exceptions
78 Effective Java: Synchronize Access to Shared Mutable Data
79 Effective Java: Avoid Excessive Synchronization
80 Effective Java: Prefer Executors, Tasks, and Streams to Threads
81 Effective Java: Prefer Concurrency Utilities Over wait and notify
82 Effective Java: Document Thread Safety
83 Effective Java: Use Lazy Initialization Judiciously
84 Effective Java: Don’t Depend on the Thread Scheduler
85 Effective Java: Prefer Alternatives To Java Serialization
86 Effective Java: Implement Serializable With Great Caution
87 Effective Java: Consider Using a Custom Serialized Form
88 Effective Java: Write readObject Methods Defensively
89 Effective Java: For Instance Control, Prefer Enum types to readResolve
90 Effective Java: Consider Serialization Proxies Instead of Serialized Instances

原文链接:Effective Java Tuesday! Favor Composition Over Inheritance

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
When you procrastinate, you become a slave to yesterday.
拖延会让你成为昨天的奴隶
评论 抢沙发

请登录后发表评论

    暂无评论内容