-
Notifications
You must be signed in to change notification settings - Fork 2
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
Refactor to increase Separation of Concerns #1
base: master
Are you sure you want to change the base?
Conversation
I’m unsure about moving away from the global store - one of the primary drivers of the dispatcher architecture was handling cases where actions had broad-ranging effects on numerous components; the complexity of coordinating these via view model updates became intractable on larger applications. I can appreciate the idea of running the transaction from the View, I’ve certainly run into problems in the past nesting the transaction at too low a level, however I would not expect the View to “group multiple operations together” - this doesn’t strike me as a view responsibility. It may be that you’d end up railroaded into this approach if the broader actions are broken down to separate model updates, but it still seems like it should be elsewhere. Ultimately, this architecture seems to be pushing towards treating the data models as ViewModels - tightly coupled to the view/component model, using fine-grained notifications for updates. I think this a actually a very good approach for simple applications, but I suspect it will not scale to more complex apps, and TBH I don’t think we’d get away with calling it unidirectional. I definitely think its worth continuing to explore - would be great to have a recommended architecture that makes the most of Realm’s strengths, and could have a positive impact on the API. (Projections? 🙂) |
Is it necessary to have a global store if the methods are being added by extensions? Can extensions be separated out into multiple Swift files so only some of them are visible to any given part of application code? That would seem to narrow the information surface about the model but still keep the idea of having that code in something other than the View. |
In my experience it is exactly in complex apps that you need to be stringent about Separation of Concerns. For simple apps you can easily get away with global state and all views being tightly coupled to the Store, but as it grows you really need to isolate the individual components so that you can reason about their state individually.
Individual components should obviously have access to call whatever action they need, even if it means they need access to the full Store. But that should be a rarity. Most components only need to work with a subset of the full data model.
How can you make it more unidirectional than Actions (which can come from any source, Views included) affecting the Model and Views reacting on the results? |
You can add methods to the Model via extensions. Both on the Realm itself, for global methods that affect everything, and on individual classes (like the method added on |
The approach you’re describing tightly couples the view to the model, and heavily implies the component hierarchy will mirror the model hierarchy (or vice versa). My comment regarding “more complex apps” is referring to situations where this is not that case (i.e. views that don’t map easily to subsets of the data model) rather than a very large data model with clear, mechanical mappings to equivalent view components. Maybe it’s worth trying to build something more complicated so we can work out how it’s likely to fall out in practice.
The Actions here have been reduced to nothing more than mutating methods on model objects. I can’t reconcile this with my understanding of Unidirectional Data Flow, sorry. It’s essentially identical to the old MVC architecture we used in Cocoa predating KVO & Bindings - view (or scripting) action methods set model properties, property setters manually trigger notifications, notifications trigger view updates. |
That's what the private repos are for, so we can embarrass ourselves within the family ;-) |
The more I think about it, the more I suspect an MVVM approach is a better fit for Realm than full unidirectional. Building a ViewModel while sticking to the zero-copy philosophy is feasible, and should more cleanly deliver the desired separation of concerns. Consider something like:
An approach like this limits the scope of how much of the data model is available to the view and removes the need for global It definitely has some rough edges - e.g. notifications on calculated properties. However, I just thought I’d throw it out for further discussion. |
Yes, and that is pretty much the point. The original MVC architecture (from smalltalk) was a pure unidirectional dataflow architecture. It was Apple and Microsoft that later changed it to make the Controllers role a kind of middleware. In the original MVC pattern, Controllers could only send events to the Model. See this diagram from wikipedia: The current unidirectional dataflow craze is really about reinventing the original MVC model (this time with a functional touch). I know that the MVVM pattern is really popular, but I still think that putting those kinds of service objects in between is an anti-pattern for any kind of coherent design. Martin Fowler said it better than I: http://www.martinfowler.com/bliki/AnemicDomainModel.html
@cmelchior @jpsim or @bigfish24 might want to join in with their thoughts. |
Right, in the wikipedia diagram the ‘Controller’ is the equivalent of what we would call the ‘Store’ (or “Dispatcher’ if a separate one is employed) in Unidirectional terminology - it receives actions and is responsible for manipulating the model. What you’re describing appears to require collapsing the controller & view responsibilities into a single class, and then making it ‘unidirectional’ by only performing view responsibilities in one section of the class, and only performing controller responsibilities in another section. This is very hard to explain and enforce, and EXTREMELY easy to unintentionally get wrong. As I’ve mentioned previously, at that point the pattern is not much more than a (non-obvious) convention, and it would be difficult for someone new to the concepts to be able to distinguish it from every other Realm Cocoa app. I feel we may be talking at cross-purposes, and there might be better ways to address the issue. Is the main problem that the full (readable) realm is available to the view controller? There’s nothing in the design that requires this, we should be able to come up with an alternative. Regarding MVVM - the main driver of this approach is to align the view & model so data binding can be more easily employed. Obviously it’s better if your model already matches your view, but generally larger applications will require re-using the same model from multiple, quite different views, and the likelihood of a good match decreases. Certainly I wouldn’t recommend adding that layer unless you had to. |
The point is more that all business logic is the provenance of the Model (in the original MVC pattern, the Controller should not contain any logic, it should just relay user actions to the Model). A large part of the MVVM pattern is about pulling business logic out of the Model and into a service layer, which is what Fowler terms as an anti-pattern that goes against the basic idea of object-oriented programming. That does not mean that it is not sometimes a good idea to add additional (non-persistent) classes or methods to the Model to handle concerns that cross-over between many types (or is relevant for so few views that you don't want to clutter the core Model classes with it). But he does say that these should be rare and very thin, and would still be part of the Model. The key to the pattern is just that Views should only react on changes from the Model and not short-circuit it by updating directly from changes they do themselves. I agree that this is not enforced in any way, so it is really just a convention, but I would still say that it is a much better architecture as long as you follow it. To make it much more clear what goes on, and at least somewhat enforce the pattern, it should really be best practice to do all changes in async transactions (which would make it clear that you cannot react to it right away, but have to wait to receive the resulting change events).
The main point is that it should be easy to work from a component model, where each View only know about of the subset of the Model that is responsible for. I agree that you can still model Separation of Concerns with using a service layer, simply by splitting that layer into separate classes for each view/use-case. But I feel that it is just adding a lot of unneeded overhead. Our goal is to simplify app development. |
IMO it’s a stretch to label it as an architecture if there’s no actual structure supporting it. If a 'traditional' bidirectional architecture looks something like this:
Then the convention-based unidirectional flow looks like this:
Pretty much the only way of verifying one over the other is to audit every line of View code. If the goal is to separate the responsibilities of manipulating the model vs the responsibilities of updating the view, as a design principal you’d really want this reflected in your class responsibilities & collaborations.
It shouldn’t be relevant whether the updates are performed synchronously or not - personally, I think the existing design achieves this clarity better. The main rough edge, and I think your original objection, was the store dependency in the view component. What would you think of: protocol Dispatcher {
func doTheThing(foo: String)
}
extension Realm: Dispatcher {
...
}
// and then in the View
class View {
// inject these somehow
var dispatcher: Dispatcher!
var myObjects: Results<MyClass>!
func viewDidLoad() {
token = myObjects.addNotificationBlock(self.render)
}
@IBAction func buttonTapped() {
dispatcher.doTheThing(foo: "Thing")
}
}
I would argue that putting business logic in a ViewModel means you’re doing it wrong. Ultimately the ViewModel should just involve cleanly separating out the model-relevant parts of a view from the UI-relevant parts of the view, and it’s only necessary if there’s significant transformation/mapping required to get the model into a shape suitable for that screen. BTW I’ve built my fair share of anaemic domain models, and I believe the causality is opposite - service layers end up being created to house logic, rather than logic incorrectly being put in service layers because they’re there. I’m happy to expand on this if you’re interested, but I’ll warn you it’s mostly unsubstantiated opinion 😉.
I hate to say it, but building a (good) unidirectional data flow app using frameworks like UIKit and AppKit is most definitely NOT simple. If simplicity is the goal you’re much better off recommending ViewModels (or direct binding models to views for simple mappings). Otherwise you typically end up with enormous amounts of code laboriously diffing the current state of the view with the new state of the model (e.g. Adam’s RBQFRC code https://github.com/Roobiq/RBQFetchedResultsController/blob/master/RBQFetchedResultsController/Source/RBQFetchedResultsController.m). Ultimately this is why Facebook essentially wrapped the entirety of UIKit for React Native. |
This is really just to facilitate discussion, and not something that necessarily have to be merged.
While having a central store that all interactions go through have some benefits (and is how it is done in most uni-directional dataflow implementations), the way it is currently implemented makes it really hard to get any kind of Separation of Concerns.
Ideally each component (view) should only know about the subset of the Store that it is responsible for (which for the
ViewController
in this case is the list ofProject
s), and it should be able to work with them independently of which Store (if any) it comes from.The refactorings I have done covers a few things to facilitate this:
ViewController
obtains the list of Projects from the Store at creation, and from that point on it only interacts with the list, it never needs to reference the Store after that.Results
it is holding, so that it won't end up updating on unrelated changes to other classes.Things I would like to do better:
ViewController
obtains the list of Projects from a global Store object. Ideally, to have full Separation of Concerns and reusability, it should not have any dependency or knowledge of the Store at all, but that is pretty hard to do with Storyboards without getting into Segues and all.Any suggestions to how this could be done even better would be appreciated.