Software Architecture

In this blog, I will write some notes about software architecture that I have learned through experience or through books.

What is software architecture
In the book "Architecting applications for the enterprise" by Dino Esposito and Andrea Saltarello, they have give a very apt definition. According to them architecture precisely refers to building a system for a client.

Software architecture is not an exact science. The principles are guidelines and not rules. Sometimes these principles might even be in conflict of each other. So the main point is building a system for a client - which means choosing the guidelines based on your needs. It all depends on the context.

What about all the -ities
While reading the book of standards is immensely boring, the ISO/IEC 9126 standard actually does a good job of categorizing the -ities. By the way, what is -ities? ities mean all the words applicable in software architecture that end with ity. For ex. Functionality, reliability, usability etc. The standard defines that there are 6 such categories
  • Functionality - based on requirements for suitability, security, inter-operability and compliance.
  • Reliability - capability of software to maintain a certain performance under special conditions. This includes maturity, fault tolerance and recoverability.
  • Usability - ability for a user to understand and use it easily.
  • Efficiency - performance in terms of appropriate time response and resource utilization.
  • Maintainability - ability to adopt for changes.
  • Portability - ability to port from one platform to another. 
  • Cohesion - High cohesion means in a function, class or library the set of responsibilities are strongly related. High cohesion is good, that is, if all responsibilities are strongly related and they are in one place (function, class or library) then they favor maintenance and re-usability. Conversely, if a function, class or library is taking on too many responsibilities, its tough to maintain and reuse. This is similar to Single Responsibility Principle.
  • Coupling - Two classes (X and Y), libraries or modules are said to be coupled if changes to one almost always require a change in other. And if X is not logically involved in the changes made to Y but it has to change because of the created dependency then they are considered as coupled. Low coupling is preferred.
  • Together - Cohesion and coupling go hand in hand. If two functions or classes which are part of a logical responsibility are together - that is fine. We don't need to take steps to decouple them. Although, in case of classes it is advisable to communicate via interfaces. A system which has low coupling and high cohesion generally is highly readable, maintainable, testable and reusable. The principle of separation of concerns means high cohesion and low coupling. Concerns and features are generally one and the same.
Composition vs Inheritance 
Inheritance is a pillar of OO programming. But it has subtle issues when it comes to fragile base classes, virtual members, constructors etc. Composition is a more defensive approach. For ex, let's say there is a User class and there is a need for RegisteredUser class. In composition, you will create a RegisteredUser class which will have a private User property. It is up to you to expose whatever public members of User you want to expose and whatever logic you want to keep between User and RegisteredUser. RegisteredUser has no access to internal members of User class and can not alter User class's behavior in any way. The composed classes have no explicit relationship so you can not substitute one for the other. If you want to do so, you can always use a IUser interface. In case of Inheritance by passing around one or the other type, some subtle bugs can come up. To avoid this during inheritance, it is important to follow the guidelines of Liskov's principle.

  • S - Single Responsibility Principle - Classes should be simple and focus on one main core task or single responsibility. A responsibility is a reason to change. Not following SRP results in huge classes that become unmanageable over time. But taking SRP to limits results in anemic classes which only have properties and no behavior. This however doesn't mean that we should not have DTOs or ViewModel classes. Their purpose is to carry data - for which they are perfectly valid.
  • O - Open Closed Principle - Modules should be open for extension and closed for modification. This principle promotes use of interfaces and composition. For ex, if a class uses a generic logging interface, it can be extended to use any logging mechanism without changing the code.
  • L - Liskov's Principle - Subclasses should be substitutable for their base classes. This is to avoid derived classes altering the behavior of base classes in a way that is not expected. It is therefore better to use composition than inheritance.
  • I - Interface Segregation Principle - Clients should not be forced to use interfaces they don't need. This is similar to SRP for interfaces. The principle says that we should try to avoid fat or thin interfaces. A thin interface doesn't really provide much value while a fat interface combines more than one responsibilities and the consumer has to implement functions for other responsibilities even if they don't need them.
  • D - Dependency Inversion Principle - High level modules should not depend upon low level modules. Both should depend upon abstractions. This is similar to programming to interfaces rather than implementations. If two modules/classes/components are not really related but need to communicate, they should do it via interfaces. Then you are left with the problem of how to convert those interfaces into concrete classes and how to pass it to the external component. For that Dependency Injection Pattern can be used. Injecting dependencies via constructors is more preferable since it allows you to see upfront and in one place all the dependencies of a class.
