Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Managers for data carriers (such as Marshallable and records) #227

Open
minborg opened this issue Oct 17, 2020 · 2 comments
Open
Milestone

Comments

@minborg
Copy link
Contributor

minborg commented Oct 17, 2020

Problem Description

There are several drawbacks with the current solution where data carriers are responsible for their own management:

  • Loosely speaking, Marshallable resembles "active records" which have several drawbacks that are generally well-known within the community. Consequently, I will not elaborate more on that here.

  • Classes that are to benefit from SelfDescribingMarshallable must sacrifice its one and only superclass.

  • Classes that sub-classes SelfDescribingMarshallable will inherit and carry the API commitment of SelfDescribingMarshallable and because data carriers are often public, the total API surface will increase unnecessarily and sometimes dramatically.

  • Classes sub-classing SelfDescribingMarshallable will carry the following methods above its bona fide data carrier methods and therefore burdens the user when coding as shown hereunder:

image

  • Since Java 14, records can be used and are expected to replace and simplify the use of opaque data carriers.

  • Many organizations have existing data carrier classes they want to reuse without having to modify them.

  • The current implementation of SelfDescribingMarshallable relies on reflection requiring the class to be publicly open to deep reflection under JPMS.

  • The current solution does not allow the use of immutable data carriers. Immutable classes are robust, fail-fast, inherently thread-safe, and can benefit from a rich set of compiler optimizations. Configuration objects, for example, are best implemented as immutables.

  • The current solution does not allow different implementations of a class to be provided depending on the data carrier content. For example Point(0, 0) could not be implemented by OrigoPoint() in the current solution.

  • The handling of data carriers is not pluggable. Once a concrete implementation is picked, it will always be used regardless of capabilities potentially added later.

  • Methods invoked on the Marshallable typically require an initial "get" operation used to obtain, for example, a serializer, for the class in question. Many of these methods are often invoked billions if not trillions of times during the application's lifetime.

Proposed Solution

Therefore, I propose that we decouple the data carrier from the logic that creates and accesses the data carrier. Thus, creating single responsibilities for these types of classes:

  • Data Carrier: Carries data
  • Data Carrier Manager: Manages (i.e. Creates, Marshalls, and potentially accesses) a Data Carrier

Manager Example

Here is an outline of a Manager skeleton:

public interface Manager<T> {

    T fromFile(String filename) throws IOException;

    void writeMarshallable(T t, @NotNull WireOut wire);

    T readMarshallable(@NotNull WireIn wire);

    void readMarshallable(T using, @NotNull WireIn wire);

    <R> R getField(T target, String name, Class<T> tClass);

    void reset(T t);

    static <T> Manager<T> create(@NotNull final Class<T> clazz) {
        return new VanillaManager<>(clazz);
    }

}

Methods could be grouped by means of methods like marshaller() that exposes all the marshaling methods or by creating distinct sub-interfaces that hold related methods together (e.g. Marshaller<T>).

User code Examples

Legacy classes

// The class HyperBankDay is already defined in a separate existing library
Manager<HyperBankDay> dayManager = Manager.create(HyperBankDay.class);

// Read a Day from the provided wire
HyperBankDay lastDay = dayManager.readMarshallable(wire);

Modern Java

// Define the data carrier
record Day(long id, float opening, float last, float min, float max, float closing) { };

// Create a Manager
Manager<Day> dayManager = Manager.create(Day.class);

// Creates a new immutable Day from the provided wire
Day lastDay = dayManager.readMarshallable(wire);

Creating Managers

Manager implementations can be created in many ways. For example, Managers could be created using reflection (as shown above), using builders or they can be generated.

Migration

Once implemented, the end-user code could be migrated at that time, later or never. Thus, older code will continue to run unaffected.

@dpisklov
Copy link
Contributor

I suggest using more standard term DTO (data transfer object) as opposed to "data carrier" - it also then suggests a standard term for "Managers" - DAO (data access object).

@peter-lawrey
Copy link
Member

This is a major change, suitable for when we re-write for Java 17.

@peter-lawrey peter-lawrey added this to the Java 17 milestone Jun 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants