Apply Agile Design Patterns In Object Oriented Programing
Write Object-Oriented code (OO), not procedural code
When I look back at code I wrote many years ago, I recognize how badly I understood Object Oriented Design back then. Most of my code was blatantly procedural, with some classes here and there to structure the code into logical chunks like glorified modules. But I failed to apply Object-Oriented principles in a useful manner because I didn't understand it. It took several years before I truly started to see the benefits. If there's one advice I can give any developer, it is to try very hard to understand OO as early as possible.
I do blame some of the books and articles for this. A lot of examples are too convenient and use real-life objects (like cars, dogs or airplanes) to explain that everything can be considered in terms of objects and behaviour. They make a good point, but applying that knowledge is very difficult when you are dealing with very abstract business processes and business rules. A lot developers can identify obvious objects (like users, products, etc) but fail to capture processes like 'completing an electronic payment' or 'matching employees based on their preferences in a schedule' in terms of objects and behaviour. The result is a lot of procedural code, which can be easily identified by a large number of methods, large classes, many switch statements, deep hierarchies of if-then-else statements and the like, making the code very hard to read and understand.
So, why is good Object-Oriented code superior (in case you are still wondering)?
- More natural: Object-Oriented code expresses processes and business rules in terms of related objects and behaviour, making it easier to model your code in a language that is more natural than procedural code. This improves communication between developers and customers, although it takes some abstraction skills from all parties involved;
- Transference of knowledge: Object-Oriented code improves transference of domain knowledge. When applied correctly (following principles and patterns), other developers more easily understand your code.
- Inheritance: Object-Oriented code allows developers to group shared logic and expose it through a common interface throughout the application (encapsulation). Furthermore, other classes can derive from this class and extend or change that logic if it makes sense. Code re-use is minimized and maintainability maximized;
- Application of design patterns: Object-Oriented code allows the application of many well-known design patterns that will improve the maintainability of your code;
Object Oriented Programing (without the Hype)
Firstly, some background. A conventional ('procedural') program consists of a sequence of commands. The commands can do input, output, manipulate data and control the order in which the commands are carried out. So as not to have to duplicate commands in different places in a program where same action needs to be performed, a set of commands can be combined into a 'function' or 'subroutine' which acts like a new command. This same set of commands can then be called from different places in the program. As well as substantially reducing the length of programs, this can make the structure neater by encapsulating the commands which, together, perform some particular operation in one place. The rest of the program need not be concerned about the details of what commands make up the function and just treat it as something which does that operation. This neatness is not merely an aesthetic feature but a great aid to making programs quicker to write, easier to debug, more reliable & more reusable.
Splitting a program into functions makes programming quicker not only because different programmers may be able to work on separate functions at the same time but, crucially, it breaks down a huge task which would be difficult for a human to store in mind as one piece into smaller units more suited human memory. It makes debugging easier because it localises the effect of a single bug so it is easier to track down and, when bugs have been eliminated from a function, one need not waste time rechecking it when debugging other parts of the program. To aid this, it is normal to have the variables a function uses internally hidden from the rest of the program, unless there is a special necessity to reveal them. This ensures that any problem related to such a 'local' variable can be traced to function which needs correcting. This also makes the program more reliable because, once a function is working, it should stay working as more of the program is built up. Other parts of the program cannot disrupt those hidden variables & commands inside the function. The splitting also, of course, makes subsequent programs quicker to write as whole functions can be reused from earlier programs and even stored in libraries for use by any future program. The generic term for this is approach of breaking a big solution into small pieces whose internal workings are not of concern to other pieces is 'modular'.
Enough about functions. Now for variables. In many computer languages, a programmer can define a compound variable type in addition to those which are ready made in a language. For example if a particular language has variable types for names & dates, a type for storing birth records could be made by combining two name type variables, for the family name and given names, with a date type variable, for the date of birth. These combined variables are called different things in different languages including 'structures' (C), 'clusters' (LabView) & even just 'types' (FORTRAN). Combined variable types can be useful. For example, instead of having to work on and pass around three variables together whenever a birth record is used, a single variable of this combine type could be thus be used. Commands & functions which don't need to use all the member variables of a combined variable need not be concerned they are there. One can even copy a combined variable in one command, rather than one command for each member variable, without knowing or caring what all the member variables are. This is applying a modular approach to collections of variables as functions were for collections of commands.
That was around for decades then someone then came up with the idea of including functions as well as variables in those combined variable types. Combined variable types could then have member functions as well as member variables. These functions are only really in the program once (it would be very inefficient otherwise) because they are written into the variable type specification not the individual variables of that type themselves. However, they act as if they were duplicated in each variable of that combined variable type because they, by default, act on the member variables of the particular variable of the variable they called with. For example one could have included a member function to calculate a person's age into that birth record combined variable type. When a particular variable of that type, storing a particular given name, family name & date of birth, has its age calculating member function called, the function will automatically use the member variables of that particular variable, not the generic variable type, to calculate an age. This can be quite handy because it neatly bundles data & the functions which act on it together. It can also aid conceptualising a program because, in calling a member function, one is effectively telling the data what to do to itself which is, in some situations, closer to reality than giving the data to a command to process.
Now you have understood that, we can go onto Object Orientated Programming at last. Correction: if you have understood then you have understood Object Oriented Programming! That idea in the preceding paragraph of putting functions into combined variable types is Object Oriented Programming! Does it not sound dramatic enough? Okay, let’s put some hype in: rename 'member functions' to 'methods'; rename 'combined variable types' to 'classes'; and rename 'variables of combined types' to 'objects'. That's all it is!
A Few Little Extras
There are few useful extras which normally come with OOP. They can mostly exist in procedural programming languages as well so they are not necessarily OOP features but they are ubiquitous in OOP languages so I suppose I ought to mention them. You can skip this section if it is too detailed.
- Data hiding: Just like functions can have local variables not visible to the rest of the program, so can objects. This is for storing data that is not revealed or set directly but only via methods (member functions). For example a birth record object could store the year, month & day of birth in separate member variables and only combine them when asked for date of birth by a method call. Some OOP advocates even recommend that all member variables are hidden and only accessed via methods but that is sometimes excessive.
- Automatic initialisation: A class can have a method (called a 'constructor') which is automatically called whenever a new object of that class comes into being. This like having a default value for a variable type that a variable is initialized to when it is created prior to being set. However, as objects have member functions (methods) as well as member variables, this has been generalised into a method call that can do a lot more than just set a default value. There is a corresponding method (called a 'destructor') which is called when an object is disposed of and is typically used for clearing up.
- Same name, different function: Different functions can have the same name provided they are distinguished by their parameter types. This is useful in procedural languages but is almost vital in OOP languages because classes may come with functions that take objects of the class as parameters (as well as member functions embedded in the class) and it likely that there will be a duplication of popular function names between classes from different programmers. In OOP, this ability to have multiple functions with the same name is called 'overloading'.
- Derived classes: If one requires a class which like a class one already has but requires extra features then one can derive a class from an existing class. The derived class has all the externally visible methods & member variables the base class it was derived from had (and, optionally, some of the hidden ones) plus whatever extras are put in. For example one could derive a class from birth record class that also a member variable for time of birth or a method for combining the family & given name into a full name. Besides aiding reuse of program parts, it is possible to have different derived classes from the same base class for slightly different situations. For example British & Chinese versions of the previous example could be made where the British one calculates a full name by appending the family name to the given name whereas the Chinese one joins them the other way around. A nice feature of this is that objects of both types could then be stored and processed the same (thereby saving programming) and yet perform differently when the full name method is called. In OOP, this ability to have the same function perform different actions depending on the derived class is called 'polymorphism'.
Apply Agile Design Patterns Using OOPS Principles
There are many good books on this topic (see the references below), but applying Agile Design Patterns and Principles to your code will greatly improve maintainability. Here are the principles (and are pretty much required):
- Dependency Injection:Decouple dependencies. According to some authors the most important one, and I agree. I do see it more as a result of the principles below. Dependency Injection is the process of decoupling your classes. One class should not depend on the concrete implementation of another to work. The biggest advantage of decoupling your classes is that the testability if your code improves massively. In fact, unit testing is just not going to work without it! Coincidentally, dependency injection also forces you to think about the interfaces of your classes and how they are supposed to interact;
- The Interface Segregation Principle: Favour composition over inheritance. You're pretty much implicitly doing this when you are injecting dependencies, but composition is the process of extending and/or changing the behaviour of a class by changing the classes it contains or uses, instead of using inheritance (deriving a new class from another or changing the inheritance tree). Inheritance is very cool, but it can make your code very hard to understand when your inheritance tree becomes deep (>2 levels). It also makes your code brittle. How often have you bumped your head when you had to change many classes because you changed some parent class? When you use composition, you split the behaviour of a class into many small classes that you 'compose' together with (for example) dependency injection. So, one class will use many other small classes to achieve its goal;
The Interface Segregation Principle (ISP) is about business logic to client’s communication.
- Single Responsibility Principle:One class should have a single responsibility, and it should encapsulate that completely from other classes. What exactly constitutes 'one thing' is a matter of taste, but the size of the class is a good general guide. If your class starts growing in the number of methods, properties and lines of code, it's probably accumulating responsibilities. Some classes do many things. They get data from a database, process it and save it again. Through different methods of boolean parameters, they often allow multiple strategies to be executed. This is not a big problem if your class is very small (every method is like 5-25 lines and there are very few methods). But when your class grows, maintainability becomes harder and harder. Small classes, that have one (small) responsibility are easier to understand, easier to change and - most importantly - easier to test in isolation. This is where unit tests come in. Writing unit tests usually naturally pushes developers to write small classes, because they are easier to test. This is why writing unit tests is so useful;
The Single Responsibility Principle is about actors and high level architecture.
- The Open-Closed Principle:Classes should be open for extension, but closed for modification. When you write a class, you should write it in such a manner that you can change the behavior of the code without rewriting the original code. You should be able to extend from the original class and override behaviors or (preferably) use composition to change the behavior of the class. The point of doing this, is that you can guarantee that the original code still works. You won't break anything. Instead, you are extending the behavior of a class through other means (inheritance, composition, overriding). This is also where many design patterns excel, like the Template Method or Strategy It's easy to get lost in very complicated code designs when you are trying to protect (and abstract) your code against any kind of change. Instead, Martin (2002) argues that developers should take a more optimistic approach and protect against change only when it's very obvious. If other parts of the code change in the future, the initial adaptation will be costly. But the code should be rewritten to protect against further changes of that type. I like Martin's metaphor of guns and bullets; don't put on full body armour. It takes a lot of time to put on and slows you down. Take the first bullet. It will hurt, but put in the effort to put on body armour against that type of gun and make sure you won't be hit by it again. You might be hit by other guns in the future, but don't assume you will be;
The Open/Closed Principle is about class design and feature extensions.
- Liskov Substitution Principle: It should be possible to completely change the implementation of an object without the clients of that object knowing that the change has happened. You should be able to add, remove, or modify any or all of the fields in an object, replace all the non-public methods of the object, and completely replace all the implementations of the public methods. As long as the public interface hasn't changed, the users of the object shouldn't know or care that a change has happened. This principle extends to any sort of modularized system, not just OO classes and objects. Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
The Liskov Substitution Principle is about subtyping and inheritance. Child classes should never break the parent class' type definitions.
Some Other Non-Standard Principles
- DRY (Don’t Repeat Yourself) means don’t write duplicate code, instead use abstraction to abstract common things in one place. if you use a hardcoded value more than one time consider making it public final constant, if you have block of code in more than two place consider making it a separate method. Benefit of this SOLID design principle is in maintenance. It’s worth to note is don’t abuse it, duplicate is not for code but for functionality means if you used common code to validate OrderID and SSN it doesn’t mean they are same or they will remain same in future. By using common code for two different functionality or thing you closely couple them forever and when your OrderID changes its format, your SSN validation code will break. So be aware of such coupling and just don’t combine anything which uses similar code but are not related.
- Encapsulate What Varies: Only one thing is constant in software field and that is “Change”, so encapsulate the code you expect or suspect to be changed in future. Benefit of this OOPS Design principle is that It’s easy to test and maintain proper encapsulated code. If you are coding in C# then follow principle of making variable and methods private by default and increasing access step by step e.g. from private to protected and not public. Several of design pattern in C# uses Encapsulation, Factory design pattern is one example of Encapsulation which encapsulate object creation code and provides flexibility to introduce new product later with no impact on existing code.
- Programming for Interface not implementation: Always program for interface and not for implementation this will lead to flexible code which can work with any new implementation of interface. So use interface type on variables, return types of method or argument type of methods in Java.
- Delegation principle: Don’t do all stuff by yourself, delegate it to respective class. Classical example of delegation design principle is equals() and GetHashCode() method in c#. In order to compare two object for equality we ask class itself to do comparison instead of Client class doing that check. Benefit of this design principle is no duplication of code and pretty easy to modify behaviour.
Founder and Head at Croydon Tutorial College, Director of First Technology Transfer Ltd.
6 年Thank you for a well written and uncomplicated overview ... very helpful for a course module I am working on