سؤال

Had a discussion that i felt i needed some external input on. Normally always use interfaces instead of concrete types (on lists, injectable services and so forth). For example in lists i just pass around IEnumerable instead on List and so forth. The same applies for custom types (ICustomClass instead on CustomClass). This works fine for webapi, logging frameworks like serilog and so forth.

But i ran into some custom code which couldnt handle serializing interfaces due to implementation details. The argument for keeping concrete types instead of interfaces (and instead of adjusting serialization) was summarized to this post. And i can see if you work with XML documents there might be a need for concrete types to ensure serialization / deserialization if that is required (?).

EDIT: Realized i might have been unclear / vague in my description. I personally would never Create an interface for a DTO itself (like IDTO). Since i do not see any point to it. I likely will never replace it and i dont inject it for example. I WOULD however often resort to have that DTO contain an IEnumerable property in favor of a List or Type[] for example. I cant remember any time ive had an custom class represented by an interface since i ntry to always keep them simple. But perhaps there is a scenario i could raise regarding this where the same questioning might persit.

Soo what i would gladly get some input on:

Is there any good concrete technical arguments for avoiding interfaces in DTOs and responses (that might be serialized when returned through api or logdumping for example) in a normal REST API that outputs json?

هل كانت مفيدة؟

المحلول

In general, you want to accept as abstract a type as possible, and return as concrete a type as possible, but the situation you came across isn't really a question of abstract vs. concrete types. If you want the caller to be responsible for making sure an argument is serializable, that needs to be specified in the interface. That means requiring both IList and something like ISerializable.

Alternately, you could implement the serialization yourself using just the IList interface. However, not all ILists are serializable. How would you serialize an infinite list, for example?

That being said, the C# syntax for parameters that require multiple interfaces is inane. There's a YAGNI argument to be made that do you really need to support everything that implements IList? Is it worth the trade off in readability?

Design principles need to be balanced, so sometimes it's okay to give up a little abstraction in order to gain clarity.

نصائح أخرى

Just a few reasons to avoid interfaces


Use the Right Tool

An interface is a tool, when used in the right way it both simplifies, and enables greater scale of development.

When used poorly it increases complexity, and inhibits usage.

DTO

A Data-Type Object is not a Object as in the instance of a class - because software engineers love to overload meaning...

  1. Object a.k.a. Object Instance from Object Orientated programming is a modelling technique for capturing the state of a thing, and how that thing responds to messages given that state (a communication protocol).
  2. Object a.k.a. Memory Object is a region/s of Zero's and One's organised in a particular layout.

In the first case an interface can be used to substitute in different object instances that still support the same communication protocol. The quintessential example would be a Shape interface allowing those functions that desire to talk to a Shape to communicate equally well with a Square as with a Circle, or a Rhombus.

The second case is a description about how to perceive the bits at given locations. This includes expectations about what states those bits should be in, or how they should be modified. eg: Whatever the field x is byte 6 to 6 + valueof(byte 5). Field y at byte 16 can be seen as a signed two's-complement little-endian 64bit integer. However there is no implementation forcing these expectations to be true. Without an implementation, the meaning is literally in the eye of the beholder. Hence no communication is happening here, and hence there can be no interface.

Note: There is technically an interface around memory objects particularly when you reach down. Its just so permissive that anything goes, so it really does nothing as a communication protocol at this level of abstraction, and so there is no point in it other than making it look like an Object-Orientated object.

Why the confusion?

Just to be obvious, and this is quite likely why the term Object is so easily confused - a class fuses an interface to data with implementation.

  • The interface are the names and signatures of the accessable functions and properties along with how they change through interaction, providing the Object-Orientatedness.
  • The data are the private variables held inside the class these form a predictable memory object that the functions manipulate.
  • The implementation are the function bodies and property accessors that supply the Object-Orientated behaviour based on the state of the internal Memory Object and other communication.

Security

Always a good objection

Now object instances are not usually sent across a network.

