markb's profileWindows Live spacePhotosBlogListsMore ![]() | Help |
|
|
Application DesignIf the goal of the project is simple enough there is no need for a detailed design. You can just design along the lines of responsibilities, make use of framework services and tack some ‘smart’ user interface onto whatever domain model you come up with. By smart user interface I mean that it's OK to have business rules in the UI or that commands are embedded in with the view management code. More complex enterprise applications start out with layered architectures. Typically there are data services, entity frameworks and other services at the bottom layer(s). A Domain Model in the middle, sometimes a thin abstraction layer for making the domain model consumable to the higher layers called a Service Gateway, and the application and presentation layers at the top. Usually there is no difference between the presentation and the view of the presentation. Ignoring framework services like entity mappers , data repositories and Inversion Of Control containers, most of the complexity is in the Domain Model and the Presentation Surface. Framework services if done right can be plugged in and possibly come from outside sources. You can use a facade together with adapters as long as the foreign code isn't so gross that it can't be contained. This is code that once it is implemented correctly and maintained can be reused. The major design problem(s) are usually centered in the Domain Model (Domain Driven Design), the Presentation Surface(s) and how the various Views of the application are attached to those surfaces. The typical object oriented design is from the bottom up. After some decomposition of the problem space developers go about coding the individual classes. There is sort of an article of faith that is common amongst code centric developers where the thinking is along the lines of ; "if the classes at the bottom have the methods and properties that model what I could possibly need from this class then I can wire the various objects together and make the code work". Bottom up design is the wrong approach in a complex application and leads to applications that are not extensible and difficult to maintain. This is because bottom up coupling is almost always too tight and for the most part there is a noticeable lack of design patterns in bottom up code. Think about building architecture, ugly buildings are built from pieces that are fit to make a whole. Granted you do get a building by just fitting the pieces together. However there is no architectural flair or theme in buildings like that. They are just functional, not really architectural. For simple applications that don’t have a high need for change and extensibility this is fine. Think of modular housing there is nothing wrong with it under the right circumstances. But if your are going to go into the future with something that you want to be of very high quality because you are going to put your identity on the line as a business - you don't want to design from the bottom up. In a complex application there are going to be patterns that emerge that are important for various reasons. These patterns become apparent during the analysis phase. There are the "ilities", extensibility, testability, flexibility, and aspects such as branding the user interface, variability in business rules that have to be plugged into a wide range of scenarios, the addition of new entities from the data services layer, etc.. When these requirements are stated up front the goal is to identify the key architectural patterns that would support them and to prove small pieces of the design in some type of adaptive coding process. It's adaptive in the sense that it's very tough to design all the way down in a predictive fashion with software. It's not like a building in that sense. There are going to be ambiguities and some design insights come with small iterative attempts at realizing design goals. The patterns that are identified as enabling your architecture are referred to as senior most design contexts. They are the theme of the architecture; the most important patterns from which all of the other pieces play a supporting role. This is just the opposite of bottom up design from parts; indeed it is top down design where the realization of the design through patterns is the most important goal, not wiring parts together to make a whole. For example if you decide that all of the business rules must be factored into separate classes. One Rule One Place - behind an interface ( a new mantra ) . Basically you have to wrap families of business rules behind interfaces and attach them to some aggregate in the code. The business rule interfaces must be composed into algorithms in some way and the results have to be surfaced through some notification pattern. The resulting business rule interfaces must be interchangeable without the context classes that use them being aware of which variant they are executing. You can build the business rule interfaces with IOC containers driven from external configuration files. Still the architecture of how the business rule interfaces play in the domain model has to be designed properly. This kind of architecture has to come down from the top and has to drive the design of the system from the onset. There is always gong to be some aspect that is the really tough part of the design. It's where the ambiguities are and where there are not readily available models from other people that you can learn from. Domain Modeling is kind of like that since it's your LOB ( line of business ) , other people can't teach you the domain. The development of the initial code should realize some model of the high risk tough stuff as soon as possible. An example of a senior design context would be the decision to manage the dependencies of the application with an Inversion of Control container. That would require that the system be programmed to interfaces and not classes. Let's take extensibility and flexibility and see what are the architectural patterns that can help us there. For the most part these objectives are met with modern coding practices. Primarily SRP ( Single Responsibility Principle ). As far as patterns go there are at least two; Inversion Of Control containers which take over the creation aspects of how interfaces are assembled ( which motivates another practice design to interfaces and not classes ) and DM-VM-V ( DataModel , ViewModel, View ) which guides you to create user interface that is thin and can be changed easily. Testability is a design and project decision around which coding method you use and how unit tests are integrated into the solution. Testability is affected by building a presentation surface and keeping the user interface thin since there is no state left in the view. It's much easier to unit test the presentation code since it's non-visual (you don't need tools to flex the GUI ). Absent the requirements for easily changed and extended user interface you can merge the presentation and view layers. In case you are thinking this sounds like a bad design choice remember that all applications have a merged presentation and view, even Model View Controller applications. In MVC the View is the presentation; there is no separation into two layers. The model is the Domain Model, the controller is kind of like a presentation but it is not a surface for attaching a view. These are senior design contexts; they make the overall look of the software building that is under construction what it is. It is making design decisions at the onset and then doing OOA/OOD decomposition. It is coding form the top down and is well suited to agile and other adaptive coding practices. Open Closed Principle When there is new functionality or behavior added to a software system only new code should be added. In particular it should not be necessary to modify old code to add new behavior or features. This basically says if you keep in mind the SRP (Single Responsibility Principle) you should have a loosely coupled system. You wind up with more interfaces with clearer responsibilities. Systems like this are easy to extend because when you don't find code in the same location that is doing lots of different things. The open / closed principle which is supported by SRP is a big driver behind the notion of aggregates and the dependencies that result from them. It has a side effect of making deep dependency graphs which brings complexity back into the design of another sort. However there is a wonderful mechanism for managing the complexity of dependency. Inversion of Control containers are so vital to the realization of extensible software because they allow you to adhere to SRP and make these complex aggregates that are driven from external configuration files. Find What Varies and Encapsulate It This tends to cut down on inheritance hierarchies and goes against the objects as smart data paradigm. Good design is partially about the identification of variance (Commonality Analysis). Choosing the proper design for encapsulating variance behind an interface shields the rest of the system. This increases the clarity of the design. Object Composition vs Inheritance Whenever there is inheritance there is some specialization that is driven from variability that is occurring. Frequently the variance can be grouped together and factored into a separate implementation hierarchy. This avoids inheritance in the core of an aggregate and shifts the deferred inheritance to an adjunct subordinate class. Evans (Domain Driven Design Quickly) states that classes that describe, provide, or help the main concept classes are value classes. That description is a little vague but these classes do not represent top level concepts in the design, they are not the nouns in the problem domain. Classes that help the core of aggregates are the products of factorization and implement various interfaces in the service of the core concept. These helper class hierarchies are much less important in the design as there is a net gain by keeping the inheritance of the core to a minimum. We would rather have a class hierarchy in a dependency class then the root of an aggregate that is a main part of the design. This is known as a Bridge pattern and it helps with the Single Responsibility Principle. It keeps the core class focused on particular responsibilities which results in looser coupling. Be very stingy with the use of inheritance when you need to handle a special case in the code. Promote High Cohesion Cohesion is a measure of how tightly focused the code is. Another way of looking at it is how closely the operations in a routine are related. Systems that are highly cohesive are loosely coupled. Loosely coupled systems allow you to introduce new behaviors without changing unrelated code. Classes should be well defined and focused on specific roles in the design – and nothing else. Cohesive classes are only about one thing. Single Responsibility Principle Highly cohesive systems obey the Single Responsibility Principle or SRP for short. When classes get to big and do too many things break them apart along lines of responsibility. Use interfaces to group the responsibilities into readily identifiable chunks. This leads to software designs that consist of groups of Aggregates since a core abstraction is now served by subordinate classes implementing various interfaces. An Aggregate is a software structure that has a root class with dependencies on provider, descriptive or helper classes. These classes are wired together with dependencies either through the constructor or property setters. In the domain layer of a design the root of aggregate structures is usually an Entity (Entities have identity and are from the data services). In the presentation layer the root of an aggregate would be a View Model which is the central abstraction for providing a surface to bind a View onto. In either case the construction of the aggregates should be done with the help of an Inversion of Control container (factory on steroids). Identify Patterns The purpose of patterns is to define relationships among the concepts in the problem domain. Think about a software system in terms of the patterns that are present. The patterns are not as important as the relationships but give us a way to talk about them. Start With Context Patterns Identify the patterns that create the context for the other patterns. These should be your starting point. They represent the 'drivers' in your design. Fundamental design decisions you have made at the beginning that should shape the overall structure of the code. Work Inward From the Context Look at the remaining patterns and at any other patterns that you might have uncovered. From this set, pick the patterns that define the context for the patterns that would remain. Repeat. Refine the Design As you refine, always consider the context implied by the patterns. Do not forget your previous design decisions and introduce new ways of doing things. Context: Separation of Behavior from Structure Design a system to handle varying, but related, business rules, algorithms and policies. The system should be externally configurable as to the particular business rules, algorithms and policies that apply in a specific situation. Each algorithm, business rule and policy should be defined to be in a family containing all variations with a common interface. Commonality and variability analysis can be used to design the interface(s). The use of Inversion Of Control container factories allow us to configure the exact class that implements a given strategy interface through generic specialization. It is possible to use composition to build complex strategies that handle sets of business rules with increasing levels of semantic relevance to the design. Context: Use an Inversion of Control Container One of the biggest issues in software systems today is managing the dependencies between objects. When we build aggregates we are using delegation to subordinate classes in order to realize the implementation of interface that serve the core responsibility of the aggregate. Basically we have dependencies on classes that the core delegates too; these classes each have their own dependencies and the resulting graphs can get deep (think transitive closure). Unmanaged, dependencies can get out of control without you even noticing. You notice when you change a constructor for some class the code breaks in enough spots so that it is a major problem. It can be hard to instantiate an object because it needs some dependency that might be a number of levels down. IOC containers help you with those types of problems, they make the code so much more flexible and extensible. Inversion of Control and Dependency Injection are two related ways to break apart dependencies in your applications. They are explained in detail by Martin Fowler in his article Inversion of Control Containers and the Dependency Injection Pattern, but a few lines are in order. Basically Inversion of Control means that you are not writing the code to couple your classes together when you need to produce an aggregate. You are requesting an interface from the framework in this case the container factory possibly supplying some arguments that allow the IOC container to construct the interface. More importantly the dependencies are coupled together inside of the container either through the constructor or by setting properties ; this is known as Dependency Injection. The control is inverted because the code for wiring up the various objects is typically a key and important part of a software system. The precise classes that implement interfaces are ‘baked into the cake’, they cannot be changed, the dependencies are hard coded. The use of an IOC container changes all of that around, now the specifics are in external configuration files, the container is the only part of the system that knows the exact class type that implements an interface. In this sense the programmer is no longer in control of the classes and how they are coupled together. The framework is in control via the IOC container. There is another aspect to using an IOC container. That is you can think of the code as composed of code that is implementing the how of the responsibility and code that is implementing the responsibility. Basically when I use an IOC container then how I am building an interface and satisfying dependencies is removed, leaving the code that is using the interface. The result is more cohesive as a whole. The other interesting aspect is when additional responsibilities are added to an interface without the client knowing about it. There is a great example of using decorators to send email from a repository that you can read about here. Context: Headless Application Support multiple views that can be quickly developed and changed without impacting other layers of the application. This notion of a 'skin' that can be removed with a minimum of impact to the rest of the application is (finally) possible with .Net 3.5 and Windows Presentation Foundation. This type of design requires very extensive support for data binding along with an enhanced command system and routed events that tunnel as well as bubble through the view hierarchy. Use the composite pattern to create complex presentations from simpler ones that support subsections of the user interface. Complex screens can be implemented in manageable pieces and reused in other views. The new pattern for user interface is much more powerful then the familiar Model-View-Controller. There is a design that has been proposed at Microsoft known DataModel / ViewModel / View that promotes the use of thin views that can be removed without hurting the rest of the application. This enables the development of applications that have a set of explicitly defined 'surface' or attach points that contain presentation logic for various 'views'. The DM-VM-V pattern keeps the user interface logic out of the view layer and in the presentation surface. This enables testing of most of the view responsibility through normal unit testing methods since behavior and state is kept in the on visual presentation. This is a major advance in terms of testing the user interface of a modern application since tools that exercise visual control are much more involved than running unit tests. A typical presentation surface consists of an aggregate whose root class is known as a ViewModel or Presenter. The core is an aggregate that presents a binding surface for any data that is visualized in the view along with any properties that are exposed through controls. This binding surface should be in the form of an explicit interface that is implemented through delegation since the responsibility of the core is primarily composition. The other typically important interface is the command interface for a presentation model and how commands are invoked from the view and how command arguments are passed through to the presentation. The Command object itself is part of the presentation not the view layer. However arguments are accessed through data binding in a similar fashion as data and properties. There is also an interface that is queried by the view layer to determine if the command is available to be executed. There is no rule concerning how many View Model surfaces are required to support a given view. In actuality there is a at least one ViewModel for every dialog and or window. In a complex main view a composite of a root ViewModel composed of child ViewModels can be employed to support complex view hierarchies. View hierarchy is a common design in GUI code while using composition to make a mirrored presentation surface is a relatively new design concept that has emerged with the desire to create super thin ‘dumb’ view layers that can be changed and or replaced entirely without affecting the application logic. Supporting "Skins" Or Keeping the Presentation Logic in the ViewModel and out of the View The ability to have multiple looks or views on the same non visual presentation surface relies on careful enforcement of a key programming goal. That is presentation logic must be kept outside of the View classes. There can be no state or commands semantics mixed in with the View code. In normal application programming the presentation code is part of the view logic. Since there is no clear separation of concerns once the view logic has been removed the application is missing most if not all of the presentation state. Essentially you have no working application outside of the view layer. An example in .Net of mixing presentation logic with view logic is illustrated by the use of code-behind files in WPF XAML. Most of the code in the code behind sections of the XAML represents presentation logic; command delegates are a common example. TrackbacksThe trackback URL for this entry is: http://ekejma.spaces.live.com/blog/cns!8272788D0933F9E5!200.trak Weblogs that reference this entry
|
|
|