I am currently refactoring an application of mine I wrote back in 2015. Things were done I am not proud of, constant learing process and all that.
Previously everything was mashed together in one executable (database calls in forms code shudder), this should now of course be different. The app in question is a windows forms application talking to a MySQL database which is also used for some php pages. Think course organisation with giving members the ability to sign up and put their name forward for attending courses.
Now, by summarising what I have read so far, having different layers in your application helps with readability, testability and maintainability. I get that and generally like the approach of separating different concerns. The problem™ I am currently facing (and this might be just me overreacting and procrastinating) is, that having layers with clearly defined boundaries does not seem to fully disentangle needed references and requirements from one particular layer from other layers.
The tiers of my application are as thus:
- DataModel: This contains my domain model / all relevant (rich) data classes. Business logic lives here (e.g. what happens with an application when the course is already full, validation, ...)
- DAL: This defines interfaces to the persistence layer. Most notably one interface that includes the interfaces for all externally relevant repositories
- Concrete persistence implementation: All repositories are only exposed through their respective interfaces
- BusinessLayer (Application- or ServiceLayer would probably be more appropriate): Interaction between the outside world and my domain model lives here. I define "Handlers" to manipulate my data and trigger the persitstence mechanisms of it. I can throw exceptions if a validation fails before going on, so that a "consuming" application can interact with the user to get the data right.
- UI: Here everything sort of comes together. Different forms are presented to the user depending on what they want to do.
This seems (maybe only at a first glance) to be rather excessive, but I feel rather comfortable with the separation. BUT. Looking at which tier needs to include references to what, things start to seem a bit more tangled than I would have expected from "separating everything into different layers":
- DAL: depends on DataModel
- Concrete persistence: depends on DAL and DataModel
- BusinessLayer: depends on DataModel
and Concrete persistence (and thus on DAL)and DAL - UI: depends on BusinessLayer, DataModel, DAL (because interfaces and dependency injection) but also on a comncrete persistence implementation.
What bugs me with this is n-fold:
- DAL and concrete persistence would not usually use any of the "rich" functionality from my domain model classes (validation, special handling of added elements), they get the correct state of the data from the database. Also the UI should never be able (thing rogue programmer personality) to directly manipulate my data objects, changes should always run through my Business Layer. Would it make sense to define the properties of the classes (basically database fields now) only as interfaces in the "DataModel" and work with the interfaces throughout? The rich functionality can then be added in concrete implementations inside the Business Layer, which I can construct by passing the interface to a constructor. The downside would be that I would need to implement the interface in every part that actually uses it, so Business Layer, concrete persistence and UI.
- Speaking of "the UI should never be able to": Because of dependency injection (and other circumstances like reverting to a local cache-repository in case of a network outage) the UI as entrypoint to the application needs to define which concrete repository it wants/needs to use. So far so good. But now I have direct access to the repositories exposed in my UI Layer although I only need it there to pass it on to the Business Layer which is supposed to do all of the persistence triggering. I suppose there is no short and elegant way around it?
- I have no specific View Model and am not sure I need one. Short of the actual database IDs (and even they are needed to identify the objects being manipulated) almost all properties of my data classes are going to be displayed and potentially edited on the UI anyway, so it rarely makes sense to construct specific classes intended for a specific view. If I were to do so I would have no idea where to define them. If I define them in the UI, then my business logic needs to rely on the UI (which would constitute a circular reference which at least Visual Studio tends to dislike). If I define them externally (as interfaces even) I would start to swamp my Business Layer with presentation specific requirements. How is this normally done?
- Speaking of swamping the Business Layer with presentation specific requirements: How can one prevent that requirements for one layer spill over to other layers? I guess sometimes it is inevitable, but: On my UI I want to display comboBoxes to quickly select the person to be edited (the ones holding a course as well as the ones attending one). Because the comboBox does not need to show things like addresses but only title, first and last name (and provide the database id of the selected entity) I do not need to do a full
SELECT * FROM
and return very rich objects (yes yes small data compared to video streaming yadda yadda, but I think it pays off to be somewhat frugal with resources). Thus a requirement from the UI side was born "we need a dummy person entry". This of course trickled down to the business layer where my handler for each category of persons to be edited now has a function to retrieve such a list of dummy entries. Naturally this function needed to go one step deeper into my DAL and concrete implementation. And because these functions are supposed to return something, I now have a class for a dummy entry in my data model. This class has no business logic attached and its data is not persisted, it just sits there. Have I missed an obvious solution aside from "just read the whole list of full datasets and pass that to the UI and be done with it"? On a sidenote: Who is responsible for defining validation constraints like "This field should be 255 long at most"? I can't do it in the interfaces, because interfaces. If I do it in the concrete implementation in the Business Layer, how do I get the information across to the person writing a new persistence repository (except telling them and documenting it outside of the code)?
Argh... This got way longer that I thought, but at least it had the cathartic effect of allowing me to (hopefully) precisely describe my problems to the world an me and get my thoughts in order. I can try to fill this with actual code if necessary, but I think the underlying concepts do not really depend on code. Thank you for bearing with me and answers to my n buggings or general recommendations and thoughts are highly appreciated.