Advertisement
Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Apr 11, 2023

The main secret of software architecture

Источник:
Просмотров:
1310

The main thing that you should know about Software Architecture reads as follows, “Graphical interface and data should be separated”. The worst thing you could do to your program is writing your code inside Form1.cs, HomeController.cs, MainWindow.cs etc. Do not write your code inside forms. Do not write your code inside controls. Do not write your code inside controllers.

This may sound familiar to you. After all, this is the main idea that any architecture patterns popularize. Do you know why stuff like MVC, MVP, MVVM, three layered architecture, DDD and so on, got so popular? Simply because they work.

Ask yourself, what is the purpose of your program? What it does? The purpose of any program is the storage and processing of data. This is how you should think about your program — as a data handler. Even though the graphical user interface (GUI) is a large part of your application, it is not the most important one. Surely the function of your program is not pressing buttons.

Start your program by designing the domain model. Classes that will contain the data and behavior of your domain, its business logic, concepts and rules. Do not think about your program as inputs and buttons. GUI depend on model and not the other way around.

Separate your model and user interface.

Beginner developers write their programs as on the left (do not do this way) where everything is centralized around GUI. When in reality it should be the right image with domain model as its core.

Define a Model

Domain model is responsible for describing processes as close as possible to the real world scenario. If you are writing calculator, your domain is calculation. If you are working on an online store, that is your model.

GUI is nothing more than infrastructure. It is the most frequently changing part of your program. Today you are using combobox, tomorrow table. Today you are using WPF, tomorrow new framework get introduced and you need to migrate to it.

Having code like this won’t allow you to do it:

class Form1
{
    private void calcButton_Click(object sender, System.EventArgs e)  
    {  
         var a = double.Parse(numberATextbox.Text);
         var b = double.Parse(numberBTextbox.Text);
        
         if (b == 0)
         {
            MessageBox.Show("Division by zero");
            return;
         }
        
         resultLabel.Text = (a / b).ToString();
    }  
}

There is no model at all! 😰

Even when your program is nothing more than CRUD you still need to move it to separate domain classes. You never know how it will evolve. But making a little effort now can save you hours in the future.

Explanation:

Defining your model has next benefits:

  1. it allows changing the GUI without changing the functionality. Same model can be reused across different GUI.
     
  2. model can be moved to a separate layer or even separate server.
     
  3. developing of GUI and model can be split between different developers.
     
  4. you can cover your model with unit tests (at least you will have something to cover 😁).
     
  5. separation of concern. Having domain code grouped together is easier to maintain and understand (the only way I like seeing spaghetti is on my plate 🍝).

Solution:

Analyze your requirements. Define what your model is. And develop classes around it.

In the example above, we are definitely working on a calculator, so let’s create a domain model for it:

class Calculator
{
    public double Divide(double a, double b)
    {
        return a / b;
    }
}

And then we can use it in our UI:

class Form1
{
    private void calcButton_Click(object sender, System.EventArgs e)  
    {  
         var a = double.Parse(numberATextbox.Text);
         var b = double.Parse(numberBTextbox.Text);
        
         var calculator = new Calculator();
         var result = calculator.Divide(a, b);
        
        resultLabel.Text = result.ToString();
    }  
}

Notice how our domain and GUI is separated.

It may seem like not worth doing 😒, but it’s just an example. In reality, your Model will be more than that.

Model should not know about GUI

Model should be separated from GUI. Do not let your Model know about inputs, controls and so on. Model should not know anything about user interface.

Give this example a look. This is how your model should not look:

class Calculator
{
    public double Divide(double a, double b)
    {
        if (b == 0)
        {
            MessageBox.Show("Division by zero");
            return 0;
        }
        
        return a / b;
    }
}

You can have one GUI. You can have multiple GUI. Your GUI could be different: desktop app, web pages, server API. You could have no GUI at all and provide your model as a library.

Imagine you have a model highly coupled with desktop UI. One day you decide to move it to a web server or sell it for commercial use. Model separated from GUI will allow you to do it. Highly coupled won’t.

