我们在只能使用默认构造函数时,是否应该优先选择组合而不是继承。

huangapple go评论113阅读模式
英文:

Should we prefer Composition over Inheritance when we can only use default constructor

问题

我了解组合优于继承的优势,但在某些情况下,类的实例是由框架使用默认构造函数创建的,我们既不能定义带参数的构造函数,也不能使用setter方法设置对象的属性。为了让这种情况更清楚,请考虑以下示例:

public class Main {

    public static void main(String... str){
        TargetFramework.component(Child.class);
    }
}

这里的TargetFramework获取一个class,并且它将在后台使用默认构造函数创建该类的实例。

假设我想要实现以下FrameworkInterface

public interface FrameworkInterface {

    void setup();

    void doAction(Record record);

    void doAnotherAction(Record record, boolean isValid);
}

现在,我可以通过继承和组合两种方式来实现这个接口:

方法1:(混合使用组合和继承)

public abstract class Parent implements FrameworkInterface {

    RecordValidator recordValidator;

    @Override
    public abstract void setup();

    @Override
    public void doAction(Record record){
        boolean isValid = recordValidator.validate(record);
        doAnotherAction(record, isValid);
    }
    
    @Override
    public void doAnotherAction(Record record, boolean isValid){

    }
}

在这个实现中,我决定使用组合,并在Parent类中定义了一个RecordValidator

这里的问题是,在创建这个类的实例时,我不能在Parent类中设置RecordValidator,因为这个类的实例是由框架使用默认构造函数创建的。但是我可以在继承Parent类的子类的setup方法中创建这个实例:

public class Child extends Parent {

    @Override
    public void setup() {
        recordValidator = new DefaultRecordValidator();
    }
}

FrameworkInterfacesetup方法会在通过默认构造函数创建实例后立即被调用,因此我们可以在其中初始化RecordValidator属性。这种方法将组合和继承混合在一起,因为我同时使用了组合和继承。然而,这种方法也有其优点,因为我将记录验证的关注点从父类的关注点中分离出来。

方法2:(只使用继承)

在这种方法中,我通过以下方式实现了FrameworkInterface

public abstract class Parent1 implements FrameworkInterface {

    @Override
    public void setup() {

    }

    @Override
    public void doAction(Record record) {
        boolean isValid = validate(record);
        doAnotherAction(record, isValid);
    }

    @Override
    public void doAnotherAction(Record record, boolean isValid) {

    }

    protected abstract boolean validate(Record record);
}

这样,我没有使用组合来定义RecordValidator,而是在Parent1类中定义了抽象的validate方法,以便子类可以使用它来实现验证行为。因此,Child类可以这样实现:

public class Child extends Parent1 {

    @Override
    protected boolean validate(Record record) {
        return false;
    }
}

我的问题是:

对于这种情况,哪种方法更好,它们各自的优缺点是什么?

英文:

I know the advantages of Composition over Inheritance but in some situation instances of the class are being created by framework using default constructor and we can not define constructor with parameter nor we can set attribute of the object using setter methods. To make this situation clear consider following example:

public class Main {

    public static void main(String... str){
        TargetFramework.component(Child.class);
    }
} 

Here the TargetFramework get a class and it will create instance of that class behind the scene using default Constructor.

Imagine I want to implement FramewrokInterface as below:

public interface FrameworkInterface {

    void setup();

    void doAction(Record record);

    void doAnotherAction(Record record, boolean isValid);
}

Now I can implement this interface in two ways considering Inheritance and Composition:

Approach 1: (Mixing and Matching Composition and Inheritance)

public abstract class Parent implements FrameworkInterface {

    RecordValidator recordValidator;

    @Override
    public abstract void setup();

    @Override
    public void doAction(Record record){
        boolean isValid = recordValidator.validate(record);
        doAnotherAction(record, isValid);
    }
    
    @Override
    public void doAnotherAction(Record record, boolean isValid){

    }
}

In this Implementation I decided to use composition and I've defined a RecordValidator as bellow:

public interface RecordValidator {
    boolean validate(Record record);
}

The problem here is that I can't set RecordValidator in Parent class when creating instance of this class because instances of this class are created by framework using default constructor but I can create this instance in setup method in child Class which extends parent class as below:

public class Child extends Parent {

    @Override
    public void setup() {
        recordValidator = new DefaultRecordValidator();
    }
}

The setup method of the FramworkInterface will be called just after instance created by default Constructor so we can use it to initialize our RecordValidator attribute; This is kind of Mixing and Matching Composition and Inheritance together to me because I'm using Composition with Inheritance together. However this approach has its own advantages because I've separated the Concern of validation of record from the Parent class Concerns.

Approach 2: (Just Inheritance)

In this approach I've implemented the FrameworkInterface in the following way:

public abstract class Parent1 implements FrameworkInterface {

    @Override
    public void setup() {

    }

    @Override
    public void doAction(Record record) {
        boolean isValid = validate(record);
        doAnotherAction(record, isValid);
    }

    @Override
    public void doAnotherAction(Record record, boolean isValid) {

    }

    protected abstract boolean validate(Record record);
}

This way instead of using composition and defining RecordValidator I've defined abstract validate method in my Parent1 class so that Child class can use it to implement validation behaviour, so the Child class can be implemented as follow:

public class Child extends Parent1 {

    @Override
    protected boolean validate(Record record) {
        return false;
    }
}

My question is:

Which approach is better for this situation and what are the pros and cons of them?

答案1

得分: 1

在这种情况下,哪种方法更好,它们的优缺点是什么?

我认为它们两者都不太理想,到了一定程度,我会寻找其他的解决方案。

从示范代码来看,例如在这两种情况下都没有可能模拟Child1的依赖关系。你可以通过实现仅用于测试的设置器或特殊构造函数来引入模拟能力。然而,我对这个设置的核心问题是你屈从于这个框架。

我建议探索其他可能性,例如,手动进行必要的依赖注入,然后“注册”一个完成的 bean 到框架中。这就是当 Uncle Bob 谈论与框架保持一定距离时的意思。

如果我们特别谈论 Java,并且这个框架不允许其他解决方案,例如事先创建 beans 并将其注册到框架中,我会联系框架的维护者,并要求实现 CDI 支持,因为这是一种处理依赖注入的标准方法。

看着你的示例,你采取了两种不同的方法,也就是重新定义了 Parent 的能力。就像在继承示例中对 Parent 所做的那样,你可以在 Parent 中定义 abstract boolean validate();,将实现委托给 Child。我甚至会更进一步,定义:

public interface class Parent extends FrameworkInterface, RecordValidator {
    ...
}

Parent 中的所有方法都是抽象的,或者可以看作是默认值,字段可以被移除)。因此,实现这个接口的每个类都会根据其自身情况实现这些方法。

英文:

Which approach is better for this situation and what are the pros and cons of them?

I would argue that both of them are suboptimal to a degree where I would look for other solutions.

Looking at the sample code, there is, for example, no possibility to mock the dependencies of Child1 in both situations. You could introduce mock capabilities by implementing setters or special constructors that are only used for testing. The core problem I have with this setup, however, is that you bow to the framework.

I would recommend exploring other possibilities, e.g. do the necessary dependency injection manually, then "register" a finished bean with the framework. This is what Uncle Bob means when he talks about keeping the framework at arm's length.

If we start talking about Java in particular and the framework does not allow any other solution to, e.g., create beans beforehand and registering them with the framework, I would contact the framework maintainers and ask to implement CDI support since this is a standardized way to handle Depencency Injection.

Looking at your example, you take two different approaches, i.e. you redefine the capabilites of Parent. Just as you did with Parent in the inheritance example, you could define abstract boolean validate(); in Parent, delegating the implementation to Child. I would even go a step further and define

public interface class Parent extends FrameworkInterface, RecordValidator {
    ...
}

(all methods in Parent are either abstract or can be seen as defaults, the field can be removed). Thus, each class implementing this interface implements the methods as it sees fit.

huangapple
  • 本文由 发表于 2020年8月16日 16:20:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/63434690.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定