0

Not sure the title accurately represents my question, sorry about that.

I have three projects: Persistence, Core (logic) and Test, set up like so (some stuff omitted for brevity):

Persistence

public struct PatientData
{
    public int? ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public interface IPatientRepository : IDisposable
{
    IEnumerable<PatientData> GetPatients();
    PatientData GetPatientByID(int patientID);
    void InsertPatient(PatientData patient);
    void DeletePatient(int patientID);
    void UpdatePatient(PatientData patient);
    void Save();
}


class PatientRepositoryEF : IPatientRepository, IDisposable
{
  // assume EF implementation here
}

public static class Factory
{
    public static IPatientRepository GetPatientRepository() {
        // not ideal, will refactor later
        // assume EF for now
        return new PatientRepositoryEF();
    }
}

Core

public class Patient
{

    // CTORS/DTORS
    public Patient() {
        this.repository = Persistence.Core.Factory.GetPatientRepository();
    }

    public Patient(Persistence.Core.IPatientRepository repository) {
        // for testability, haven't actually used...
        this.repository = repository;
    }

    ~Patient() {
        if (repository != null) {
            repository.Dispose();
        }
    }

    // PERSISTENCE
    private Persistence.Core.IPatientRepository repository;

    public void Fill(int patientID) {
        Persistence.Core.PatientData data = repository.GetPatientByID(patientID);
        this.ID = data.ID;
        this.FirstName = data.FirstName;
        this.LastName = data.LastName;
    }

    public void Save() {
        repository.Save();
    }

    // other domain stuff

}

Test

    static void Main(string[] args) {

        Patient p = new Patient();

        p.Fill(1546);

        // test that data fills ok
    }

This all works fine, but I thought to dump that public Fill method and set up a public constructor to take an ID so consumers can either a) create a new/empty Patient, or b) pass an ID through the ctor to fill the model accordingly. Figure these changes:

add new CTor to Core.Patient and privatize fill():

    public Patient(int patientID) {
        this.repository = Persistence.Core.Factory.GetPatientRepository();
        fill(patientID);
    }

    void fill(int patientID) { /* fill method here */ }

change test project to this:

    static void Main(string[] args) {

        Patient p = new Patient(1546);

        // test that data fills ok
    }

Now, where the test project worked perfectly before (with p.Fill exposed), I can no longer compile the test project without a reference to the Persistence project (you must add a reference to assembly 'Persistence')

This isn't a major issue, I can work around it, but thought it'd be nice to bury that Fill() method. I'm guessing that this is has something to do with dependency visibility when objects are constructed, but on the other hand it runs the parameterless constructor from the test project just fine without the test project requiring the persistence reference.

I'm not clear on why this reference would be required with the only change being param vs. paramless construction. Can someone explain?

Edit: The exact error is:

The type 'Persistence.Core.IPatientRepository' is defined in an assembly that is not referenced. You must add a reference to assembly 'Persistence, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

If I replace Patient p = new Patient(1546); with Patient p = new Patient(), all is well.

Thanks

jleach
  • 7,410
  • 3
  • 33
  • 60
  • Is `fill(int patientID)` `public void fill(int patientID)` or void `fill(int patientID)` – Steven Wexler Nov 23 '15 at 19:20
  • It was public, I changed it to private and called from the constructor, which is when I can no longer compile the outer test project. – jleach Nov 23 '15 at 19:39
  • This is only a comment about the code, but I'm compelled to point you to [Why are mutable structs "evil"?](http://stackoverflow.com/q/441309/382780) to potentially save future headache. – 31eee384 Nov 23 '15 at 20:16
  • Thanks 31eee384, I hadn't fully settled on using a struct there - I've used them in the past for MVVM PropertyChanged backing and it worked well, but I hadn't seen this discussion. That said, I've been bitten by them before and have grown careful since :) – jleach Nov 23 '15 at 20:23
  • A couple of "code smell", at least to me: 1) a Patient has a reference to its PatientRepository. sounds bad. 2) The code in finalizer, disposing the repository reference. finalizer will run... when GC decide to call it, (or never, if you call `GC.SuppressFinalize` on that object). i.e, you don't know when it will be run. Sounds bad as well. What does _repository.Dispose() does? if it's "important", you should be in control of when it happens, if not... why call it in finalizer? – Gian Paolo Nov 23 '15 at 20:25
  • Wasn't quite sure about either of those Gian (still trying to get a handle on things, as you can see). The PatientRespository that the Patient sees is at least an interface and not a concrete class though... as for disposing, I picked up on that already, but hadn't refactored it quite yet. Noted, thanks. – jleach Nov 23 '15 at 20:28
  • @jdl134679, even if you have just an Interface reference, the question is "why a class should reference a repository containing instances of this class?". Don't know your code, but I usually consider a repository as a "in memory persistence" tool, and for SRP, a class shouldn't know its instances are stored in a repository (or in a db, or in a XML file, or whatever). Just my 2 cents. About finalizers, have a look at [Implementing a Dispose Method](https://msdn.microsoft.com/en-us/library/ms244737.aspx): it's the only case I had to use finalizer – Gian Paolo Nov 23 '15 at 20:38
  • @GianPaolo: I thought to have my application layer stuff reference the domain models directly, and edit, save, load them as required. Thus the general hierarchy of layers is Application->Domain/Logic->Persistence. You seem to be suggesting that I ought to be sourcing the Application's domain model instances from somewhere else? I'm certainly open to ideas if you happen to have a reference handy. – jleach Nov 23 '15 at 21:05
  • @jdl134679, I don't understand exacly what you say in last comment. I think the method Load and Save for a class should be method not of that class. Just note that right now, with your code above, calling Save on an instance of Patient just forward the call to the repository, and calling save on a second instance will Save the (same, ain't it?) repository. – Gian Paolo Nov 23 '15 at 21:24