Would you change your model in this case? Model is the foundation of your application. If the foundation changes, the whole building has to be rebuilt. Please, repaint your walls without demolishing the house.

Explanation:

Having pure, GUI agnostic model allows:

  1. to change the GUI without changing the functionality (déjà vu). Changes in GUI won’t require changes in model (jamais vu).
     
  2. to support multiple GUI.
     
  3. to write unit tests 🧪. No longer need in having those complex UI components in your tests.

Solution:

There are multiple ways to get rid of your GUI inside your model and each case should be discussed separately.

For the one with message box, we could just replace it with Exception and let the calling code to deal with it.

public double Divide(double a, double b)
{
    if (b == 0)
    {
        throw new InvalidOperationException("Division by zero");
    }
    
    return a / b;
}

The adherents of functional programming would claim that exceptions are nothing more than side effect which makes method signature dishonest. So likely they would go with Result class:

public Result<double?> Divide(double a, double b)
{
    if (b == 0)
    {
        return Result.Fail("Division by zero");
    }
    
    return Result.Success(a / b);
}

If having a call to GUI from your model is a must, the only acceptable solution in this case would be to use inversion of dependency.

Define an interface in your domain and implement it for each GUI separately:

namespace Domain
{
                          . . .
    public double? Divide(double a, double b, IOutput output)
    {
        if (b == 0)
        {
            output.Write("Division by zero");
            return null;
        }
        
        return a / b;
    }
                          . . .
}

namespace UI
{
    class Form1 : IOutput
    {
        private void calcButton_Click(object sender, System.EventArgs e)  
        {  
                          . . .
             var result = calculator.Divide(a, b, this);
                          . . .
        }  

        public void Write(string message)
        {
            MessageBox.Show(message);
        }
    }
}

Note:

So far, you’ve seen different ways to fix a mistake. Do you know what is the best one? Don’t make them 😁

Not experienced developers may not even notice how they allow GUI to seep through their Model. This usually happens when you don’t have separate layers. That is why I suggest moving Model to a separate project.

The difference between a separate project and a folder is that project allow you to manage dependencies.

Keep everything in one project only if you feel confident enough in what you are doing.

Can I calculate in my UI?

No!

UI should be pretty straightforward — get the input, pass it to model, display the current model’s state.

On the other hand, calculations such as adding two numbers, figuring out the price of an order, file processing and so on are complicated tasks which should be part of the model. After all, it is your domain.

Beginners design their programs like this:

While professionals would have it similar to this:

Some developers so desperately avoid defining their model, they would rather find a framework that do their calculations for them. Do you know what happens when a customer asks to change or update the framework? 😃

Explanation:

  1. calculation is concern of your Domain, so let it stay this way.
     
  2. UI should be responsible for displaying data and not for calculating domain rules.
     
  3. modern UI is already complicated enough, do not make it even worse.
     
  4. What if calculation is part of your UI and you need to move it to the server? Will you create UI library on the server? What if UI. developers decided to change their components? Will you adjust your model to it?
     
  5. imagine you have multiple UI components and each of them implements the same calculation a lit of bit different depending on the available API. One day you need to update the formula. Will you update every of them? What if you forget one?
     
  6. since your code is embedded in UI you have no classes, which results in no inheritance, no encapsulation, no OOP at all. Say goodbye to GoF patterns.
     
  7. changing UI is just a nightmare.
     
  8. at some point, you would need to perform the same calculation on another level. Do you like duplicating the code?
     
  9. unit tests 😒 I know, I know… Just could not resist 😀

Solution:

It’s simple. Just don’t do it 😁 Place all calculation inside your Model.

What about validation?

With validation is a little bit trickier. Validation is a cross-cutting concern that you would like to perform at everywhere level of your application. There are always be some validation at UI, however it does not mean that you can completely avoid validation on model level.

You can not rely on GUI validation, and expect that UI-developers did everything right. Always make sure that your domain model is in a valid state, even when it means code duplication.

With validation, follow the next rule:

perform simple validation (required field is not empty; user entered a number not a string) at GUI.

perform validation of business rules (order can not be registered without any products) at model level.

Back to our example:

class Form1
{
    private void calcButton_Click(object sender, System.EventArgs e)  
    {  
         var isAValid = double.TryParse(numberATextbox.Text, out double a);
         var isBValid = double.TryParse(numberBTextbox.Text, out double b);
        
         if (!isAValid) { MessageBox.Show("A is not valid"); return; }
         if (!isBValid) { MessageBox.Show("B is not valid"); return; }

         var calculator = new Calculator();
         var result = calculator.Divide(a, b);
        
        resultLabel.Text = result.ToString();
    }  
}

Notice that our Divide method works with correct data type, while all type conversion and validation is performed at UI level.

public double Divide(double a, double b)
{
    if (b == 0)
    {
        throw new InvalidOperationException("Division by zero");
    }
    
    return a / b;
}

However, there is validation of domain rules inside our model too.

Explanation:

The UI should be as simple as possible. It is responsible for getting data from the user and passing it to the model. All simple validation can be performed at the UI level, while complex business rules should be part of your model.

Solution:

Identify what are simple validation rules that can be performed at the UI level and what is specific to your domain.

Note:

From user experience, it may be unpleasant to receive an error during execution of an operation. Even consumers of your model would like to make sure methods can be called instead of actually calling them to get a validation error.

To validate inputs ahead, you can define a separate method that can be called on both UI and Model:

public (bool, string) CanDivide(double a, double b)
{
    if (b == 0)
    {
        return (false, "Division by zero");
    }
    
    return (true, string.Empty);
}

public double Divide(double a, double b)
{
    var (canDivide, errorMessage) = CanDivide(a, b);
    if (!canDivide)
    {
        throw new InvalidOperationException(errorMessage);
    }
    
    return a / b;
}

What about threads?

Having no model has its benefits 😃. You can mix everything together to get highly performed code. Different GUI frameworks could offer you several ways to execute your code, up to running it in multiple threads. And that is exactly why people get so confused when starting defining a model that was previously running in multiple threads.

In fact, threads have nothing to do with your model and architecture. Model describe your domain, while threads are a way of executing your code.

The architecture may or may not support multithreading. But the execution flow itself — is beyond the competence of the model.

Ideally, your business logic should describe pure rules, while calling code should care whenever it is executed in single or multiple threads. In practice, you would like to perform some complex logic in parallel. Just make sure you minimize the numbers of such cases.

Where do I start developing my application?

Start with your Model.

Do not think about your application as buttons and inputs. Start from analyzing requirements and defining a model.

Ideally, your application should not rely on GUI at all. Make it executable from console and only then add the GUI.

Imagine you need to build a car. Where would you start? When you start from UI, it is the same as starting building a car from body color and only then fitting the engine inside.

Explanation:

  1. Model is primary while UI is secondary. You can have no GUI at all. But you can not have a program without Model.
     
  2. GUI depends on Model and not the other way around.
     
  3. GUI changes more frequently than Model. In the case of Model change, GUI will change as well.
     
  4. Model is always the same, while GUI could be different (WinForms, WPF, ASP.NET etc).

Note:

It is much easier to show a mockup of your program to a customer than to explain classes and their relation. With existing wireframe, you may not resist and start creating your program from UI. Honestly, that how I do 😁

However, it does not mean you can start from the UI. I have seen multiple beginner developers start from GUI and leave the Model behind and later forget about it. So please, start from your Model.

Practical advices ✊

Use correct types

Use data types that best match the nature of your model.

Do not use string to store date, there is DateTime. Do not use DateTime when you only need the date part. Try DateOnly. Do not use string for numbers, arrays or complex types.

Use decimal for money instead of int or double. Even better, define your own type.

If you work with coordinates — use float or double, but don’t use int, even if the current requirement says that coordinates can only be integers. Today you are working with integers, and tomorrow you will need to handle fractions. Specification can change, but nature can not.

