I return to this question every couple of years, so now i decided to solve it once and for all, by asking here.

So, the sequence:

  1. I'm writing a simple application that parses Json file (configuration to 3rd party app, further named as config) and allows user to do some editing with it. I've picked a platform, made some basic UI, binded some buttons to NewFileCommand, OpenFileCommand, SaveFileCommand, etc.
  2. Now I need to store somewhere the opened (and deserialized into a class) config, so I made a new StorageService class. It has some methods like OpenFile(), ReloadFile(), SaveFile(), etc. They are called from the ViewModel, StorageService opens file, read the config and store it as a field. When needed, it saves it to a file - so far so good.
  3. At this moment, I want to add some manipulations with the config itself. I really dont want to put it in the StorageService. Some methods like GetConfigPropertyByNameAndDoSomeMagicWithIt() are definitely should be placed to another service.
  4. So, i made another service, OperationsService. Now i want it it have an access to the config, stored in StorageService and add methods GetConfigPropertyByNameAndDoSomeMagicWithIt(). I dont want to expose the StorageService.config into the public API - because i already have all necessary accessors. For everyone else, except of OperationsService. Which i pretty much want to make the one, who can access StorageService.config. Looking like a good use case for old good C++ concept of the "friend" class, where OperationsService can be declared as a friend of StorageService and access its internals.
  5. I'm recalling that I've faced similar issue earlier, so I googled for "C# friendly class", opened the first link in a hope to find a modern solution - and realized that I've a deja vu feeling.

So, what is a flow in my current approach? How to solve it in "proper modern OOP architecture" manner?
Make OperationsService an internal class of StorageService? That would mean that all the code would be stored in the same file (pretty much violation of SOLID).
Make OperationsService an internal class of StorageService and make it partial and move to another file? Seems kinda creepy.

In general, I don't even want OperationsService to be exposed (not even to register it in IoC container) for the rest of the app - just to do the heavy lifting. OperationsService would be not that large, so I don't want to make a third (some kind of a wrapper service) as well.

In terms of objects I can rephrase the problem as "object A has a private field, which should be accessible only for object B. Object's A responsibility is IO, Object's B responsibility is some logic, so they cant be merged".

To sum up, we have:

  • config, which is POCO and contains deserialized data from Json.
  • StorageService, which contains OpenFile(), SaveFile(). Also, it contains current instance of the config.
  • theoretical OperationsService, which contains some logic in GetConfigPropertyByNameAndDoSomeMagicWithIt(). In order to run that logic, it has to have an access to the config as well. The question is how to relate OperationsService and StorageService.
  • 2
    The SOLID principles say nothing about physical distribution of code to files. – Doc Brown Aug 8 at 21:10
  • 1
    What is a "C# friendly class?" – Robert Harvey Aug 8 at 21:13
  • @DocBrown Well, i've been always referring S to be related to it: "single reason to change" kinda leads to that (at least for me). Kinda: if more than one class in a file - then more then one reason to change it. But technically you're right: its not about the physical disctribution. – Vitalii Vasylenko Aug 8 at 21:14
  • 3
    That is a friend class, not a "friendly" class. The moral equivalent in C# is probably a partial class. – Robert Harvey Aug 8 at 21:16
  • 1
    What's wrong with making a class that's explicitly and openly in charge of reading, storing, and editing the config? No need for 'friend' classes. – Eric King Aug 8 at 21:23
up vote 1 down vote accepted

One way of priveliging OperationsService over the rest of the system in regards to specific data is directly passing such data to it on a need to use basis: You could keep your config manipulation logic inside OperationsService and add a public AcceptOperations() method in StorageService that takes an OperationsService object as an arg and calls upon its relevant config manipulation logic, directly passing to it any privately held relevant config data.

Concretely, you could still have GetConfigPropertyByNameAndDoSomeMagicWithIt() be a member of OperationsService, that would call AcceptOperations() on the relevant StorageService instance and pass this as an arg. AcceptOperations() itself would call some concrete ManipulateConfig() method on the OperationsService instance given to it as a param, passing the internal config data to be manipulated as args for ManipulateConfig().

