Should Domain objects for a public API be able to save/send themselves? My main questions:  FileWithSave handles its saving itself, but delegates it to a repository object.  This allows implementation details to be available without being present in the API when saving (transpose this to a Mail sending API as well, available for send but not public).  Is this OK in terms of OOP practices?  Are there any maintainence gotchas I'm missing?  Does it need reorganization?

Without save:

package com.example.external;

public interface File {
    /**
     * Get the unique identifier.
     *
     * @return unique identifier.
     */
    String getId();

    /**
     * Get the file name.
     *
     * @return file name.
     */
    String getName();

    /**
     * Sets name for the file.
     *
     * @param name file name.
     */
    void setName(String name);

    /**
     * Get the file size.
     *
     * @return file size.
     */
    Long getSize();
}


Without save implementation:

package com.example.internal;

public class FileImpl implements File {
    // I don't want it in my public signature but I want it when I'm saving for some reason :(
    private Something somethingPrivate;
    private final String id;
    private final String name;
    private final Long size;

    public FileImpl(String id, Long size) {
        this.id = id;
        this.size = size;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getSize() {
        return size;
    }
}


Repository for File:

package com.example.external;

public interface FilingCabinet {
    /**
     * Get a file from the cabinet.
     *
     * @param id unique identifier.
     * @return file.
     */
    File get(String id);

    /**
     * Store a file in the cabinet.
     *
     * @param file.
     */
    void store(File file);
}


Without save client sample code:

FilingCabinet cabinet = room.getFilingCabinet();
File file = cabinet.get("my-unique-id");
file.setName("my-new-name");
cabinet.store(file);


With save:

package com.example.external;

public interface FileWithSave extends File {
    /**
     * Save the changes.
     */
    File store();
}


With save implementation:

package com.example.internal;

public class FileWithSaveImpl implements FileWithSave {
    private Something somethingPrivate;
    private final String id;
    private final String name;
    private final Long size;
    private final PrivateFilingCabinet repo;

    public FileWithSaveImpl(String id, Long size, PrivateFilingCabinet repo) {
        this.id = id;
        this.size = size;
        this.repo = repo;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public Long getSize() {
        return size;
    }

    @Override
    public void store() {
        repo.save(this);
    }
}


Wrapper class:

package com.example.external;

/**
 * Wraps PrivateFilingCabinet and injects PrivateFilingCabinet in FileWithSave object.
 */
public interface Secretary {
    /**
     * Get the file for someone.
     *
     * @param unique identifier.
     * @return file.
     */
    FileWithSave getFile(String id);
}


Repository injected in to FileWithSave and not part of the public API package:

package com.example.internal;

public interface PrivateFilingCabinet {
    /**
     * Get a file from the cabinet.
     *
     * @param id unique identifier.
     * @return file.
     */
    FileWithSaveImpl get(String id);

    /**
     * Store a file in the cabinet.
     *
     * @param file.
     */
    void store(FileWithSaveImpl file);
}


With save client sample code:

FileWithSave file = company.getSecretary().getFile("my-unique-id");
file.setName("my-new-name");
file.store();
This is a clear setup:


FilingCabinet cabinet = room.getFilingCabinet();
File file = cabinet.get("my-unique-id");
file.setName("my-new-name");
cabinet.store(file);



You can get files from the cabinet, modify the file,
and put the file back into the cabinet for storage.
The cabinet is in charge of storing the file.

This is an unclear, confusing setup:


FileWithSave file = company.getSecretary().getFile("my-unique-id");
file.setName("my-new-name");
file.store();



There is a company.
The company has a secretary.
You can get files from the secretary, modify the file,
and the file can store itself.

The second setup makes me wonder about several things:


Who's in charge of storing the file?
After I do .store(), does the secretary still have the same vision of the file as me?
What if I want to store the file somewhere else? For example, at a different company, or in a cabinet.


In the first setup I don't need to ask such questions,
as the answers are obvious.

Single responsibility principle

Another warning sign is that the second setup violates the single responsibility principle. The FileWithSave is not just a file anymore, it's also in charge of storage.
You might say, that's kind of the point, hence the name file-with-save.
Inheritance is not really about giving additional responsibilities to classes.
The purpose of inheritance is more about specialization.
The best sub-classes do the same things as their parents, just differently.
Giving a file the additional responsibility to save itself seems too much.
Even if this is not inherently a bad design,
you can replace with a better one (the first one), and so you should.

Bad practices

Use interface types instead of implementations.
This is especially crucial in interface definitions.
So instead of this:


public interface PrivateFilingCabinet {
    FileWithSaveImpl get(String id);
    void store(FileWithSaveImpl file);
}



It should have been:

public interface PrivateFilingCabinet {
    FileWithSave get(String id);
    void store(FileWithSave file);
}