Do not use numerical types to store identifiers. Some values look like numbers, however in reality they aren’t. Phone number, number of your bank card and so on. All those are identifiers that better be string.

With numerical types, you can encounter next issues:

  • data overflow. Some identifiers are just too long to fit an integer value.
  • requirement can change. You may need to add letters in front of your bank card, which can result in rewriting tons of code.

How do you determine whenever it is an identifier or number indeed? Just ask yourself, does it make sense to add/multiply your values? If it does not behave like a number, then it just an identifier. Does it make sense to subtract one phone number from another? Probably not. Identifier it is.

Use collections that fit best your model. List<T> is not the only collection that exists. Dictionary<K, T> can store values with a unique key, like passports. It can result in efficient search and prevent from adding two passports with the same number.

Create own collections

Developers usually know they can define their own types. However, somehow they totally miss the part you can create your own collections.

If your model has a list of students, you can create a separate type for it:

class Students
{
    private List<Student> _students = new ();
                      .  .  .
}

Do not be afraid of inheritance, even when others say to prefer composition. With it, you can avoid redefining all the methods.

class Students: List<Student> { . . . }

You can restrict available methods with an interface:

interface IStudetns
{
    void Add(Student item);
}

The benefits of having your own collection is that you can encapsulate model specific logic there or just make your code neat.

For example, instead of writing like this:

students.Add(new Student("John", "Doe"));

You can simply go like this:

students.Add("John", "Doe");

Wrapping up

I believe this article completely shines out the importance of the Model, so let’s avoid repeating it one more time 😄

I just don’t understand how all those hard-working students learning about OOP, design patterns, SOLID, completely forget everything as soon as they find their first job.🤔

I am not saying procedural programming is a bad thing. However, can one build an application with God objects and functions? Surely you can. Is it justified? For a simple program, it may be. But I have not seen an airplane built on fronds and sticks. If it was good 40 years ago, it does not mean it is applicable now. Software evolve and the same with architecture. Your requirements likely will change, your code will become too complex and unmanageable. Beginners create programs that work, professionals create programs that work for decades.

There is another important aspect. Designing a model forces one to analyze the domain in more detail, revealing its features and regularities. In fact, it generally changes the whole thinking.

Being said that, everything described here is no dogma that must be used always and everywhere. There are always exceptions, there are always alternative approaches. Nevertheless, remember: the presence of at least some architecture is better than its complete absence.

I bet you have not even noticed how you managed to finish this article 😃. I hope you learn something new here, and will start thinking in terms of Model and not GUI. 😄

Похожее
Oct 20, 2023
BackgroundDo you remember the original black-and-white movie The Blob? Perhaps you saw only the recent remake. In either case, the story line was almost the same: A drip-sized, jellylike alien life form from outer space somehow makes it to Earth.Whenever...
Jul 23, 2023
Author: Kenji Elzerman
I wrote a few articles about design patterns. A few examples are the Repository Pattern (or the generic version) and the Strategy Pattern. Both are very effective when writing software. But I have also mentioned SOLID, which is a collection...
Jul 24, 2023
Author: Mihai Sandu
Small changes, Big wins: How little tweaks make your code easy on the eyeWhen we think of clean code, our brain jumps to SOLID principles, short classes, no comments, and good design patterns. While all of those apply, they are...
Aug 24, 2022
Author: Jaydeep Patil
In this article, we will learn about global exception handling implementation using .NET Core 6 Web API step-by-step.Agenda Introduction Implementation of Global Exception HandlingPrerequisites Visual Studio 2022 .NET...
Написать сообщение
Почта
Имя
*Сообщение


© 1999–2024 WebDynamics
1980–... Sergey Drozdov
Area of interests: .NET Framework | .NET Core | C# | ASP.NET | Windows Forms | WPF | HTML5 | CSS3 | jQuery | AJAX | Angular | React | MS SQL Server | Transact-SQL | ADO.NET | Entity Framework | IIS | OOP | OOA | OOD | WCF | WPF | MSMQ | MVC | MVP | MVVM | Design Patterns | Enterprise Architecture | Scrum | Kanban