Andy
Andy

Reputation: 8908

How to handle inheritance of Spring Beans in MVC projects

I have a project with many inter-dependent pieces:

myProject
  - core
  - core_web (depends on core)
  - app1_core (depends on core)
  - app1_web (depends on app1_core and core_web)
  - app2_core (depends on core)
  - app2_web (depends on app2_core and core_web)

I have the typical MVC pieces defined in core and core_web: services, controllers, repositories, JPA entities, etc.

The app1 project uses all this core stuff as-is--there are no additional pieces except for some specific configuration with application.properties that I will be able to work out with some @Configuration magic.

The app2 project, however, adds a Date field and a List field to an entity. The effect of the addition cascades everywhere: DTOs, factories, controllers, services, etc. become a muddle inheritance mess that just feels icky from a design perspective. Is there a better, DRY-er, more best-practice-y way of handling this, or am I just going to have to inherit nearly every class from my core code just to add a couple fields to an entity?

As a simple example (using Lombok here for brevity and awesomeness), here's an @Entity from core:

package com.example.core;

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ACCOUNT")
@ToString(of = {"id", "emailAddress", "name"})
@EqualsAndHashCode
public class Account implements Serializable {

    private static final long serialVersionUID = 1L;

    @Embedded
    private Address address;

    @Id
    @GenericGenerator(
            name = "SEQ_ACCOUNT_ID",
            strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
            parameters = {
                    @Parameter(name = "sequence_name", value = "SEQ_ACCOUNT_ID"),
                    @Parameter(name = "initial_value", value = "1"),
                    @Parameter(name = "increment_size", value = "1")
            }
    )
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ACCOUNT_ID")
    @Column(name = "ACCOUNT_ID", updatable = false, nullable = false, unique = true)
    private Long id;

    @Column(name = "EMAIL", nullable = false, length = 200, unique = true)
    private String emailAddress;

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = StringUtils.lowerCase(emailAddress);
    }

    @JsonIgnore
    @Column(name = "PASSWORD_HASH", nullable = false, length = 256)
    private String passwordHash;

    @Column(name = "NAME", nullable = false, length = 200)
    private String name;

    @Column(name = "PHONE", nullable = false, length = 30)
    private String phoneNumber;

    @Column(name = "URL")
    private String url;
}

Now I add the extra field in app2

package com.example.app2;

@Data
@NoArgsConstructor
@Table(name = "ACCOUNT")
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Bride extends Account {
    @Column(name = "WEDDING_DATE")
    private Date weddingDate;
}

I expect this amount of work. It's decent, elegant, doesn't duplicate much, and as long as I tell Spring not to scan both classes for entities, it's all good. Where it starts getting tedious is when I start adding service code, for example:

package com.example.core;

@Service
public class AccountService {

    private AccountRepository accountRepository;

    @Autowired
    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    /* the command object here would mirror Account, but contain some code
     * to handle input validation and the like. I know I could just use the
     * entity class, but I've been bitten by using entity classes as DTOs or
     * command objects before, so I'd like to keep them separate.
     */
    public Account createAccount(CreateAccountCommand createAccountCommand) {
        Account account = new Account();
        account.setEmailAddress(createAccountCommand.getEmailAddress());
        account.setPasswordHash(createPasswordHash(createAccountCommand.getPassword()));
        account.setName(createAccountCommand.getName());
        account.setPhoneNumber(createAccountCommand.getPhoneNumber());
        account.setUrl(createAccountCommand.getUrl());
    }
}

Now the main question: How do (read: should) I deal with the extra field in Bride? Create a new class called BrideService that extends AccountService and calls super.createAccount()? So then I need a command object that has that extra field in it now, too? And then the validator needs to deal with the extra field, so I inherit from the AccountValidator class? As you can see, I get this slippery slope of messy, inherited classes that only serve the purpose of adding one or two fields' logic to all the classes that should be able to handle them generically at some point. I mean, that's the purpose of polymorphism, right? So that I can use Account all over the place and the services, etc. "just work?" Is there somewhere in this tangled mess that the adapter pattern would work? Thanks in advance for any advice.

Upvotes: 1

Views: 340

Answers (1)

Derrops
Derrops

Reputation: 8117

Inheritance is a slippery slope. I would suggest you consider not extending Account, you are only adding one field after all, hardly worth creating so many more classes for just that. A Bride to me doesn't really feel like an Account, maybe a BrideAccount would be a better name. But can you just use composition instead:

@Entity
public class BrideAccount {

    @ID
    Integer id;

    @Column(name = "WEDDING_DATE")
    private Date weddingDate;

    Account account;
}

You won't have to then extend all these other controller etc. but can reuse them.

Other option is to make an interface of Account, and have all these controllers use the interface and extend only what you need to.

But I strongly suggest you don't add classes if you don't need to. If you could add a field to the Account class which is just that every account doesn't make use of, as in a super set class, that is preferable IMO. You just need to be really careful about what fields are being used and what not. Another field such as AccountType, maybe needed for this purpose of telling you what the type of account it is, then you would know if the wedding fields would be in use. This will be needed in the CreateAccountCommand as well. I answered a similar question here: Dynamically constructing composition object. In general I default to not creating more classes because I am biased from my experience of complex type hierarchies creating more problems than they solved.

Upvotes: 1

Related Questions