How do I control access between packages properly?
To control access between packages, assign the correct visibility modifier to each package element based on its intended audience. Public (+) allows any dependency, while private (-) restricts access to the containing package alone. Protected (#) and friendly (~) offer intermediate levels for sub-classes or sibling modules. Applying these rules prevents tight coupling and ensures modular integrity.
Understanding UML Package Visibility Modifiers
In UML, packages serve as logical containers for related diagrams, classes, or subsystems. Without strict access controls, a large model becomes a tangled web of dependencies.
Package access control is the mechanism used to enforce these boundaries. It dictates what external components can see or interact with internal elements.
There are four standard visibility markers used in package diagrams. Each marker acts as a filter, passing or blocking access requests.
The Public Visibility Modifier (+)
The plus sign (+) indicates that elements are globally accessible. Any package or class in the system can reference these elements.
Use this modifier when defining interfaces or core services that other modules must depend upon.
If a package is marked public, its contents are visible to all external packages without restriction.
This is the most permissive level and should be used sparingly to avoid creating a “god package” that depends on nothing.
The Private Visibility Modifier (-)
The minus sign (-) is the strictest level of package access control. It hides the contents from all external packages.
Elements marked private are only visible within the package itself or by elements defined inside that same package.
This is ideal for internal helper classes, utility methods, or configuration details that should remain implementation-specific.
External packages cannot even see the names of private elements, preventing accidental coupling.
The Protected Visibility Modifier (#)
The hash sign (#) offers a middle ground suitable for inheritance hierarchies. It is commonly used when package content extends beyond the package boundary.
Protected elements are visible to classes within the package and to subclasses of those classes, even if those subclasses exist in other packages.
This supports polymorphism while keeping the bulk of the implementation hidden from unrelated external clients.
It is essential when designing extensible frameworks where the core structure is shared but specific details are private.
The Friendly Visibility Modifier (~)
The tilde (~) denotes package-private access. This is a powerful tool for package access control in modular systems.
Elements marked as friendly are visible to any package within the same package set or context, but not to unrelated packages.
Use this for modules that act as a cohesive unit but should not expose their internals to the rest of the application.
This prevents external leakage while allowing internal components to interact freely.
Applying Visibility Rules to Manage Dependencies
Once you understand the modifiers, the next step is applying them to manage package dependencies effectively.
Dependencies represent the usage relationships between packages. Correct visibility ensures these dependencies remain loose.
Step 1: Define the Public Interface
Start by identifying which packages expose services to the rest of the system.
Mark the interfaces and entry points with the public (+) modifier.
All other internal elements within that package should default to private (-) or friendly (~).
This creates a clear contract where the public interface is stable, and internal changes do not impact dependents.
Step 2: Encapsulate Implementation Details
Mark all concrete implementations, helper classes, and internal logic as private (-).
This ensures that other packages cannot depend on implementation specifics, only on the public interface.
If a private class changes, external packages remain unaffected because they never see it.
This approach significantly reduces the cost of refactoring in large models.
Step 3: Handle Extension Points Carefully
Use the protected (#) modifier for elements intended for extension by external subclasses.
Ensure that protected methods do not expose sensitive internal state that should remain hidden.
Review the inheritance structure regularly to ensure protected elements are not overused.
Step 4: Enforce Module Boundaries
Group related packages into a cohesive module or subsystem.
Use the friendly (~) modifier to allow interactions within the module while blocking access from outside.
Set the root of the module to public only for specific entry points.
This creates strong boundaries that enforce modularity and separation of concerns.
Common Pitfalls in Package Access Control
Even with clear rules, developers often make mistakes that undermine the effectiveness of package access control.
These errors lead to tight coupling and fragile architectures that are difficult to maintain.
Overuse of Public Visibility
Marking everything as public is the most common error in package design.
This creates a “leaky” abstraction where internal implementation details are exposed to all consumers.
It makes it impossible to refactor internal logic without breaking external code.
Always default to private or friendly unless there is a clear need for public access.
Ignoring Protected Semantics
Developers sometimes confuse protected (#) with public (+) in package contexts.
Protected implies that subclasses can access members, but it does not grant access to unrelated packages.
Assuming protection implies broader access leads to unintended coupling across packages.
Always verify the inheritance chain to ensure protected elements are actually being accessed by subclasses.
Misusing Friendly Access
The friendly (~) modifier is often misunderstood or ignored in favor of public visibility.
If a package group relies on friendly access, external packages cannot interact with it.
This can lead to developers forcing public access to bypass the restriction, which defeats the purpose.
Ensure all developers understand that friendly access is a boundary, not a feature.
Advanced Strategies for Modularization
For very large systems, simple visibility modifiers may not be enough to manage complexity.
Advanced strategies involve combining package access control with architectural patterns.
Layering and Segregation
Organize packages into distinct layers such as UI, business logic, and data access.
Define strict visibility rules between layers to prevent vertical dependencies.
Use public interfaces at layer boundaries to enforce separation.
Restrict lower layers from accessing upper layer components directly.
Interface Segregation
Split large packages into smaller, focused packages with narrow public interfaces.
This reduces the surface area for potential dependencies and improves package access control.
Each package should expose only the functionality it needs to provide.
Minimizing the public API reduces the cognitive load on developers using the package.
Dependency Inversion
Apply dependency inversion principles to package dependencies.
High-level modules should depend on abstractions defined in public packages.
Low-level modules should implement these abstractions.
This ensures that package access control promotes flexibility and reduces coupling.
Verification and Maintenance
Once you have applied package access control, you must verify that it is working as intended.
Regular reviews and automated checks help maintain the integrity of your modular system.
Static Analysis and Linting
Use static analysis tools to detect violations of visibility rules.
Configure linters to flag public access where private is expected.
These tools can also identify unauthorized dependencies between packages.
Integrate these checks into your CI/CD pipeline for automated enforcement.
Code Reviews and Documentation
Include visibility checks in your code review process.
Ensure documentation clearly describes the visibility rules for each package.
Update diagrams regularly to reflect changes in access control.
Train new team members on the importance of package access control.
Impact Analysis
Before changing a visibility modifier, analyze its impact on dependent packages.
Check if any external packages rely on the modified element.
If dependencies exist, update them before or during the change.
This prevents breaking changes and ensures system stability.
Key Takeaways
- Use + for public interfaces that external packages must use.
- Use – for internal implementation details that should remain hidden.
- Use # for elements intended for inheritance by subclasses.
- Use ~ for package-level access within cohesive modules.
- Default to private or friendly to minimize coupling and protect internal logic.
- Regularly verify access rules using static analysis tools.