While developing my application I faced an interesting situation:
I have multiple DTO's which are almost identical in terms of methods, only properties are different. For example I have AccountDTO
which looks like that:
readonly class AccountDTO {
protected const KEY = "account";
public FirstNameDTO $firstName;
public LastNameDTO $lastName;
public BirthdayDTO $birthday;
public CompanyDTO $company;
public EmailDTO $email;
public CredentialsDTO $credentials;
public AgreementsDTO $agreements;
}
Also, for example, I have CredentialsDTO
which looks like that:
readonly class CredentialsDTO {
protected const KEY = "credentials";
public PhoneDTO $phone;
public OTPDTO $otp;
}
Both DTOs have the same methods:
public static function getKey(): ?string {
return static::KEY;
}
public function __construct(?array $dtos) {
foreach ($dtos as $key => $dto) {
$this->$key = $dto;
}
}
public function toArray() : ?array {
...
}
I made an AbstractNestedDTO
class which had these methods and extended both AccountDTO
and CredentialsDTO
with it, but it went out that readonly properties could be set only within their class, in my case within AbstractNestedDTO
which is obviously abstract and cannot be instantiated.
So I ended up with NestedDTOInterface
and NestedDTOTrait
.
After doing so I asked myself a question: when do I have to use interfaces, traits, abstract classes and their combinations? Are there any recommendations?
Thanks in advance for clarifying that question.
-
"when do I have to use interfaces, traits, abstract classes and their combinations" sounds extremely broad to me, which makes this question a candidate for closing it ("needs more focus"). However, I feel it should be possible to reword the question slightly to give it more focus on the specific case of DTOs with those typical kind of common methods - any suggestions?Doc Brown– Doc Brown2024年08月20日 08:55:48 +00:00Commented Aug 20, 2024 at 8:55
-
... BTW, do you really use individual type aliases for each single property? Each one with the suffix "DTO"?Doc Brown– Doc Brown2024年08月20日 09:07:42 +00:00Commented Aug 20, 2024 at 9:07
2 Answers 2
I think the easiest way to approach this is by splitting the process of making these design decisions into separate steps, first thinking about the "interface", then about the implementation:
How do I want the public "interface" of those DTO classes look like ("interface" in the broad sense, maybe supported by the language element
interface
, but not mandatory).Which elements does my programming language provide to make the interface decision explicit?
If the public "interface" contains common methods, is there a way to make a common or generic implementation?
Which elements does my programming language provide to implement these common methods generically?
In your specific case, you already know which common methods you want, and you know PHP provides either interfaces or common base classes (maybe abstract ones) for making the "interface" decision explicit. I would usually
start with an
interface
(in the PHP sense) as long as I don't know if there will be a common implementationswitch to a common base class (if necessary, in combination with an
interface
) when it turns out aninterface
is not sufficent and there might be a chance to place a common (default) implementationadd means like
trait
s for the implementation (or whatever my programming language provides as alternatives) when I hit the limits of a common base class (like the limits you noted, or when common base class collides with inheriting from other classes in a single-inheritance language like PHP). When it turns out I don't need a common base class with this "other means" anymore, I would probably switch back to aninterface
.
So this is almost the same what you already did - try the most simple approach first, and when you hit its limits, refactor and try a more complex one, but never more complex than required. Just don't try it the other way round, where you start with something unnecessary complex "just in case".
Note the above is just a rough guide for beginners, maybe a little bit oversimplified. In reality, I would also take thoughts about refactoring common code into separate classes into account, the "prefer composition-over-inheritance" guideline, the layering of the application, which code I really need a stable interface for (and which not), and a few more factors and options.
I am going to answer generically rather giving specifics for a particular language.
Three concepts worth considering are:
- A contract - which states that a particular class supports a set of logically similar/grouped operations.
- Code reuse - often multiple classes need similar functionality and one doesn't want to repeatedly type the same code into multiple classes.
- Inheritance hierarchies - one class is a specialization of something else.
Those concepts may map to Interfaces, Traits and class inheritance, however in many languages the lines become very blurry - for example, sometimes interfaces can provide default implementations. Additionally specific languages may add additional restrictions such as:
- Protection levels - in some languages all methods in an interface are public.
- Single Inheritance - some languages only provide single inheritance through class inheritance - this is to avoid problems of which method to call, when multiple parents provide a method with identical names / calling signatures.
Finally it should be noted that there are other options for code reuse such as:
- Utility classes.
- Composition.
- Code generation.
Typical you should consider the concepts above along with the typical coding standards for your language - when choosing which language features to use.
However, as a rule of thumb I would suggest.
It is generally safe to introduce contracts (simple interfaces) whenever you think there is a chance that you may need to switch out / have multiple implementations - in short it doesn't add a huge amount of extra maintenance overhead and typically, if it turns out that the code needs to be refactored down the road it's usually cheap to do so.
If you think it will clean up your code feel free to introduce a 2 level inheritance tree - a base (possibly abstract) class and a bunch of implementation classes. However pay attention to any friction as you continue to maintain your code. If you find yourself working around issues that the inheritance is causing you, considering refactoring to something else.
Finally think about code re-use and what technique is best to minimize it, however you should balance this against keeping your code maintainable, it should also be noted that a little bit or repetition is sometimes more maintainable than going overboard on trying to extract all "similar" code.
Explore related questions
See similar questions with these tags.