Encapsulating Collection Properties
Encapsulation is a key aspect of object-oriented programming and software engineering. Unfortunately, many systems fail to properly encapsulate collection properties, resulting in reduced quality.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
Encapsulation essentially means hiding the inner workings of something and exposing a limited public interface. It helps promote more modular code that is more reliable, since verifying the public interface's behavior provides a high degree of confidence that the object will interact properly with collaborators in a system. One area in which encapsulation often isn't properly followed is with collection properties.
Any time you have an object that has a collection of related or child objects, you may find this represented as a collection property. If you're using .NET and Entity Framework, this property is often referred to as a navigation property. Some client code can fetch the parent object from persistence, specify to EF that it should load the related entities, and then navigate from the parent object to its related objects by iterating over an exposed collection property. For example, a Customer object might have a set of Orders they've placed previously. This could be represented most simply by having a public List
Some developers might offer token resistance to this total lack of encapsulation by removing the setter (or making it private), but the damage is done as long as the property exposes a List data type, with all of its mutable functionality. This kind of design exposes too much functionality from the Customer, since it inherently allows any client code that works with a Customer to:
- Directly add or remove an order to/from the Customer
- Clear all orders from the Customer
In these cases, the Customer object in question has no way of controlling, preventing, or even detecting these changes to its Orders collection. Why is this important? Well, there is probably a decent amount of workflow involved in placing a new order for a customer. It's probably not sufficient to simply add a new order without any additional work. Now, you can argue that somewhere there's a service that does all the required work, but how does the object model enforce the use of said service? If any client code can instantiate an order and add it to a customer, how is the design of the system leading developers toward doing the right thing (using a service, in this case)? On the other hand, if there is no way to directly add an order to a customer, developers will probably quickly discover that there is a service for this purpose, and it's more likely that this service will provide the only way of adding new orders to customers.
In most cases, there are only certain operations on related collections that an object should expose, and these it probably wants to have direct control over. If Customer collaborators shouldn't be able to directly delete all of a customer's orders, don't expose the collection as a List. Instead, expose a ReadOnlyCollection