There may be things that just work well together. Chocolate and Peanut Butter may be a good example of this, if Reese's is to be believed. However, we all know that oil and water don't mix well. In our struggle to build large, complex applications application developers must work hard to find those things which do work well together and avoid those that don't. As development teams get larger it becomes harder and harder to keep a consistency of vision that keeps every developer focused on the same design approaches. However, it doesn't have to be that way.
Here are some examples of how design approaches can be incompatible and some suggestions on how to build a common design approach.
To inherit or not, that is the question
Object oriented development has a fundamental design approach that is inheritance. That is to say that I can say that I want all of the properties and behaviors of a base class but that I want to extend it to support my own methodology. Not every application framework supports object oriented inheritance. Sometimes because the underlying components don't support inheritance and other times because testing the inheritance scenarios would be too tall a challenge to accomplish.
This is not as awful as it might at first appear. It simply requires an encapsulation approach to integrating with the API. Using the encapsulation approach, the object that you're deriving functionality from is held inside the new class as a member variable and it properties and methods are exposed one-by-one through your new class. While this is tedious and doesn't allow new features from the base class to flow through, it is an effective way of approaching APIs which do not support inheritance.
On the converse side, some APIs require that the classes you create be subclasses of existing classes. This is because the infrastructure of the base classes is used in the infrastructure code of the solution. You must derive from the class to allow the infrastructure to manage your new classes. This is a way of design which is fundamentally consistent with the foundations of object orientation.
Neither of these approaches is wrong. However, the approaches are sufficiently inconsistent to the point that using one with an API that expects the other will be disastrous.
Relational databases and multi-value fields
Relational databases work by creating highly efficient ways to relate information in multiple tables. Each table contains a set of rows each with a set of fields. The indexing mechanisms used by a relational database engine creates a way to rapidly find a given record or set of records based on a value in one of the fields.
Relational databases handle situations where there may be zero to many entire rows of information related to the first row by placing it in a separate table and connecting a key value between the first table and the second table. For instance, a table of data may represent people. A second table may contain cars. Each person may own more than one car. In this case the rows in the car table may have a field which indicates their owner.
This relationship works well for all sorts of questions such as how many cars does a given person have, What is the most cars that a single person has, who owns a car, etc.
A different approach than embedding the person id associated with the car in the car table is to include a field in the person table which contains a list of the ids for car records. This field, called a multi-value field because it can contain multiple values, makes it possible to maintain a pristine car record without the reference to the person record but does so at some significant cost in terms of the kinds of questions that can be easily solved.
The questions about who owns a given car becomes difficult since there is no clear join key between the car and the person that can be queried on. It's technically possible to have the relational database engine do pattern matching in the people table's cars field. However, this will invariably result in a table scan — an operation that can not take advantage of indexing and one which is expensive from the stand point of resource utilization.
The relational database itself is not designed to handle multi-value fields well. As a result trying to use multi-value fields in a database will be a challenge. Relational databases and multi-valued fields are incompatible design approaches. If you intend to use multi-valued fields a different kind of database may be more appropriate than a traditional relational database.
Factories and enumerations
Another design case that is challenging is the implementation of an abstract factory model — which allows for any object to be created based on configuration. This is a completely flexible model that can be very powerful in terms of creating a solution for extensibility.
A common error in putting a factory together is to use an enumeration to control what object the factory actually creates. This is an error because it takes a very flexible solution which can be changed quickly to support new functionality and couples it to an approach which requires recompilation of the code. The result is that the amount of extensibility that you can have with a factory tied to an enumeration is limited. A better approach is to use a configuration based identifier for what item to create. By looking at the object type to create as being driven by configuration at run time rather than an enumeration at compile time you retain the flexibility of the factory design pattern.
The fundamental design philosophies are incompatible. A factory is about freely extensible. An enumeration is about the semi-rigid definition of values.
Programming as theory building
Peter Naur, most known for the language syntax notation "Backus-Naur Form" wrote an article titled "Programming as Theory Building" in 1985. The basic premise of the article is that programming is more akin to theory building that most people believe. Said another way, programming is the development of mental models of how the system should work. The more aligned the theories of different developers are the more consistency there will be in the application and the lower the overall cost of the application will be.
If Peter is fundamentally correct, then having incompatible or opposing designs within the same application makes it difficult to work with. If you must develop one theory of operation for one subsystem and then must radically and incompatibly change that theory for another subsystem you're dramatically increasing the apparent complexity of the system.
The goal should be to remove incompatibilities in the design in as much as possible and if the incompatibilities are not able to be resolved at least they should be clearly identified and documented so that it's apparent where it will become necessary to change the theory.
There are no easy answers to repairing incompatible design approaches. However, it's critically necessary if you intend to develop maintainable systems. Generally you have to do non-trivial work to resolve incompatibilities. However, despite the effort the solutions aren't generally all that difficult to see. Sometimes it means just moving one of the design approaches close enough to the other to be compatible.
For instance, if you have sealed classes that can not be derived from and there are other classes in the solution which must be derived from — you can take the time and effort to make sure that the sealed classes can be inherited from (i.e. unsealed).
Slightly more complicated is adding the necessary code to convert a multi-value field into a lookup or cross reference table. It means creating another table, and updating any of the code in the application to be able to process it, however, it would be possible to emulate the operation of a multi-value field through properties so that the entire system doesn't have to change â€" just those places which would benefit from a cross table.
Similarly, you may be able to replace the internal representation of an enumeration to an integer (the underlying type of an enumeration) and then cast the enumeration to an integer for those situations where you must know how to instantiate a specific class.
Creating consistency isn't always perfect, however, it creates an opportunity to move from conflicting design patterns to ones which are more compatible â€" ones that developers can understand and live with versus patterns that are so radically opposed that they do nothing but breed confusion.