Reputation: 8908
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
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