Click here to Skip to main content
15,867,453 members
Articles / OOP

10 Golden Rules Of Good OOP

Rate me:
Please Sign up or sign in to vote.
4.87/5 (111 votes)
8 May 2014CPOL11 min read 202.8K   169   41
The following guidelines are not exhaustive and are meant to be applied on top of the SOLID principles and proper use of OO Design Patterns.

Introduction

A good architecture means money saved in learning, maintaining, testing, fixing, extending and scaling source code. This requires more time and care for the initial development, but quickly returns the investment with great interests.

Inevitably, even the best designed architectures need some adjustment and refactoring in time.

Patches, modifications and last minute changes are part of the IT business, therefore, ‘quick and dirty’ solutions, when needed, should always try to confine themselves in an isolated place where it will be easy to refactor later on, and that will affect as minimum as possible all the other components.

The following guidelines are not exhaustive and are meant to be applied on top of the SOLID principles and proper use of OO Design Patterns.

1. Avoid Almighty Classes, Use Meaningful Names

When designing a new class or refactoring an existing one, a developer should make a list of all of the tasks performed by that class and come up with a name that easily and briefly represents what the class does.

If the class does too many things, a representative name is usually something monstrous like: PageInfoBuilderAndConfigurationLoaderAndLinkAnalizerAndCacheManager. That means the class has too many responsibilities and should be broken into multiple components, ideally, with one responsibility each.

When using OO Design Pattern, it is much easier to come up with meaningful names, since the design pattern itself frequently identifies the type of responsibility.

For example:

  • If the class creates objects that use the suffix Factory or Builder
  • If the class is responsible for coordination and communication between other business classes, then use the suffix Mediator or Façade
  • If the class is used to control the use of a resource class, then Proxy would be a good suffix
  • If a class is wrapping another in other to adjust its use for a consumer class, then use the Adapter suffix

2. Avoid Too Many If or Switch Statements

Excessive conditional logic makes the head of the developer spin like Regan in The Exorcist. Things become uglier when the same conditional logic is applied in many places in the application. It means that different business behaviors are managed with ifs and switches instead of being properly managed using inheritance or composition, separating the different behaviors into different implementations of a common abstract class or interface. For instance, there is a configuration variable that is telling if data should be stored in a database or in a file. Then everywhere in the code wherever we have to save something, we use the conditional logic:

VB.NET
If (ConfigParameters.SaveTo == StorageType.DataBase) 
           //Save something in the db
Else 
           //Save something to file

Imagine that we need to add a different behavior (for instance, save to a distributed cache): now we have to extend the conditional logic everywhere in the code. Imagine then, multiple configuration variables determining multiple behaviors and leading to a jungle of unmaintainable and intricate conditional statements.

The way to go, in the above example, should be:

  1. Create an abstract class or interface representing a Storage object (e.g., IStorage), with save and load methods.
  2. Create different implementations of the abstraction such as DBStorage, FileStorage, CacheStorage, etc.
  3. Create a factory that will instantiate the right storage implementation based on configuration and return it as abstraction (IStorage)
  4. When in need of saving data, just write: myStorage.SaveSomething(…) where myStorage is a IStorage variable created through the factory

Sure, it’s a lot of work but still much less work than maintaining conditional jungles. It also has one big advantage that is explained in the following guideline.

Developers that are abusing conditional logic are digging their own graves, and usually end up being overstressed.

3. Use Seams: Extending Is Better Than Changing

A seam is an area of source code where the behavior can be changed without editing the code. Seams leverage flexibility and they are great for one simple reason: They encourage extending the architecture instead of changing it.

Why is extending better than changing? Pretend that you are a new developer and you are asked to add new storage options to an application.

Would you prefer to go through the code and change it with the new options wherever something is saved? Finding your way around the code that someone else has written, that you do not fully understand yet, risking of breaking something or missing one of the hundred places that need to be changed?

Or would you rather create a new class that you will write from scratch that would implement the IStorage interface and be done without even looking at someone else’s code, risk free?

It is not a surprise if the degree of confidence of a developer is higher in this last case.

4. Avoiding Global State and Non-Deterministic Behaviors

COBOL developers in the ‘80s knew already that abusing global variables is a bad, bad practice. Global variables are rarely justified, and their damning side effects can lead to mental insanity. That wise old rule is still extremely valuable also in modern object-oriented languages, such as C#.

One of the many issues is that they create a global state of the application that compromises the deterministic behavior of functions/methods; in other words, calling the same function twice with the same parameters can give completely different results. Hence, the code is fragile, difficult to debug, and extremely difficult to run on multiple threads.

Use of any sort of global variables/objects is therefore highly discouraged.

Ideally a good architecture is stateless. Practically speaking though, almost every architecture needs some sort of state (e.g., a database, files, etc.).

The solution is state isolation and consists in two simple rules:

  1. Keep the life and scope of a state as short as possible (which means, for instance, that the class members should be encapsulated).
  2. Isolate the state by wrapping into a separate layer that presents itself through abstractions (base classes or interfaces).