This proposal takes in part some resemblance to the classic OOP Visitor Pattern, excluding the Double Dispatch idiom but keeping the concept of restrictively defining who can visit what and definitely in keeping with the original idea behind the pattern which is to perform such separation of responsibilities such that "... [it provides] the ability to add new operations to existent object structures without modifying the structures." The whole idea of such a pattern is to aid follow the Open-closed principle which is one of SOLID principles, keeping of which you have mentioned to be a concern of yours to begin with.

  • This one is seems to be the most suitable approach in my case. The Visitor patters is exactly what i've been looking for. – Vitalii Vasylenko Aug 9 at 14:03

You can put the classes you want to be friendly with one another in a separate assembly. Then only declare the ones you want to be available from outside the assembly as public. The ones you want to hide from classes in other assemblies you declare internal. This should give you pretty much what you want.

From an OOP perspective this is better than friend since friend allows access beyond public members, which is inherently contra-OOP.

  • I got your point. Though, i'm not really up for having multiple assemblies just because of that :) Basically, i have a small StorageService, who can read and deserialize (pure IO) class, and it stores the result (so i can serialize and write it later). What i need is to expand that and add some logic (somewhere). – Vitalii Vasylenko Aug 8 at 22:10
  • Just some thoughts loud: The part stores the result of reading might be the key. Basically, if i store the result in just another service (CacheService?) and keep StorageService stateless - in that case i dont need OperationsService to have a privileged access to StorageService. – Vitalii Vasylenko Aug 8 at 22:10
  • Access-Control is not inherently a part of OOP, though it is a common feature. – Deduplicator Aug 8 at 22:13
  • In terms of objects i can rephrase the problem as "object A has a private field, which should be accessible only for object B. Object's A responsibility is IO, Object's B responsibility is some logic, so they cant be merged". – Vitalii Vasylenko Aug 8 at 22:17
  • 1
    OK, here's another approach. You can put your friend class within the class that is to use it, as a member, and declare it private (make it a nested class). I think this only works one way (the containing class can use the contained class) but that is probably OK. – Martin Maat Aug 8 at 22:35

Objects are supposed to handle their internal state and should be modifying themselves as necessary. I think you've extracted you objects incorrectly. Storage Service is really a persistence manager and it shouldn't be holding config values as internal values. Storage service should just manage the translation from file to object and vice versa. then you could either have a config object that has all the methods that do stuff to config values or expose them, or a config manager class that accepts a config object on creation and exposes the necessary access methods.

The reason you are having trouble is that Storage Service is currently breaking the single responsibility principle. It becomes more apparent when you are attempting to break it further with GetConfigPropertyByNameAndDoSomeMagicWithIt(). The proper solution in such a case would be to refactor to objects with better defined responsibilities. friend is a band-aid solution to a poor design.

  • 3
    Sorry but I have to disagree with you regarding "friend is a band-aid solution to a poor design." In the context of C++, it is a mainline and longstanding language feature. Specifically, there is enough basis to surmise that friend does not at all violate encapsulation. As such, I find it hard to accept plainly such a claim that it is a band-aid solution. Please see here: stackoverflow.com/questions/17434/… and mainly here in this well respected and authoritative resource: isocpp.org/wiki/faq/friends#friends-and-encap – SkepticalEmpiricist Aug 9 at 12:43
  • If i have config object that has all the methods to do stuff - that will break config's single responsibility (its just a POCO object, only stores the data). But i agree with point that StorageService has a state (the config itself) - i assumed that it might be the weak point. But if i follow your advice - that would make config data public. I want to keep it private, but available for OperationsService - and how exactly to do that is actually the core of my question. – Vitalii Vasylenko Aug 9 at 12:49

I think what you're missing is IConfigurationService a concrete version of which depends on IConfigStorageService. You could have LocalConfigStorageService, which picks up config from a file...or S3ConfigStorageService which retrieves from a cloud bucket. Or even DatabaseConfigStorageService which picks it up from a database. In all three cases the IConfigStorageService is responsible for retrieving a config and storing the update to the config for the IConfigurationService.

The IConfigurationService uses the returned config to respond to the consumers requests of it.

This specifically is an instance of the Bridge Pattern

  • Thanks for the comment. It absolutely makes sense for a bit larger apps. If\when app will grow, i'd definitely go that way - but for now its a bit early and unnecessary, because as of this moment, config comes only from the file and there are no plans to add DB or cloud. Good point, but, main question "where to store logic" is still unanswered :) – Vitalii Vasylenko Aug 10 at 8:34

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.