To be sent across the network they would also have to carry their own interface, and implementation. Javascript is pretty much the iconic example here, and the plethora of articles online discussing why you should not eval(my_received_js_object) can give us a sense that sending objects, is bad idea...

But this is trivial, there is a much more central problem to serialising object instances.

How to Receive an instance of an Object Interface?

Here is the poser:

How do you send a message across time/space (be that a hardrive or a network) that will allow the recipient to precisely recreate the Object Instance (or at least a reasonable analogue) that the sent message represents?

The problem here is that an interface could have hundreds of different implementations. So which implementation do you mean?

If you were the serialisation framework receiving such a message you could solve this problem by:

  • Reflecting about the currently available implementations for that interface, and deducing the correct one.
  • Ask the program using the framework to indicate the appropriate implementation.
  • Provide an implementation for every knowable interface...
  • throw your hands up in the air and give up.

How to Send an instance of an Object Interface?

On the other foot, someone had to make the message. How would they have interrogated the interface in order to send it? As a horror story, consider trying to serialise interface IDispose { void dispose(); };

  • Reflecting on the type information about the object and hoping that accessing all of the properties is enough to store the object.
  • Ask the program to label the correct properties/methods to obtain data from.
  • Ask the program to hand craft a message for you to send on its behalf.
  • Have a mechanism for cracking into the implementation behind the interface (completely braking encapsulation)
  • send the entire object, including implementation and state, wait... that's insecure.
  • give up and write c++ template meta-programs instead...

The Alternative?

Dictate the exact memory objects that your serialisation framework will send/receive. Then provide support to the sender/recipient to handle the mapping of their domain from and to those memory objects.

It doesn't really mater which serialisation framework you use, they all fundamentally work this way. They have a few core data objects that they can trivially serialise/deserialise.

These core representations are obtained from the object instances through some form of mapping. Sometimes the mapping is trivially automated (just grab all the properties), but the more Object-Orientated your implementation behaves, the more you will need to provide special directives like:

  • don't touch this property,
  • call this function to ask for my serialisation,
  • use this constructor
  • use the default constructor and call this method.

At the recipient those core models are generated in terms of function calls, or some form of DOM (Document Object Model). Which are then mapped either automatically, or through special directives/handlers back into Object instances that the recipient can work with.

Architecturally

An Object instance is a self-contained unit of state, state that is only known to that object, and completely managed by that object. Which means that any serialisation/deserialisation is also handled by that object's implementation - no exceptions.

Of course the object implementation could leverage the serialisation framework, but that does impose coupling between what is probably a domain entity and the framework. That is never a good sign.

Keeping the code sound and clean would then mean that the Object instances return Memory Objects which are then trivially serialised by the framework. This has low coupling, ensuring that the framework can be easily changed.

Normally always use interfaces instead of concrete types.

Every time you add an interface, you add another type and you create a level of abstraction, which complicates the code. Since there are costs to interfaces, there needs to be good reasons for using them too. So "normally always use interfaces instead of concrete types" is not a good approach. Use interfaces when they add benefit.

For example, you might have a DTO:

public class DTO
{
    public string Name;
    public string Address;
}

It's publicly mutable, and this may be fine. It's simple, so if it does the job, then all is well. But you might want it to be immutable, so you make it so:

public class DTO
{
    public DTO(string name, string address) => (Name, Address) = (name, address);

    public string Name { get; }
    public string Address { get; }
}

But now you have a problem: many serialization tools can't handle such a mixture of constructor and read-only properties. So you can deal with this with an interface that exposes the type as read-only publicly:

public interface IDTO
{
    string Name { get; }
    string Address { get; }
}

internal class DTO : IDTO
{
    public string Name { get; internal set; }
    public string Address { get; internal set; }
}

Now you can expose the dto via IDTO generally (it then being publicly read-only), and provide any serialization code with access to DTO directly so it can write to those properties.

But we've added a lot of complexity. You have to weigh up the benefits of it not being writable, publicly with that extra complexity. Only incur that complexity, and thus only use an interface, if the benefits are valuable enough to you.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى softwareengineering.stackexchange
scroll top