1 Answers1

-1

EDIT:

The line this.repository = Persistence.Core.Factory.GetPatientRepository(); appears to be driving your error. You realize the namespace Persistence.Core is not the same as your project Core right? It sounds like you have a namespace and/or reference issue and you are just going to have to systematically track it down. Very hard to figure this one out remotely.


All project dependencies must be linked once and no more than once (else you get the dreaded circular dependency compile error). So in your case, Core project must have a formal reference to Persistence, and then Test must have a formal reference to Core (but not to Persistence). If Persistence must be directly utilized outside of Core, then you will need to either expose it via Core or think about refactoring your design.

The circular dependency issue can be frustrating for those trying to create the typical 3-tier architecture without doing the full work to truly isolate tiers 1 and 3. Two techniques that may help are event handler/callback approaches and refactoring with more interfaces. Both techniques are prone to requiring substantial refactoring, which quickly incentivizes taking long thinks in the design phase before one starts pushing keys.

Settling for a 2-tier architecture is not always terrible (depending on the specific context), but it is highly recommended you merge the Business Logic and Data Layer if you do go this route. The Presentation layer will almost always undergo more frequent and radical changes that your storage layer, so it makes sense to choose that layer as the isolated one.

Special Sauce
  • 5,338
  • 2
  • 27
  • 31
  • That is exactly my setup: Test -> Core -> Persistence. Test references *only* Core, not persistence (in the question, note that the error message states that it gives a compile error because there's *no* reference to persistence, not because there *is*). Besides which, the case you state wouldn't be a circular dependency anyway. There is no direct reference from Test to Persistence, hence my confusion. – jleach Nov 23 '15 at 20:13
  • And thank you, but no, this cannot be refactored into less layers. My question is specifically, why does it tell me I need the reference from Test to Persistence if I use a public ctor to load the repository from within Core, but not if I use a private method instead. – jleach Nov 23 '15 at 20:16
  • It sounds like you have a Visual Studio glitch from some project assemblies being out of sync. Try doing a "Clean Solution" and then "Rebuild Solution". If that doesn't work, try removing all the project references, do a clean, then re-reference all projects, and rebuild. – Special Sauce Nov 23 '15 at 20:19
  • No luck, still not letting me build the solution if I include the `Patient p = new Patient(12345);` in the Test project. Doesn't like anything but the parameterless constructor. – jleach Nov 23 '15 at 20:26
  • Can you please edit your question to supply the exact error (verbatim) you are getting? – Special Sauce Nov 23 '15 at 20:27
  • Thanks for the edit to the answer, but I'm still somewhat confused, for that line is the exact same in both the default as well as the parameterized constructor. If it works in a default parameterless constructor, why would it fail in the constructor that takes a patientID? Anyway, I might call it a fluke for tonight, maybe it'll click for me tomorrow. Thanks for the help. – jleach Nov 23 '15 at 21:01
  • No problem. Wish I had more for you. One trick you could try is to temporarily change your `Core` project from a Class Library (\*.dll) to a Console Application (\*.exe) project type. Just add the required Program.cs file with `static void main(string[] args)`. Then you can try testing this code and ctor without having to involve the `Test` project. If it still fails, the source of the problem will likely be more clear since `Test` has been removed from the equation. – Special Sauce Nov 23 '15 at 21:10
  • Good idea, will do (tomorrow!). Thanks – jleach Nov 23 '15 at 21:14