4

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.

Greg Burghardt
45.9k8 gold badges85 silver badges150 bronze badges
asked Aug 19, 2024 at 18:04
2
  • "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? Commented Aug 20, 2024 at 8:55
  • ... BTW, do you really use individual type aliases for each single property? Each one with the suffix "DTO"? Commented Aug 20, 2024 at 9:07

2 Answers 2

3

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:

  1. 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).

  2. Which elements does my programming language provide to make the interface decision explicit?

  3. If the public "interface" contains common methods, is there a way to make a common or generic implementation?

  4. 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 implementation

  • switch to a common base class (if necessary, in combination with an interface) when it turns out an interface is not sufficent and there might be a chance to place a common (default) implementation

  • add means like traits 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 an interface.

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.

answered Aug 20, 2024 at 9:40
2

I am going to answer generically rather giving specifics for a particular language.

Three concepts worth considering are:

  1. A contract - which states that a particular class supports a set of logically similar/grouped operations.
  2. Code reuse - often multiple classes need similar functionality and one doesn't want to repeatedly type the same code into multiple classes.
  3. 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.

answered Aug 20, 2024 at 8:26

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.