For instance, a database can present data in the form of business entities through a well-defined interface of a Data Access Layer. The isolation will make life much easier when it comes to testing or troubleshooting a problem.

5. Avoid Static Classes for Helpers, Utilities & C.

Developers are frequently tempted to create static classes, especially when it comes to helpers, utilities and so on. The reason why a developer resorts to a static class is not much different from the reason a COBOL developer would use a global variable in his code: it gives immediate access to data from anywhere in the code, no effort needed.

Unfortunately, static classes act frequently as a global state and create the non-determinism that should be avoided. But it gets worse. Since static classes can be used everywhere in the code without being passed explicitly as parameters, they create secret dependencies that are not revealed by the API documentation. The code behavior then becomes less and less declarative and both the clarity and maintainability of the code drops dramatically. Static classes come with global variables and dependencies creating tight coupling in all the consumers (coupling is transitive).

Last, but not least, code using static classes is not testable in isolation, making unit testing a nightmare.

Unless strictly needed for performance reasons, the use of static classes should be avoided. Static variables are still OK for constant objects (although a static property without setter would be better) or to hold private references to objects inside factory classes.

6. Separate Creational Logic from Business Logic

One of main principles of a good architectural design is that the logic of creating an object and the business logic (what the object really does) are two different concerns that should be kept as far apart as possible.

The creation of objects is a concern that belongs to specialized classes, such as factories or builders. They should exclusively have the monopoly of creating objects.

The objects, on the other side, should only be concerned about performing business logic (ideally, a single business concern only) and not to worry about creating other objects (dependencies).

For instance, an object called HTMLAnalyzer (designed to analyze HTML links) needs a LinkAnalyzer in order to work. That means it depends upon the LinkAnalyzer class and therefore, an abstraction of LinkAnalyzer should be explicitly passed as parameter in the constructor of HTMLAnalyzer or as parameter of the methods that are using it. A developer may instead think of creating the LinkAnalyzer inside the HTMLAnalyzer using the new statement.

Double mistake:

  • First of all, the new statement creates a dependency to a specific type (no abstraction); therefore, the behavior is carved in stone and impossible to change without major refactoring.
  • To test HTMLAnalyzer in isolation is now impossible because we will have to test LinkAnalyzer at the same time.

The correct way is to use dependency injection to explicitly and declaratively inject all the needed dependencies when and where they are needed, for instance, in the constructor.

In this case, the object that uses a HTMLAnalyzer will invoke a factory to obtain an abstraction (interface or base class) of a LinkAnalyzer object and inject it into the HTMLAnalyzer constructor.

At any time, the behavior of LinkAnalyzer can be changed by creating alternative implementations and the only change required will be in the factory, not in the actual code of the business logic where changes are expensive and dangerous.

One simple implication of this is that the Singleton design pattern is intrinsically wrong and should never be used. It mixes in itself the creational logic (it creates itself) and the business logic, not to mention that it holds itself as static global object that it will never make it to the garbage collector (a singleton, like love, is forever) and constitutes a nasty global state of which we already spoke.

Factories are the long, simple, easy to control and boring pieces of code where all the changes are simple.

Business objects contain all the magic tricks and complexity, so the less we change here, the less we break.

Keeping them separated will lead to less business logic modification and more business logic extension.

7. Keep Constructors Out Of Business

As a corollary of the previous point, business logic should never be coded into constructors of an object. The purpose of constructors is only to assign some properties, initialize variables, perform simple parameter validations, and eventually hook up events. If constructors are actively doing something business-related, then, we will never be able to separate the creational logic from the business, and the architecture will always be messy.

8. Law of Demeter

This principle is mostly unknown among developers and, of course, one of the most useful. Without going into formal definitions, the principle states that a class should depend only and strictly upon what it is using; therefore, only what is really used should be injected into a class.

A great explanation that I have found in a Google TechTalk (http://www.youtube.com/watch?v=RlfLCWKxHJ0) is the following:

In an eCommerce system, each user has a Wallet object containing a collection of CreditCard objects and other financial information. The class responsible for the online payment has a method like this:

C#
bool Pay(Wallet wallet, string ccNumber, double amount) {
                CreditCard cCard = wallet.GetCreditCards().GetCard(ccNumber);
                return ProcessPayment(cCard, amount);
}

How would you consider a payment method like this? When you are paying for your new shirt at Macy’s, would you give your entire wallet full of credit cards and cash to the cashier and let him/her pick the right Credit Card or bills from it? Of course not. So if a payment class needs only a CreditCard object, a CreditCard shall be given--nothing more, nothing less. No need to create a dependency by passing a huge container object (the Wallet) full of unnecessary objects that creates tight coupling and security risks.

Container objects (usually named with vague suffixes like Container, Context, Service or ServiceLocator, Portal, Environment, etc.) should not be passed as parameters of constructors or methods (unless the whole content is needed).

9. Keep Complexity Level Low

The innermost level of nested loops, switches and ifs is the measure of how complex is a method. For instance, if inside a foreach loop inside another foreach loop is scored as a complexity of 3. The complexity of a method is the maximum complexity of its code and should never be above 4. If it is more than 4, then it is refactoring time!

Cyclomatic complexity is easily measurable, however it is not the only type of complexity to watch for.
If you want to know more about code complexity, I discuss the topic more in dept in the article The Rule Of Transparency.

10. No Long Methods

As a rule of thumb, every method should fit in a screen without vertical scrolling. If it does not, it is time to refactor. Long methods are often a sign of too many responsibilities and procedural programming.
Refactoring long methods will make a developer very appreciative about the absence of global variables.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
United States United States
Technical Director at AFS Technologies, New York, NY

More info about my skills, projects, presentations, etc.:
https://www.linkedin.com/in/giovanniscerra

Comments and Discussions

 
Question[My vote of 1] Avoid Static Classes for Helpers, Utilities & C. Pin
kiquenet.com5-Mar-18 1:46
professionalkiquenet.com5-Mar-18 1:46 
AnswerRe: [My vote of 1] Avoid Static Classes for Helpers, Utilities & C. Pin
Shahin Khorshidnia17-Jan-22 21:17
professionalShahin Khorshidnia17-Jan-22 21:17 
QuestionOOP Pin
Member 1084098013-Dec-14 5:34
Member 1084098013-Dec-14 5:34 
GeneralMy vote of 3 Pin
Member 1085901828-Oct-14 3:29
Member 1085901828-Oct-14 3:29 
GeneralRe: My vote of 3 Pin
Giovanni Scerra28-Oct-14 5:33
professionalGiovanni Scerra28-Oct-14 5:33 
GeneralMy vote of 5 Pin
jamicore16-Jul-14 12:03
jamicore16-Jul-14 12:03 
GeneralMy vote of 5 Pin
Prasad Khandekar3-Jul-14 20:58
professionalPrasad Khandekar3-Jul-14 20:58 
GeneralCtrl+D! Pin
csakii1-Jul-14 23:33
csakii1-Jul-14 23:33 
GeneralMy vote of 1 Pin
Steve Conlan1-Jul-14 4:08
Steve Conlan1-Jul-14 4:08 
GeneralRe: My vote of 1 Pin
Giovanni Scerra1-Jul-14 9:27
professionalGiovanni Scerra1-Jul-14 9:27 
GeneralRe: My vote of 1 Pin
Steve Conlan2-Jul-14 19:19
Steve Conlan2-Jul-14 19:19 
GeneralRe: My vote of 1 Pin
Giovanni Scerra3-Jul-14 0:53
professionalGiovanni Scerra3-Jul-14 0:53 
GeneralRe: My vote of 1 Pin
Shahin Khorshidnia17-Jan-22 21:15
professionalShahin Khorshidnia17-Jan-22 21:15 
QuestionStatic methods Pin
irneb1-Jul-14 4:01
irneb1-Jul-14 4:01 
AnswerRe: Static methods Pin
Giovanni Scerra1-Jul-14 9:22
professionalGiovanni Scerra1-Jul-14 9:22 
GeneralMy vote of 5 Pin
Renju Vinod16-Jun-14 0:40
professionalRenju Vinod16-Jun-14 0:40 
QuestionUse of static ... Pin
_Noctis_5-Jun-14 22:53
professional_Noctis_5-Jun-14 22:53 
AnswerRe: Use of static ... Pin
Giovanni Scerra7-Jun-14 3:43
professionalGiovanni Scerra7-Jun-14 3:43 
GeneralRe: Use of static ... Pin
_Noctis_7-Jun-14 3:47
professional_Noctis_7-Jun-14 3:47 
Question5 star indeed Pin
J. Wijaya3-Jun-14 0:42
J. Wijaya3-Jun-14 0:42 
Thank for summaries many best practices and approachs from a great resources.
Thirst for knowledge ...

Question5 starts ! Question about Avoiding Global State and Non-Deterministic Behaviors? Pin
Twiggy Ramirezz1-Jun-14 16:31
Twiggy Ramirezz1-Jun-14 16:31 
AnswerRe: 5 starts ! Question about Avoiding Global State and Non-Deterministic Behaviors? Pin
Giovanni Scerra2-Jun-14 1:37
professionalGiovanni Scerra2-Jun-14 1:37 
GeneralRe: 5 starts ! Question about Avoiding Global State and Non-Deterministic Behaviors? Pin
Twiggy Ramirezz3-Jun-14 4:07
Twiggy Ramirezz3-Jun-14 4:07 
GeneralStatic classes are testable in isolation Pin
markmnl20-May-14 19:31
markmnl20-May-14 19:31 
GeneralRe: Static classes are testable in isolation Pin
Giovanni Scerra21-May-14 2:00
professionalGiovanni Scerra21-May-14 2:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.