Other Vectors (just guidelines not principles)
  • KISS - Keep it simple stupid. It means that we should write only the logic that is necessary and avoid unneeded complexity.
  • YAGNI - You ain't gonna need it. Similar to KISS.
  • DRY - Don't repeat yourself. Avoid code duplication.
Domain Driven Design - DDD is about crunching knowledge about a given business domain and producing a software model that faithfully mirrors it. The focus here is on behavior rather than data. Few of the important aspects of DDD are noted below -
  • Ubiquitous language - is a vocabulary shared by all parties involved in the project. It is used in both documentation and discussions. It also helps in naming and structuring the classes and properties and other constructs. It promotes clarity and minimizes assumptions. The language emerges out of numerous discussions that business teams have with development teams.
  • Bounded Context - areas of the domain that are better treated independently because of their own ubiquitous language. So when analyzing the domain, we find that same ubiquitous language is not applicable yet the systems are connected - it is helpful to treat it as a separate bounded context with its own architecture.
  • Domain Model - it is a conceptual view of a business domain. One bounded context has one domain model.
  • Modules - Within a bounded context, a domain model is sub divided into modules. A module is similar to a .NET namespace. One bounded context or domain model can have several modules. A module consists of entities and value objects. Both are .NET classes.
  • Value Objects - Object whose attributes never change after instantiation and if they change the object becomes a new instance which is fully identified by new collection of attributes. For ex. Address is a good example of a value object.
  • Entities - When uniqueness is important to a specific object i.e when an attribute such as ID is required - then that object is an entity. Entities have both data and behavior.
  • Persistence of Entities - A domain model doesn't directly take care of its own persistence. Repositories takes care of persistence on behalf of entities.
  • Aggregates - When few individual entities are being constantly used and referenced together - the collection of entities is termed as aggregate. It is common to first decompose the domain model into aggregates and then into entities and value objects. Although they are separate entities, we treat them as a single unit for the purposes of data changes. Each aggregate has a root entity which is called aggregate root. Aggregate also represent invariant conditions in the domain model. An invariant condition is essentially a business rule that is required to be constantly verified in the domain model. An aggregate root can have an interface that can have properties or methods that check these invariant conditions.
  • Domain Services - Any logic that spans across multiple aggregates is placed in classes called domain services. Ideally, if multiple entities in the same aggregate need some logic, that logic should go in aggregate root but if multiple aggregates are involved - domain services come into the picture. For ex. a checkout operation requires checking availability of product, checking price changes and checking shipping estimates. This logic can't be part of one aggregate.
  • Repositories - are domain services responsible for persisting aggregates. One repository per aggregate root is advisable. It is in the repositories where actual data access code using connection strings or commands would be written. For ex, CustomerRepository, OrderRepository etc.
  • Domain Events - Similar to events. When in requirements the 'when' adverb is used, most of the times it means that some event is going to take place 'when' something else happens. Instead of writing sequential code, it is desirable to break into events. This allows for defining a list of handlers without touching the code that raised the event. It also decouples the handler from the event meaning same handlers can be run in case of different events.
  • Some similarity with relation databases - entities correspond to tables. Aggregates are union of tables linked together by FK relationships. And Aggregate root is the unique table in a graph of FK relationships that contain only outbound FK references.
When to use DDD - If the software project has some level of complexity, will need to be maintained for some time and will be updated for changes - it is advisable to look into DDD. DDD is more about analysis and mirroring the actual business domain - and the closer the software is to the business domain the better. If the need is for a simple or a one-time application - then we don't need to put much emphasis on DDD. If the job can be done by using say for example, wordpress, then also we don't need to worry about DDD. The point is to not create any unnecessary complexity.

No comments: