Create a permission service in the tenant system to take over user authentication and authorization. We call this service Go-easy-login

In this paper, the essence is the actual combat permission system of domain driven design micro service further summary and improvement, learning domain driven design itself is a gradual process, the training is in the field of object-oriented programming ideas and concepts, and past and present, including the future, most people just in object oriented clothing, doing the process oriented, oriented database of rough work, See why we need domain driven design, if you come into contact with domain driven design, but suffer from does not know how to begin, the concept is to understand but I don’t know how to practice, this will help you open the door to practice domain driven design, if you never understand a domain driven design, this paper is also introduction domain driven design one of the best articles, Take you to feel the extraordinary charm of domain driven.

The project structure

Code first, first show the directory structure of the code and the corresponding files, we can first YY corresponding role, and then with questions to read.

login
  	base
                encrypt.go
  		token.go
  		repoImpl.go
      domain
	  	   service
  			loginService.go
  			loginService_test.go
  		  loginUser.go
  		  loginUser_test.go
  	mocks
  		  EncryptHelper.go
  		  LoginUserRepo.go
Copy the code

How do you detach from technical details

Domain-driven design places more emphasis on business logic and the corresponding domains (models) that are established, without any technical details, i.e. databases, caches, etc. Object oriented is a simulation and reflection of external objective things, a User class should have the ability to eat,drink,play,happy and so on, a User can not have the ability to connect to the database, relying on any technical details is against object oriented programming. Then the question comes, I understand the truth, how to do it, if we are in a project, no technology to the words, is it to achieve our goal? (Reader: WTF, how?)

To create a new project, do not rely on any third-party packages, according to the function of what we want to do, do a tenant under the system of jurisdiction, take over the user’s authentication and authorization, we create a new LoginUserE to represent the land user, DoVerify perform authentication process, at the same time we hope to have the bill of landing, is decided by calling system encryption, This means that the LoginUserE domain needs to be able to get EncryptHelper from EncryptWay, so we’ll start with a new loginuser.go

package domain

type LoginUserE struct {
	Username     string
	IsLock       bool
	UniqueCode   string
	Mobile       string
	canLoginFunc func(a) bool
	EncryptWay
}

func (user *LoginUserE) CanLogin(a) bool {
	var can bool
	ifuser.canLoginFunc ! =nil {
		can = user.canLoginFunc()
	} else{ can = ! user.IsLock }return can
}

func (user *LoginUserE) DoVerify(sourceCode string, encryptedCode string) (bool, error) {
	if! user.CanLogin() {return false, errors.New("can not login")
	}
	match := user.EncryptHelper().Match(sourceCode, encryptedCode)
	return match, nil
}
Copy the code

The problem here is that EncryptHelper(), we know that encryption methods, like MD5, have to rely on other packages, loginUser.go we don’t want to rely on any third party packages, which seems to enter into a contradiction. In Alistair Cockburn’s hexagonal architecture, the Domain is inside the core, and other dependencies communicate through interfaces. In other words, the domain layer defines the interfaces, the infrastructure layer implements the interfaces, and we define the EncryptHelper interface

type EncryptHelper interface {
	Encrypt(password string) string
	Decrypt(password string) string
	Match(source, encryptedString string) bool
}
Copy the code

Then create encrypt.go in the Base infrastructure layer to implement this class

type MD5Way struct{}

func (md5 MD5Way) Match(source, encryptedString string) bool {
	return md5.Encrypt(source) == encryptedString
}

func (MD5Way) Encrypt(password string) string {
	data := []byte(password)
	md5Bytes := md5.Sum(data)
	return string(md5Bytes[:])
}

func (MD5Way) Decrypt(password string) string {
	panic("not support")}Copy the code

The problem has not been solved, the base layer of the concrete implementation class, how to make the domain layer is not directly dependent on at the same time, and can be used? The best approach is actually dependency injection, but introducing dependency injection falls into a different kind of paradox — dependency injection can be reduced to one of the technologies that doesn’t depend on any of the technical details. I’ll talk more about this below, and see how I can do it without dependency injection. Var EncryptMap = make(map[EncryptWay]EncryptHelper)

var EncryptMap = make(map[EncryptWay]EncryptHelper)

func (encryptWay EncryptWay) EncryptHelper(a) EncryptHelper {
	if helper, ok := EncryptMap[encryptWay]; ok {
		return helper
	} else {
		panic("can not find helper")}}func AddEncryptHelper(encryptWay EncryptWay, helper EncryptHelper) {
	EncryptMap[encryptWay] = helper
}
Copy the code

AddEncryptHelper registers the corresponding EncryptHelper for classes outside the core layer. This may seem like a good idea at first glance, but once you have more fields in your project, it increases the maintenance cost of your code. Dependency injection (DI) actually hides the process of building concrete implementation classes. Whether or not to introduce at the Domain level varies from person to person and project to project. If you have a better way, feel free to suggest it in the comments section.

Field service

If you are new to or have never been involved in domain-driven design, your thinking is pretty fixed, like why do we need domain-driven design? You’ve been doing analysis and design around spreadsheets for a long time, thinking about how we should build tables for certain features. I’m sure, in the process of interpretation of the above functions, you start thinking about how the table design, I draw on the table which field again how, this is absolutely be database, kidnapped by technology of thinking but also of the problem is that we always need to eventually solve this problem, the database services to solve the problem in the field. Another problem the domain service solves is assembly logic. For example, LoginUserE.DoVerify does not rely on any third party packages or other classes of the same class, but its input parameter is isolated by our dependencies. So let’s talk a little bit about how Login works, first we define LoginCmd as the Login entry,

//implemention will show right behind this
func (service *LoginService) Login(loginCmd common.LoginCmd) (string, error) 

type LoginCmd struct {
	Username         string
	TenantId         string
	EffectiveSeconds int
	Mobile           string
	SourceCode       string
	LoginWay         string
	EncryptWay       string
}
Copy the code

This is where we design the database. We need to find out if this user exists. So the problem is, we can’t rely on database technology directly, but we need it. The analogy, of course, is defining interfaces

type LoginUserRepo interface {
	GetOne(username, tenantId string) *domain.LoginUserDO
}
Copy the code

But this brings us back to dependency injection, which I still haven’t used for simplicity’s sake, but which I personally recommend

var loginService *LoginService
type LoginService struct {
	LoginUserRepo
}
func NewLoginService(repo LoginUserRepo,) *LoginService {
//do not argue to use double check lock,it's a example and does not hurt anyway
	if loginService == nil {
		return &LoginService{
			LoginUserRepo: repo,
		}
	} else {
		return loginService
	}
}
Copy the code

This allows us to pass the repoImpl when we initialize LoginService, achieving dependency isolation. Domain services do not need to know any warehousing methods, or even what database is used at the bottom. I only care about fetching and fetching. I only care about results, and that is the essence of defining interfaces.

Going back to GetOne(username, tenantId String) * domain.loginUserdo, another point exposed here is that the DataObject class is defined in the domain layer, not in the Service, and not in the base. One of the things I struggled with was, since the Domain layer doesn’t rely on database technology, should it also not care about DataObjects, and would dataObjects be better placed under the Base layer?

The reason that DataObject is now in the Domain layer is that

1. The domain core layer does not depend directly on other layers, which will be violated if DataObject is placed in the base layer; 2. As the interface definer, the domain layer has the right to define what it wants to store according to its own requirements, and other layers only need to comply and implement.Copy the code

At the same time, we didn’t want to have a lot of convert in our code, from CMD to DO, from DO to E, so we extracted the dto.go file to store concert code. The final code looks like this.

func (service *LoginService) Login(loginCmd common.LoginCmd) (string, error) {
	userDO := service.GetOne(loginCmd.Username, loginCmd.TenantId)
	userE := common.ToLoginUserE(*userDO)
	userE.EncryptWay = domain.EncryptWay(loginCmd.EncryptWay)

// Login way contains PASSWORD and SMS,encryptCode () is to get which one to be verify,so userE will not care about which way is exactly by logining
	encryptCode := service.encryptCode(loginCmd.LoginWay, userDO)
	if_, err := userE.DoVerify(loginCmd.SourceCode, encryptCode); err ! =nil {
		return "", err
	}

	//todo add login event and callback
	return service.token(userE.UniqueCode, loginCmd.EffectiveSeconds), nil
}

func (service *LoginService) encryptCode(way string, userDO *domain.LoginUserDO) string {
	switch way {
	case "PASSWORD":
		return userDO.Password
	case "SMS":
		return service.FindSmsCode(userDO.Mobile)
	default:
		panic("unknown login way")}}Copy the code

A new storm appeared, and the service. Token (userE UniqueCode, loginCmd. EffectiveSeconds) what is the meaning of this logic, this paper also does not appear, let me slowly to explain. LoginService () : loginService () : loginService () : loginService () : loginService () : loginService () : loginService () : loginService () : loginService (

type LoginService struct {
	LoginUserRepo
	token func(uniqueCode string, effectiveSeconds int) string
}

func NewLoginService(repo LoginUserRepo, token func(uniqueCode string, effectiveSeconds int) string) *LoginService {
	if loginService == nil {
		return &LoginService{
			LoginUserRepo: repo,
			token:         token,
		}
	} else {
		return loginService
	}
}
Copy the code

The final result

Copy the same, shield technical details, emphasize business logic, the ultimate goal is to achieve reusable business logic, organized into a reusable self-enclosed business model. We ended up with a model like this.

This business model will not be broken in any technology framework, any Web framework, or any other scenario, nor will it be affected by any database technology chosen. The details of the external technology will not be implemented with you. This article focuses on building the model, and the choice of technology will be made by yourself, which will not affect the model at all.

Test drive makes domain drive perfect

Down as a whole meaning lies in isolation, these are the accumulation of experience, there is no effective rules to follow, the answer is I don’t know, but if you comply with the test driver behavior, it forces you to think, what to rely on, what not to rely on, because all the third-party dependencies, all need to use the Mock to replace, This is why the mocks file exists in the directory. Well written tests, no worries.

conclusion

Looking back, this is the third article I’ve written on domain-driven design, with the goal of making domain-driven design easier for more people to understand and practice, writing good code, getting praise from successors, and improving code quality.

It’s a long way to go. Give me a thumbs up!

Akik PLZ called me a red scarf

Source: Domain-driven best Practices – Code that tells you how to do domain-driven design

Source code address: github.com/iamlufy/go-… As the code improves, you can check out git commit records

This blog welcome to reprint, but without the consent of the author must retain this paragraph of statement, and in the article page obvious location to give the original link, otherwise reserve the right to pursue legal responsibility.